WIP working on process diffs and notifications

This commit is contained in:
AnisHADJARAB 2024-12-12 09:44:06 +00:00
parent 4afdcf9a7a
commit 52f881981b
16 changed files with 414 additions and 54 deletions

View File

@ -12,7 +12,6 @@
</head> </head>
<body> <body>
<div id="header-container"></div> <div id="header-container"></div>
<div id="containerId" class="container"> <div id="containerId" class="container">
<!-- 4NK Web5 Solution --> <!-- 4NK Web5 Solution -->
</div> </div>

View File

@ -80,6 +80,7 @@ body {
.nav-wrapper { .nav-wrapper {
position: fixed; position: fixed;
z-index: 2;
background: radial-gradient(circle, white, var(--primary-color)); background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */ /* background-color: #CFD8DC; */
display: flex; display: flex;

View File

@ -70,20 +70,20 @@ function hideSomeFunctionnalities() {
} }
} }
async function setNotification(notifications: INotification[]): Promise<void> { async function setNotification(notifications: any[]): Promise<void> {
const badge = document.querySelector('.notification-badge') as HTMLDivElement; const badge = document.querySelector('.notification-badge') as HTMLDivElement;
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement; const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
if (notifications?.length) { if (notifications?.length) {
badge.innerText = notifications.length.toString(); badge.innerText = notifications.length.toString();
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement; const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
notificationBoard.querySelectorAll('.notification-element')?.forEach(elem => elem.remove())
noNotifications.style.display = 'none'; noNotifications.style.display = 'none';
for (const notif of notifications) { for (const notif of notifications) {
const notifElement = document.createElement('div'); const notifElement = document.createElement('div');
notifElement.className = 'notification-element'; notifElement.className = 'notification-element';
notifElement.setAttribute('notif-id', notif.id.toString()); notifElement.setAttribute('notif-id', notif.states[0]?.commited_in.toString());
notifElement.innerHTML = ` notifElement.innerHTML = `
<div>${notif.title}</div> <div>${notif.states[0]?.commited_in}</div>
<div>${notif.description}</div>
`; `;
// this.addSubscription(notifElement, 'click', 'goToProcessPage') // this.addSubscription(notifElement, 'click', 'goToProcessPage')
notificationBoard.appendChild(notifElement); notificationBoard.appendChild(notifElement);

View File

@ -0,0 +1,70 @@
.validation-modal {
display: block; /* Show the modal for demo purposes */
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
height: fit-content;
}
.modal-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.validation-box {
margin-bottom: 15px;
width: 100%;
}
.expansion-panel-header {
background-color: #e0e0e0;
padding: 10px;
cursor: pointer;
}
.expansion-panel-body {
display: none;
background-color: #fafafa;
padding: 10px;
border-top: 1px solid #ddd;
}
.expansion-panel-body pre {
background-color: #f6f8fa;
padding: 10px;
border-left: 4px solid #d1d5da;
overflow-x: auto;
}
.diff {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.diff-side {
width: 48%;
padding: 10px;
}
.diff-old {
background-color: #fee;
border: 1px solid #f00;
}
.diff-new {
background-color: #e6ffe6;
border: 1px solid #0f0;
}
.radio-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}

View File

@ -0,0 +1,62 @@
<style>
</style>
<div id="validation-modal" class="validation-modal">
<div class="modal-content">
<div class="modal-title">Validate</div>
<div class="validation-box">
<div class="expansion-panel">
<div class="expansion-panel-header">Validation 1</div>
<div class="expansion-panel-body">
<div class="radio-buttons">
<label>
<input type="radio" name="validation1" value="old">
Keep Old
</label>
<label>
<input type="radio" name="validation1" value="new">
Keep New
</label>
</div>
<div class="diff">
<div class="diff-side diff-old">
<pre>-old line</pre>
</div>
<div class="diff-side diff-new">
<pre>+new line</pre>
</div>
</div>
</div>
</div>
<div class="expansion-panel">
<div class="expansion-panel-header">Validation 2</div>
<div class="expansion-panel-body">
<div class="radio-buttons">
<label>
<input type="radio" name="validation2" value="old">
Keep Old
</label>
<label>
<input type="radio" name="validation2" value="new">
Keep New
</label>
</div>
<div class="diff">
<div class="diff-side diff-old">
<pre>-foo\n-bar</pre>
</div>
<div class="diff-side diff-new">
<pre>+baz</pre>
</div>
</div>
</div>
</div>
</div>
<div class="modal-action">
<button onclick="validate()">Validate</button>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
document.querySelectorAll('.expansion-panel-header').forEach(header => {
header.addEventListener('click', function(event) {
const target = event.target as HTMLElement
const body = target.nextElementSibling as HTMLElement;
if(body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
});
});
function validate() {
console.log('==> VALIDATE')
}
window.validate = validate

View File

@ -17,3 +17,14 @@ export interface IMessage {
id: string; id: string;
message: any; message: any;
} }
export interface UserDiff {
new_state_merkle_root: string, // TODO add a merkle proof that the new_value belongs to that state
field: string,
previous_value: string,
new_value: string,
notify_user: boolean,
need_validation: boolean,
// validated: bool,
proof: any, // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
}

View File

@ -0,0 +1,50 @@
import processHtml from './process-element.html?raw'
import processScript from './process-element.ts?raw'
import processCss from '../../4nk.css?raw'
import { initProcessElement } from './process-element';
export class ProcessListComponent extends HTMLElement {
_callback: any;
id: string = '';
zone: string = '';
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
console.log('CALLBACK PROCESS LIST PAGE')
this.render();
setTimeout(() => {
initProcessElement(this.id, this.zone);
}, 500);
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if(this.shadowRoot) this.shadowRoot.innerHTML = `
<style>
${processCss}
</style>${processHtml}
<script type="module">
${processScript}
</scipt>
`;
}
}
if (!customElements.get('process-4nk-component')) {
customElements.define('process-4nk-component', ProcessListComponent);
}

View File

@ -1,17 +1,21 @@
import { interpolate } from '../../utils/html.utils'; import { interpolate } from '../../utils/html.utils';
import Services from '../../services/service'; import Services from '../../services/service';
import { Process } from 'pkg/sdk_client';
import { getCorrectDOM } from '~/utils/document.utils';
let currentPageStyle: HTMLStyleElement | null = null; let currentPageStyle: HTMLStyleElement | null = null;
export async function initProcessElement(id: string, zone: string) { export async function initProcessElement(id: string, zone: string) {
const processes = await getProcesses(); const processes = await getProcesses();
const currentProcess = processes.find((process) => process[0] === id)[1]; const container = getCorrectDOM('process-4nk-component');
await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' }); // const currentProcess = processes.find((process) => process[0] === id)[1];
const wrapper = document.querySelector('.process-container'); // const currentProcess = {title: 'Hello', html: '', css: ''};
if (wrapper) { // await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' });
wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' }); // const wrapper = document.querySelector('.process-container');
injectCss(currentProcess.css); // if (wrapper) {
} // wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' });
// injectCss(currentProcess.css);
// }
} }
async function loadPage(data?: any) { async function loadPage(data?: any) {
@ -39,7 +43,7 @@ function removeCss() {
} }
} }
async function getProcesses(): Promise<any[]> { async function getProcesses(): Promise<Record<string, Process>> {
const service = await Services.getInstance(); const service = await Services.getInstance();
const processes = await service.getProcesses(); const processes = await service.getProcesses();
return processes; return processes;

View File

@ -0,0 +1,48 @@
import processHtml from './process.html?raw'
import processScript from './process.ts?raw'
import processCss from '../../4nk.css?raw'
import { init } from './process';
export class ProcessListComponent extends HTMLElement {
_callback: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
console.log('CALLBACK PROCESS LIST PAGE')
this.render();
setTimeout(() => {
init();
}, 500);
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if(this.shadowRoot) this.shadowRoot.innerHTML = `
<style>
${processCss}
</style>${processHtml}
<script type="module">
${processScript}
</scipt>
`;
}
}
if (!customElements.get('process-list-4nk-component')) {
customElements.define('process-list-4nk-component', ProcessListComponent);
}

View File

@ -1,10 +1,11 @@
import { addSubscription } from '../../utils/subscription.utils'; import { addSubscription } from '../../utils/subscription.utils';
import { navigate } from '../../router';
import Services from '../../services/service'; import Services from '../../services/service';
import { getCorrectDOM } from '~/utils/html.utils';
// Initialize function, create initial tokens with itens that are already selected by the user // Initialize function, create initial tokens with itens that are already selected by the user
export async function init() { export async function init() {
const element = document.querySelector('select') as HTMLSelectElement; const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
const element = container.querySelector('select') as HTMLSelectElement;
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside // Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper); if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper);
@ -117,8 +118,9 @@ function openOptions(e: Event) {
// Function that create a token inside of a wrapper with the given value // Function that create a token inside of a wrapper with the given value
function createToken(wrapper: HTMLElement, value: any) { function createToken(wrapper: HTMLElement, value: any) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
const search = wrapper.querySelector('.search-container'); const search = wrapper.querySelector('.search-container');
const inputInderline = document.querySelector('.selected-processes'); const inputInderline = container.querySelector('.selected-processes');
// Create token wrapper // Create token wrapper
const token = document.createElement('div'); const token = document.createElement('div');
token.classList.add('selected-wrapper'); token.classList.add('selected-wrapper');
@ -297,7 +299,9 @@ function removeToken(e: Event) {
// Remove token attribute // Remove token attribute
(target.parentNode as any)?.remove(); (target.parentNode as any)?.remove();
dropdown?.classList.remove('active'); dropdown?.classList.remove('active');
const process = document.querySelector('#' + target.dataset.option); const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
const process = container.querySelector('#' + target.dataset.option);
process?.remove(); process?.remove();
} }
@ -351,8 +355,10 @@ addSubscription(document, 'click', () => {
}); });
async function showSelectedProcess(elem: MouseEvent) { async function showSelectedProcess(elem: MouseEvent) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
if (elem) { if (elem) {
const cardContent = document.querySelector('.process-card-content'); const cardContent = container.querySelector('.process-card-content');
const processes = await getProcesses(); const processes = await getProcesses();
const process = processes.find((process: any) => process[1].title === elem); const process = processes.find((process: any) => process[1].title === elem);
@ -381,8 +387,9 @@ async function showSelectedProcess(elem: MouseEvent) {
} }
function select(event: Event) { function select(event: Event) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const oldSelectedProcess = document.querySelector('.selected-process-zone'); const oldSelectedProcess = container.querySelector('.selected-process-zone');
oldSelectedProcess?.classList.remove('selected-process-zone'); oldSelectedProcess?.classList.remove('selected-process-zone');
if (target) { if (target) {
target.classList.add('selected-process-zone'); target.classList.add('selected-process-zone');
@ -392,13 +399,22 @@ function select(event: Event) {
} }
function goToProcessPage() { function goToProcessPage() {
const target = document.querySelector('.selected-process-zone'); const container = getCorrectDOM('process-list-4nk-component') as HTMLElement
const target = container.querySelector('.selected-process-zone');
console.log('🚀 ~ goToProcessPage ~ event:', target); console.log('🚀 ~ goToProcessPage ~ event:', target);
if (target) { if (target) {
const process = target?.getAttribute('process-id'); const process = target?.getAttribute('process-id');
console.log('=======================> going to process page', process); console.log('=======================> going to process page', process);
navigate('process-element/' + process); // navigate('process-element/' + process);
document.querySelector('process-list-4nk-component')?.dispatchEvent(
new CustomEvent('processSelected', {
detail: {
process: process,
},
}),
);
} }
} }

View File

@ -5,6 +5,7 @@ import Services from './services/service';
import { cleanSubscriptions } from './utils/subscription.utils'; import { cleanSubscriptions } from './utils/subscription.utils';
import { LoginComponent } from './pages/home/home-component'; import { LoginComponent } from './pages/home/home-component';
import { prepareAndSendPairingTx } from './utils/sp-address.utils'; import { prepareAndSendPairingTx } from './utils/sp-address.utils';
import ModalService from './services/modal.service';
export {Services}; export {Services};
const routes: { [key: string]: string } = { const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html', home: '/src/pages/home/home.html',
@ -48,7 +49,7 @@ async function handleLocation(path: string) {
const accountComponent = document.createElement('login-4nk-component'); const accountComponent = document.createElement('login-4nk-component');
accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;') accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;')
if(container) container.appendChild(accountComponent) if(container) container.appendChild(accountComponent)
} else { } else if(path !== 'process') {
const html = await fetch(routeHtml).then((data) => data.text()); const html = await fetch(routeHtml).then((data) => data.text());
content.innerHTML = html; content.innerHTML = html;
} }
@ -56,10 +57,21 @@ async function handleLocation(path: string) {
await new Promise(requestAnimationFrame); await new Promise(requestAnimationFrame);
injectHeader(); injectHeader();
// const modalService = await ModalService.getInstance()
// modalService.injectValidationModal()
switch (path) { switch (path) {
case 'process': case 'process':
const { init } = await import('./pages/process/process'); // const { init } = await import('./pages/process/process');
init(); const { ProcessListComponent } = await import('./pages/process/process-list-component');
const container2 = document.querySelector('#containerId');
const accountComponent = document.createElement('process-list-4nk-component');
if (!customElements.get('process-list-4nk-component')) {
customElements.define('process-list-4nk-component', ProcessListComponent);
}
accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;')
if(container2) container2.appendChild(accountComponent)
break; break;
case 'process-element': case 'process-element':

View File

@ -9,6 +9,23 @@ self.addEventListener('activate', (event) => {
// Event listener for messages from clients // Event listener for messages from clients
self.addEventListener('message', async (event) => { self.addEventListener('message', async (event) => {
const data = event.data; const data = event.data;
if(data.type === 'START') {
const fetchNotifications = async () => {
const itemsWithFlag = await getAllItemsWithFlag();
// Process items with the specific flag
itemsWithFlag?.forEach(item => {
console.log(item); // Do something with each flagged item
});
event.ports[0].postMessage({
type: "NOTIFICATIONS",
data: itemsWithFlag
});
}
fetchNotifications()
setInterval(fetchNotifications, 2 * 60 * 1000);
}
if (data.type === 'ADD_OBJECT') { if (data.type === 'ADD_OBJECT') {
try { try {
@ -47,3 +64,21 @@ async function openDatabase() {
}; };
}); });
} }
async function getAllItemsWithFlag() {
const db = await openDatabase();
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = (event) => {
const allItems = event.target.result;
const itemsWithFlag = allItems.filter(item => item);
resolve(itemsWithFlag);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

View File

@ -1,3 +1,5 @@
import Services from "./service";
class Database { class Database {
private static instance: Database; private static instance: Database;
private db: IDBDatabase | null = null; private db: IDBDatabase | null = null;
@ -27,6 +29,11 @@ class Database {
options: { autoIncrement: true }, options: { autoIncrement: true },
indices: [], indices: [],
}, },
AnkDiff: {
name: 'diffs',
options: { key: 'new_state_merkle_root'},
indices: []
}
}; };
// Private constructor to prevent direct instantiation from outside // Private constructor to prevent direct instantiation from outside
@ -51,7 +58,7 @@ class Database {
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => { Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
if (!db.objectStoreNames.contains(name)) { if (!db.objectStoreNames.contains(name)) {
let store = db.createObjectStore(name, options); let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
indices.forEach(({ name, keyPath, options }) => { indices.forEach(({ name, keyPath, options }) => {
store.createIndex(name, keyPath, options); store.createIndex(name, keyPath, options);
@ -107,7 +114,12 @@ class Database {
// Set up the message channels // Set up the message channels
this.messageChannel.port1.onmessage = this.handleAddObjectResponse; this.messageChannel.port1.onmessage = this.handleAddObjectResponse;
this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse; this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse;
registration.active?.postMessage(
{
type: 'START',
},
[this.messageChannel.port2],
);
// Optionally, initialize service worker with some data // Optionally, initialize service worker with some data
} catch (error) { } catch (error) {
console.error('Service Worker registration failed:', error); console.error('Service Worker registration failed:', error);
@ -131,8 +143,13 @@ class Database {
} }
} }
private handleAddObjectResponse = (event: MessageEvent) => { private handleAddObjectResponse = async (event: MessageEvent) => {
console.log('Received response from service worker (ADD_OBJECT):', event.data); const data = event.data
console.log('Received response from service worker (ADD_OBJECT):', data);
if(data.type === 'NOTIFICATIONS') {
const service = await Services.getInstance()
service.setNotifications(data.data)
}
}; };
private handleGetObjectResponse = (event: MessageEvent) => { private handleGetObjectResponse = (event: MessageEvent) => {

View File

@ -1,7 +1,7 @@
import modalHtml from '../components/login-modal/login-modal.html?raw'; import modalHtml from '../components/login-modal/login-modal.html?raw';
import modalScript from '../components/login-modal/login-modal.js?raw'; import modalScript from '../components/login-modal/login-modal.js?raw';
import validationModalStyle from '../components/validation-modal/validation-modal.css?raw';
import Services from './service'; import Services from './service';
import { U32_MAX } from './service';
import { navigate } from '../router'; import { navigate } from '../router';
import { addressToEmoji } from '../utils/sp-address.utils'; import { addressToEmoji } from '../utils/sp-address.utils';
import { RoleDefinition } from 'pkg/sdk_client'; import { RoleDefinition } from 'pkg/sdk_client';
@ -51,6 +51,23 @@ export default class ModalService {
document.head.appendChild(script); document.head.appendChild(script);
} }
} }
async injectValidationModal() {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text());
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/validation-modal/validation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
const css = document.createElement('style');
css.innerText = validationModalStyle;
document.head.appendChild(css);
}
}
// this is kind of too specific for pairing though // this is kind of too specific for pairing though
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, commitmentTx: string, merkleRoot: string) { public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, commitmentTx: string, merkleRoot: string) {

View File

@ -21,7 +21,7 @@ export default class Services {
private pairedAddresses: string[] = []; private pairedAddresses: string[] = [];
private sdkClient: any; private sdkClient: any;
private processes: IProcess[] | null = null; private processes: IProcess[] | null = null;
private notifications: INotification[] | null = null; private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
private database: any; private database: any;
private routingInstance!: ModalService; private routingInstance!: ModalService;
@ -538,30 +538,35 @@ export default class Services {
} }
getNotifications(): INotification[] { getNotifications(): any[] | null {
return [ // return [
{ // {
id: 1, // id: 1,
title: 'Notif 1', // title: 'Notif 1',
description: 'A normal notification', // description: 'A normal notification',
sendToNotificationPage: false, // sendToNotificationPage: false,
path: '/notif1', // path: '/notif1',
}, // },
{ // {
id: 2, // id: 2,
title: 'Notif 2', // title: 'Notif 2',
description: 'A normal notification', // description: 'A normal notification',
sendToNotificationPage: false, // sendToNotificationPage: false,
path: '/notif2', // path: '/notif2',
}, // },
{ // {
id: 3, // id: 3,
title: 'Notif 3', // title: 'Notif 3',
description: 'A normal notification', // description: 'A normal notification',
sendToNotificationPage: false, // sendToNotificationPage: false,
path: '/notif3', // path: '/notif3',
}, // },
]; // ];
return this.notifications
}
setNotifications(notifications: any[]) {
this.notifications = notifications
} }
async importJSON(content: any): Promise<void> { async importJSON(content: any): Promise<void> {