diff --git a/dnas/how/zomes/coordinator/how/src/document.rs b/dnas/how/zomes/coordinator/how/src/document.rs index 7bf1cb0..7ecec48 100644 --- a/dnas/how/zomes/coordinator/how/src/document.rs +++ b/dnas/how/zomes/coordinator/how/src/document.rs @@ -79,6 +79,22 @@ fn get_documents_inner(base: EntryHash) -> HowResult> { get_input.push(GetInput::new(hash, GetOptions::default())) } } + _get_docs(get_input) +} + +#[hdk_extern] +pub fn get_document(hash: EntryHash) -> ExternResult { + let mut get_input= vec!(); + let x = GetInput::new(hash.try_into().map_err(|_e| wasm_error!("hash error"))?, GetOptions::default()); + get_input.push(x); + let result = _get_docs(get_input)?; + if result.len() != 1 { + return Err(wasm_error!(WasmErrorInner::Guest(String::from("could not find document")))); + } + Ok(result[0].clone()) +} + +fn _get_docs(get_input: Vec) -> HowResult> { let document_elements = HDK.with(|hdk| hdk.borrow().get_details(get_input))?; diff --git a/ui/src/elements/how-document.ts b/ui/src/elements/how-document.ts index 5949a66..6dec3de 100644 --- a/ui/src/elements/how-document.ts +++ b/ui/src/elements/how-document.ts @@ -5,7 +5,7 @@ import { consume } from '@lit/context'; import { StoreSubscriber } from "@holochain-open-dev/stores"; import {sharedStyles} from "../sharedStyles"; -import {EntryHashB64, encodeHashToBase64} from "@holochain/client"; +import {EntryHashB64, decodeHashFromBase64, encodeHashToBase64} from "@holochain/client"; import {howContext, Section, SectionType, SourceManual, Document, DocType, HilightRange, CommentInfo, Comment, CommentStatus, MarkTypes, MarkDocumentInput, CommentAction, applyApprovedComments, CommentStats, DocumentStats, DocumentAction, VoteAction, ApprovalAction, Dictionary, Unit} from "../types"; import {HowStore} from "../how.store"; import {ScopedElementsMixin} from "@open-wc/scoped-elements"; @@ -23,6 +23,7 @@ import { ActionHash } from "@holochain/client"; import { HowConfirm } from "./how-confirm"; import { CommentControl, Control } from "../controls"; import {until} from 'lit-html/directives/until.js'; +import { hrlB64WithContextToRaw, hrlWithContextToB64 } from "../util"; /** * @element how-document @@ -397,6 +398,15 @@ import {until} from 'lit-html/directives/until.js'; return false } + private async addAttachment() { + if (this._store.weClient) { + const hrl = await this._store.weClient.userSelectHrl() + if (hrl) { + const doc : Document = this._documents.value[this.currentDocumentEh] + await this._store.markDocument(this.path, [{hash: doc.documentHash!, mark: JSON.stringify(hrlWithContextToB64(hrl)), markType: MarkTypes.Attachment}]) } + } + } + private sectionRow(doc:Document, section: Section, index: number, comments:Array, isSteward: boolean) : TemplateResult { let commentsHTML if (this.canSeeComments(doc, section)) { @@ -543,6 +553,57 @@ import {until} from 'lit-html/directives/until.js'; > `) } + if (this._store.weClient) { + const isSteward = unit.stewards.includes(this._store.myAgentPubKey) + if (isSteward) { + affordancesHTML.push(html` +
+ { + const attachment = { hrl: [this._store.dnaHash, decodeHashFromBase64(this.currentDocumentEh)], context: {} } + // @ts-ignore + this._store.weClient?.hrlToClipboard(attachment) + }} + > + this.addAttachment()} + > +
`) + } + for (const mark of doc.marks.filter(m=>m.markType==MarkTypes.Attachment)) { + const attachment = JSON.parse(`${mark.mark}`) + affordancesHTML.push(html` +
+ ${until(this._store.weClient.entryInfo(hrlB64WithContextToRaw(attachment).hrl) + .then(res=> { + if (res) { + const entryInfo = res.entryInfo + return html` + ${entryInfo.name} + + { + const hrl = hrlB64WithContextToRaw(attachment) + // @ts-ignore + this._store.weClient.openHrl(hrl.hrl, hrl.context) + }} + > + ` + }} + ), + html`...` + )} +
+ `) + } + } } let tasksHTML: Array = [] if (docStats.emptySections > 0) { diff --git a/ui/src/elements/how-unit.ts b/ui/src/elements/how-unit.ts index f1dc0f0..6547115 100644 --- a/ui/src/elements/how-unit.ts +++ b/ui/src/elements/how-unit.ts @@ -307,9 +307,6 @@ export class HowUnit extends ScopedElementsMixin(LitElement) { ` } - else { - collectionsHTML = html`collections: ${collections.length}` - } return html`
diff --git a/ui/src/elements/svg-icons.ts b/ui/src/elements/svg-icons.ts index da0f4c4..0a9d166 100644 --- a/ui/src/elements/svg-icons.ts +++ b/ui/src/elements/svg-icons.ts @@ -74,6 +74,8 @@ export const SVG = { `, collect: ``, - chevron: `` - - } \ No newline at end of file + chevron: ``, + paperclip: ``, + clipboard: `` + } + diff --git a/ui/src/holochain-app.ts b/ui/src/holochain-app.ts index 9ffc0d2..c7ed7f5 100644 --- a/ui/src/holochain-app.ts +++ b/ui/src/holochain-app.ts @@ -16,6 +16,7 @@ import { AdminWebsocket, AppAgentClient, AppAgentWebsocket, + encodeHashToBase64, } from '@holochain/client'; import { provide } from '@lit/context'; import { LitElement, css, html } from 'lit'; @@ -28,10 +29,19 @@ import {howContext} from "./types" import { localized, msg } from '@lit/localize'; import { ScopedElementsMixin } from "@open-wc/scoped-elements"; -import { WeClient, isWeContext, initializeHotReload } from '@lightningrodlabs/we-applet'; +import { WeClient, isWeContext, initializeHotReload, HrlWithContext, Hrl } from '@lightningrodlabs/we-applet'; +import { appletServices } from './we'; +import { HowUnit } from './elements/how-unit'; +import { HowDocument } from './elements/how-document'; const appId = 'how' +enum RenderType { + App, + Unit, + Document, +} + @localized() @customElement('holochain-app') export class HolochainApp extends ScopedElementsMixin(LitElement) { @@ -50,6 +60,9 @@ export class HolochainApp extends ScopedElementsMixin(LitElement) { @property() _profilesStore!: ProfilesStore; + renderType = RenderType.App + hrl: Hrl| undefined + async firstUpdated() { const config:ProfilesConfig = { @@ -78,23 +91,81 @@ export class HolochainApp extends ScopedElementsMixin(LitElement) { } const appAgentClient = await AppAgentWebsocket.connect(new URL(url), appId) - this._howStore = new HowStore(appAgentClient, "how") + this._howStore = new HowStore(undefined, appAgentClient, "how") this._profilesStore = new ProfilesStore( new ProfilesClient(appAgentClient, 'how'), config ); } else { - const weClient = await WeClient.connect(); - - if ( - !(weClient.renderInfo.type === "applet-view") - && !(weClient.renderInfo.view.type === "main") - ) throw new Error("This Applet only implements the applet main view."); + const weClient = await WeClient.connect(appletServices); + + switch (weClient.renderInfo.type) { + case "applet-view": + switch (weClient.renderInfo.view.type) { + case "main": + // default is allready App + break; + case "block": + switch(weClient.renderInfo.view.block) { + default: + throw new Error("Unknown applet-view block type:"+weClient.renderInfo.view.block); + } + break; + case "entry": + switch (weClient.renderInfo.view.roleName) { + case "how": + switch (weClient.renderInfo.view.integrityZomeName) { + case "how_integrity": + switch (weClient.renderInfo.view.entryType) { + case "unitx": + this.renderType = RenderType.Unit + this.hrl = weClient.renderInfo.view.hrl + break; + case "document": + this.renderType = RenderType.Document + this.hrl = weClient.renderInfo.view.hrl + break; + default: + throw new Error("Unknown entry type:"+weClient.renderInfo.view.entryType); + } + break; + default: + throw new Error("Unknown integrity zome:"+weClient.renderInfo.view.integrityZomeName); + } + break; + default: + throw new Error("Unknown role name:"+weClient.renderInfo.view.roleName); + } + break; + default: + throw new Error("Unsupported applet-view type"); + } + break; + case "cross-applet-view": + switch (weClient.renderInfo.view.type) { + case "main": + // here comes your rendering logic for the cross-applet main view + //break; + case "block": + // + //break; + default: + throw new Error("Unknown cross-applet-view render type.") + } + break; + default: + throw new Error("Unknown render view type"); + } //@ts-ignore const client = weClient.renderInfo.appletClient; - this._howStore = new HowStore(client, "how") - + this._howStore = new HowStore(weClient, client, "how") + if (this.renderType == RenderType.Unit) this._howStore.pullUnits() + else if (this.renderType == RenderType.Document) { + await this._howStore.pullUnits() + // @ts-ignore + await this._howStore.pullDocument(this.hrl[1]) + } //@ts-ignore const profilesClient = weClient.renderInfo.profilesClient; @@ -109,7 +180,12 @@ export class HolochainApp extends ScopedElementsMixin(LitElement) { if (!this.loaded) return html`Loading...`; return html` - + ${this.renderType == RenderType.App ? html` + `:""} + ${this.renderType == RenderType.Unit && this.hrl ? html` + `:""} + ${this.renderType == RenderType.Document && this.hrl ? html` + `:""} @@ -119,6 +195,8 @@ export class HolochainApp extends ScopedElementsMixin(LitElement) { static get scopedElements() { return { "how-controller": HowController, + "how-unit": HowUnit, + "how-document": HowDocument, }; } } diff --git a/ui/src/how.service.ts b/ui/src/how.service.ts index f3083ed..767cb9d 100644 --- a/ui/src/how.service.ts +++ b/ui/src/how.service.ts @@ -1,4 +1,4 @@ -import { AppAgentClient, EntryHashB64, AgentPubKeyB64, AppAgentCallZomeRequest, RoleName, encodeHashToBase64, decodeHashFromBase64 } from '@holochain/client'; +import { AppAgentClient, EntryHashB64, AgentPubKeyB64, AppAgentCallZomeRequest, RoleName, encodeHashToBase64, decodeHashFromBase64, EntryHash } from '@holochain/client'; import { UnitInput, RustNode, RustTree, Initialization, DocumentOutput, DocumentInput, UpdateDocumentInput, AdvanceStateInput, UnitOutput, MarkDocumentInput, HowSignal, Unit, UpdateUnitInput} from './types'; import { ActionHash } from '@holochain/client'; @@ -35,6 +35,10 @@ export class HowService { return await this.callZome('get_units', null) } + async getDocument(input: EntryHash): Promise { + return this.callZome('get_document', input); + } + async createDocument(input: DocumentInput): Promise { return this.callZome('create_document', input); } @@ -63,7 +67,7 @@ export class HowService { let tree:RustTree = await this.callZome('get_tree', null); return tree.tree } - + async notify(signal: HowSignal, folks: Array): Promise { return this.callZome('notify', {signal, folks}); } diff --git a/ui/src/how.store.ts b/ui/src/how.store.ts index 71f4572..cfc0fb5 100644 --- a/ui/src/how.store.ts +++ b/ui/src/how.store.ts @@ -1,4 +1,4 @@ -import { EntryHashB64, AgentPubKeyB64, AppAgentClient, RoleName, encodeHashToBase64, decodeHashFromBase64, AgentPubKey } from '@holochain/client'; +import { EntryHashB64, AgentPubKeyB64, AppAgentClient, RoleName, encodeHashToBase64, decodeHashFromBase64, AgentPubKey, DnaHash, EntryHash } from '@holochain/client'; import { AgentPubKeyMap, EntryRecord } from '@holochain-open-dev/utils'; import { writable, Writable, derived, Readable, get } from 'svelte/store'; import cloneDeep from 'lodash/cloneDeep'; @@ -24,6 +24,8 @@ import { Progress, } from './types'; import { Action, ActionHash } from '@holochain/client'; +import { WeClient } from '@lightningrodlabs/we-applet'; +import { getMyDna } from './util'; export type HowConfig = { processRoot: string @@ -60,6 +62,7 @@ export class HowStore { public tree: Readable = derived(this.treeStore, i => i) public documentPaths: Readable>> = derived(this.documentPathStore, i => i) public processes: Readable> = derived(this.documents, d => this.getProcesses(get(this.treeStore))) + public dnaHash: DnaHash|undefined private processTypes: Readable> = derived( this.tree, @@ -70,13 +73,18 @@ export class HowStore { ) constructor( + public weClient: WeClient|undefined, protected client: AppAgentClient, roleName: RoleName, - zomeName = 'how' + zomeName = 'how', + ) { this.myAgentPubKey = encodeHashToBase64(client.myPubKey); this.service = new HowService(client, roleName, zomeName); - + + getMyDna(roleName, client).then(res=>{ + this.dnaHash = res + }) client.on( 'signal', signal => { console.log("SIGNAL",signal.payload) const payload = signal.payload as HowSignal @@ -241,12 +249,14 @@ export class HowStore { async pullMeta() { const docs = await this.pullDocuments("") - const meta = docs.find(d=>d.content.documentType == DocType.TreeMeta) - if (meta) { - this.treeName = meta.content.content[0].name - this.config = JSON.parse(meta.content.content[0].content) as HowConfig - this.processRootPath = this.config.processRoot.split(".") - Document.processRoot = this.config.processRoot + if (docs) { + const meta = docs.find(d=>d.content.documentType == DocType.TreeMeta) + if (meta) { + this.treeName = meta.content.content[0].name + this.config = JSON.parse(meta.content.content[0].content) as HowConfig + this.processRootPath = this.config.processRoot.split(".") + Document.processRoot = this.config.processRoot + } } // await this.pullDocuments("soc_proto.process.define.declaration") @@ -265,6 +275,19 @@ export class HowStore { return get(this.documentPathStore)[path] } + async pullDocument(hash: EntryHash) : Promise { + const doc = await this.service.getDocument(hash) + doc.content = new Document(doc.content) + doc.content.documentHash = doc.hash + doc.content.marks = doc.marks + const unit = get(this.units)[encodeHashToBase64(doc.content.unitHash)] + if (unit) { + this.updateDocumentStores(unit.path(), doc) + } + return doc + } + + async updateDocument(hash: EntryHashB64, document: Document) : Promise { const path = this.getDocumentPath(hash) let newHash: EntryHashB64 = "" diff --git a/ui/src/types.ts b/ui/src/types.ts index 3430a13..9a4dbf0 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -386,6 +386,7 @@ export enum MarkTypes { CommentStatus = 1, Vote = 2, Approval = 3, + Attachment = 4, } export type Offsets = { diff --git a/ui/src/util.ts b/ui/src/util.ts new file mode 100644 index 0000000..f1bce11 --- /dev/null +++ b/ui/src/util.ts @@ -0,0 +1,34 @@ +import { AppAgentClient, CellType, decodeHashFromBase64, DnaHash, encodeHashToBase64, type EntryHash } from "@holochain/client"; +import type { HrlB64WithContext, HrlWithContext } from "@lightningrodlabs/we-applet"; + +export function hrlWithContextToB64(hrl: HrlWithContext): HrlB64WithContext { + return { + hrl: [encodeHashToBase64(hrl.hrl[0]), encodeHashToBase64(hrl.hrl[1])], + context: hrl.context, + }; +} + +export function hrlB64WithContextToRaw(hrlB64: HrlB64WithContext): HrlWithContext { + return { + hrl: [decodeHashFromBase64(hrlB64.hrl[0]), decodeHashFromBase64(hrlB64.hrl[1])], + context: hrlB64.context, + }; +} + +export const hashEqual = (a:EntryHash, b:EntryHash) : boolean => { + if (!a || !b) { + return !a && !b + } + for (let i = a.length; -1 < i; i -= 1) { + if ((a[i] !== b[i])) return false; + } + return true; +} + +export const getMyDna = async (role:string, client: AppAgentClient) : Promise => { + const appInfo = await client.appInfo(); + const dnaHash = (appInfo.cell_info[role][0] as any)[ + CellType.Provisioned + ].cell_id[0]; + return dnaHash +} diff --git a/ui/src/we.ts b/ui/src/we.ts new file mode 100644 index 0000000..955202f --- /dev/null +++ b/ui/src/we.ts @@ -0,0 +1,67 @@ +import { CellType, type AppAgentClient, type RoleName, type ZomeName, type DnaHash, decodeHashFromBase64, encodeHashToBase64 } from '@holochain/client'; +import type { AppletHash, AppletServices, EntryInfo, Hrl, HrlWithContext, WeServices } from '@lightningrodlabs/we-applet'; +import { HowStore } from './how.store'; +import { getMyDna } from './util'; + +const ROLE_NAME = "how" +const ZOME_NAME = "how" + +export const appletServices: AppletServices = { + // Types of attachment that this Applet offers for other Applets to attach + attachmentTypes: async ( + appletClient: AppAgentClient, + appletHash: AppletHash, + weServices: WeServices + ) => ({ + }), + // Types of UI widgets/blocks that this Applet supports + blockTypes: { + }, + getEntryInfo: async ( + appletClient: AppAgentClient, + roleName: RoleName, + integrityZomeName: ZomeName, + entryType: string, + hrl: Hrl + ): Promise => { + + const store = new HowStore(undefined, appletClient, "how") + switch (entryType) { + case "document": { + const docHash = hrl[1] + const doc = await store.pullDocument(docHash) + const title = doc.content.content[0].content + return { + icon_src: `data:image/svg+xml;utf8,`, + name: title + }; + } + case "unitx": + const units = await store.pullUnits() + const unitHash = encodeHashToBase64(hrl[1]) + + return { + icon_src: `data:image/svg+xml;utf8,`, + name: units[unitHash].shortName, + }; + } + }, + search: async ( + appletClient: AppAgentClient, + appletHash: AppletHash, + weServices: WeServices, + searchFilter: string + ): Promise> => { + const store = new HowStore(undefined, appletClient, "how") + const dnaHash = await getMyDna(ROLE_NAME, appletClient) + const units = await store.pullUnits() + const lower = searchFilter.toLowerCase() + return Object.entries(units) + .filter(([_h,unit]) => unit.shortName.toLowerCase().includes(lower)) + .map(([entryHashB4,unit]) =>{ + return { hrl: [dnaHash, decodeHashFromBase64(entryHashB4)], context: {} } + }) + + }, +}; + \ No newline at end of file