├── static ├── images │ └── holochain-icon.png ├── css │ ├── custom.css │ └── devhub.css ├── googlefonts │ ├── mulish-italic.ttf │ ├── mulish-normal.ttf │ └── mulish.css ├── bootstrap-icons-v1 │ └── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 ├── web-components │ ├── img-src.js │ ├── holochain-img.js │ ├── input-file.js │ ├── notification-toast.js │ ├── layout-banner.js │ └── notification-bar.js ├── anchor-icon.js ├── templates │ ├── main.html │ ├── profiles │ │ └── single.html │ ├── admin.html │ ├── publishers │ │ ├── single.html │ │ ├── update.html │ │ └── create.html │ └── apps │ │ ├── update.html │ │ ├── create.html │ │ └── single.html ├── custom-elements.js ├── index.html └── popper-v2 │ └── popper-v2.9.2.min.js ├── bundled └── web-happ.yaml ├── .gitignore ├── tests ├── set_global_context.js ├── templates │ ├── publishers.html │ ├── single.html │ ├── update.html │ ├── create.html │ └── errors.html ├── setup.js └── e2e │ ├── test_publisher_crud.js │ └── test_app_crud.js ├── src ├── admin_controllers.js ├── openstate.js ├── client.js ├── filters.js ├── generic_controllers.js ├── publisher_controllers.js ├── countries.js ├── index.js └── app_controllers.js ├── flake.nix ├── create_viewpoint.js ├── package.json ├── webpack.config.js ├── Makefile └── flake.lock /static/images/holochain-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/app-store-gui/HEAD/static/images/holochain-icon.png -------------------------------------------------------------------------------- /static/css/custom.css: -------------------------------------------------------------------------------- 1 | 2 | .text-pre { 3 | white-space: pre; 4 | } 5 | .text-pre-wrap { 6 | white-space: pre-wrap; 7 | } 8 | -------------------------------------------------------------------------------- /static/googlefonts/mulish-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/app-store-gui/HEAD/static/googlefonts/mulish-italic.ttf -------------------------------------------------------------------------------- /static/googlefonts/mulish-normal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/app-store-gui/HEAD/static/googlefonts/mulish-normal.ttf -------------------------------------------------------------------------------- /static/bootstrap-icons-v1/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/app-store-gui/HEAD/static/bootstrap-icons-v1/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /static/bootstrap-icons-v1/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/app-store-gui/HEAD/static/bootstrap-icons-v1/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /bundled/web-happ.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | manifest_version: "1" 3 | name: App Store 4 | ui: 5 | bundled: "../web_assets.zip" 6 | happ_manifest: 7 | bundled: "../tests/appstore.happ" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | holochain 3 | tests/DNA_* 4 | tests/AGENT* 5 | tests/*.happ 6 | static/dist 7 | static/dependencies 8 | static/web-components/purewc-* 9 | appstore.webhapp 10 | *.zip 11 | -------------------------------------------------------------------------------- /tests/set_global_context.js: -------------------------------------------------------------------------------- 1 | 2 | global.Vue = require('vue'); 3 | global.showdown = require('showdown'); 4 | global.holohash = require('@whi/holo-hash'); 5 | global.HolochainClient = require('@whi/holochain-client'); 6 | global.CruxPayloadParser = require('@whi/crux-payload-parser'); 7 | -------------------------------------------------------------------------------- /src/admin_controllers.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("admin"); 3 | 4 | const common = require('./common.js'); 5 | 6 | 7 | module.exports = async function () { 8 | 9 | async function dashboard () { 10 | return { 11 | "template": await common.load_html("/templates/admin.html"), 12 | "data": function() { 13 | return { 14 | "new_admin": null, 15 | "new_member": null, 16 | }; 17 | }, 18 | async created () { 19 | this.mustGet(async () => { 20 | await this.refresh(); 21 | }); 22 | }, 23 | "computed": { 24 | ...common.scopedPathComputed( `viewpoint/group`, "group" ), 25 | }, 26 | "methods": { 27 | async refresh () { 28 | await this.$openstate.read("viewpoint/group"); 29 | }, 30 | async reset () { 31 | await this.$openstate.resetMutable("viewpoint/group"); 32 | }, 33 | async update () { 34 | await this.$openstate.write("viewpoint/group"); 35 | }, 36 | }, 37 | }; 38 | }; 39 | 40 | return { 41 | dashboard 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Holochain Development Env"; 3 | 4 | inputs = { 5 | nixpkgs.follows = "holochain-flake/nixpkgs"; 6 | flake-parts.follows = "holochain-flake/flake-parts"; 7 | holochain-nix-versions.url = "github:holochain/holochain/?dir=versions/0_2"; 8 | 9 | holochain-flake = { 10 | url = "github:holochain/holochain"; 11 | inputs.holochain.url = "github:holochain/holochain/holochain-0.2.2"; 12 | inputs.lair.url = "github:holochain/lair/lair_keystore-v0.3.0"; 13 | }; 14 | }; 15 | 16 | outputs = inputs @ { ... }: 17 | inputs.holochain-flake.inputs.flake-parts.lib.mkFlake 18 | { 19 | inherit inputs; 20 | } 21 | { 22 | systems = builtins.attrNames inputs.holochain-flake.devShells; 23 | perSystem = 24 | { config 25 | , pkgs 26 | , system 27 | , ... 28 | }: { 29 | devShells.default = pkgs.mkShell { 30 | inputsFrom = [ inputs.holochain-flake.devShells.${system}.holonix ]; 31 | packages = with pkgs; [ 32 | ]; 33 | }; 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /static/web-components/img-src.js: -------------------------------------------------------------------------------- 1 | class HTMLImgSrcElement extends HTMLElementTemplate { 2 | static CSS = ` 3 | :host { 4 | } 5 | 6 | img { 7 | object-fit: cover; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | `; 12 | static template = ` 13 | 14 | `; 15 | static refs = { 16 | "$img": `img`, 17 | }; 18 | 19 | 20 | // Element constants 21 | 22 | static properties = { 23 | "bytes":{ 24 | "reflect": false, 25 | updateDOM () { 26 | this.updateSource(); 27 | }, 28 | }, 29 | "mime-type": { 30 | updateDOM () { 31 | this.updateSource(); 32 | }, 33 | }, 34 | }; 35 | 36 | updateSource () { 37 | this.$img.src = URL.createObjectURL( 38 | new Blob([this.bytes], { 39 | "type": this['mime-type'] || 'image/png', 40 | }) 41 | ); 42 | } 43 | 44 | attributeCallback ( name, _, value ) { 45 | if ( !["width", "height", "alt", "title"].includes( name ) ) 46 | return; 47 | 48 | if ( value === null ) 49 | this.$img.removeAttribute( name ); 50 | else 51 | this.$img.setAttribute( name, value ); 52 | } 53 | } 54 | 55 | customElements.define("img-src", HTMLImgSrcElement ); 56 | -------------------------------------------------------------------------------- /create_viewpoint.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("viewpoint"); 3 | 4 | global.WebSocket = require('ws'); 5 | 6 | const json = require('@whi/json'); 7 | const { AgentPubKey, 8 | ActionHash } = require('@whi/holo-hash'); 9 | const { AgentClient } = require('@whi/holochain-client'); 10 | 11 | const APP_ID = "app-store"; 12 | const APP_PORT = 44_001; 13 | 14 | if ( process.argv.length < 3 ) 15 | throw new Error(`Missing arguments for admin and member hashes`); 16 | 17 | const args = process.argv.slice(2); 18 | const admins = args.map( hash => new AgentPubKey(hash) ); 19 | 20 | (async function () { 21 | const appstore = await AgentClient.createFromAppInfo( APP_ID, APP_PORT ); 22 | 23 | console.log("Creating a group with admins: %s", json.debug(admins) ); 24 | const group_input = { 25 | "admins": admins, 26 | "members": [], 27 | 28 | "published_at": Date.now(), 29 | "last_updated": Date.now(), 30 | "metadata": {}, 31 | }; 32 | const response = await appstore.call("appstore", "appstore_api", "create_group", group_input ); 33 | const group = response.payload; 34 | 35 | console.log("Group ID: %s", new ActionHash(group.id) ); 36 | 37 | appstore.close(); 38 | })(); 39 | -------------------------------------------------------------------------------- /static/css/devhub.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Entity Reference Cards (New Entity Cards) 4 | */ 5 | .entity-ref { 6 | padding: 30px 20px 15px; 7 | box-shadow: #eee 0 0 3px; 8 | } 9 | .entity-ref > * { 10 | z-index: 1; 11 | } 12 | .entity-ref::before { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | z-index: 0; 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 3px; 20 | } 21 | .entity-ref::before { 22 | padding: 15px 20px; 23 | font-size: .7em; 24 | } 25 | 26 | .zome-ref::before, 27 | .zome-version-ref::before { 28 | color: #00a1df; 29 | border-bottom: 4px solid #00a1df; 30 | } 31 | .dna-ref::before, 32 | .dna-version-ref::before { 33 | color: #c000ff; 34 | border-bottom: 4px solid #c000ff; 35 | } 36 | .happ-ref::before, 37 | .happ-release-ref::before { 38 | color: #6e00e0; 39 | border-bottom: 4px solid #6e00e0; 40 | } 41 | .gui-ref::before, 42 | .gui-release-ref::before { 43 | color: #ebbe44; 44 | border-bottom: 4px solid #ebbe44; 45 | } 46 | .zome-ref::before { content: "Zome"; } 47 | .zome-version-ref::before { content: "Zome Version"; } 48 | .dna-ref::before { content: "DNA"; } 49 | .dna-version-ref::before { content: "DNA Version"; } 50 | .happ-ref::before { content: "hApp"; } 51 | .happ-release-ref::before { content: "hApp Release"; } 52 | .gui-ref::before { content: "GUI"; } 53 | .gui-release-ref::before { content: "GUI Release"; } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appstore-gui", 3 | "version": "0.1.0", 4 | "description": "A web-based UI for developers using Holochain's App Store", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/holochain/appstore.git" 10 | }, 11 | "author": "Matthew Brisebois", 12 | "license": "ISC", 13 | "bugs": { 14 | "url": "https://github.com/holochain/appstore/issues" 15 | }, 16 | "homepage": "https://github.com/holochain/appstore#readme", 17 | "dependencies": { 18 | "@babel/runtime": "^7.20.6", 19 | "@msgpack/msgpack": "^3.0.0-beta2", 20 | "@purewc/select-search": "^0.1.1", 21 | "@purewc/template": "^0.2.3", 22 | "@tauri-apps/api": "^1.2.0", 23 | "@whi/crux-payload-parser": "^0.3.2", 24 | "@whi/holo-hash": "^0.3.0", 25 | "@whi/holochain-backdrop": "^2.2.1", 26 | "@whi/holochain-client": "^0.81.0", 27 | "@whi/identicons": "^0.1.2", 28 | "@whi/weblogger": "^0.1.5", 29 | "compressorjs": "^1.2.1", 30 | "openstate": "^0.2.2", 31 | "pako": "^2.1.0", 32 | "showdown": "^2.1.0", 33 | "vue": "^3.2.45", 34 | "vue-router": "^4.1.6", 35 | "vuex": "^4.1.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.20.5", 39 | "@babel/plugin-transform-modules-commonjs": "^7.19.6", 40 | "@babel/plugin-transform-runtime": "^7.19.6", 41 | "@babel/preset-env": "^7.20.2", 42 | "@faker-js/faker": "^7.6.0", 43 | "@whi/json": "^0.1.6", 44 | "@whi/repr": "^0.1.0", 45 | "@whi/stdlog": "^0.3.4", 46 | "babel-loader": "^9.1.0", 47 | "chai": "^4.3.7", 48 | "copy-webpack-plugin": "^11.0.0", 49 | "mocha": "^10.2.0", 50 | "webpack": "^5.76.3", 51 | "webpack-cli": "^5.0.1", 52 | "ws": "^8.11.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | const WEBPACK_MODE = process.env.WEBPACK_MODE || "production"; 6 | 7 | const MAX_SIZE_KB = 250; 8 | const MAX_SIZE = MAX_SIZE_KB * 1_000; 9 | 10 | 11 | module.exports = { 12 | target: "web", 13 | mode: WEBPACK_MODE, 14 | entry: [ "./src/index.js" ], 15 | resolve: { 16 | mainFields: ["browser", "main"], 17 | }, 18 | output: { 19 | "publicPath": "/", 20 | "path": path.resolve("static", "dist"), 21 | "filename": "webpacked.app.js" 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.m?js$/, 27 | exclude: /(node_modules|bower_components)/, 28 | use: { 29 | loader: "babel-loader", 30 | options: { 31 | presets: ["@babel/preset-env"], 32 | plugins: [ 33 | ["@babel/plugin-transform-runtime", { 34 | "regenerator": true, 35 | }], 36 | ["@babel/plugin-transform-modules-commonjs", { 37 | "allowTopLevelThis": true, 38 | }], 39 | ], 40 | } 41 | } 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new webpack.DefinePlugin({ 47 | // Vue says to do this - https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags 48 | "WEBPACK_MODE": JSON.stringify( WEBPACK_MODE ), 49 | "__VUE_OPTIONS_API__": JSON.stringify( true ), 50 | "__VUE_PROD_DEVTOOLS__": JSON.stringify( false ), 51 | }), 52 | ], 53 | stats: { 54 | colors: true, 55 | errorDetails: true, 56 | }, 57 | devtool: "source-map", 58 | optimization: { 59 | minimizer: [ 60 | new TerserPlugin({ 61 | terserOptions: { 62 | keep_classnames: true, 63 | }, 64 | }), 65 | ], 66 | }, 67 | performance: { 68 | maxEntrypointSize: MAX_SIZE, 69 | maxAssetSize: MAX_SIZE, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /static/web-components/holochain-img.js: -------------------------------------------------------------------------------- 1 | 2 | if ( !customElements.get("img-src") ) 3 | throw new Error(` depends on Web Component `); 4 | 5 | 6 | class HTMLHolochainImgElement extends HTMLElementTemplate { 7 | static CSS = ` 8 | :host { 9 | display: inline-block; 10 | position: relative; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | .spinner-border { 16 | position: absolute; 17 | left: 50%; 18 | top: 50%; 19 | translate: -50% -50%; 20 | 21 | display: inline-block; 22 | width: 2em; 23 | height: 2em; 24 | 25 | border: 0.25em solid currentcolor; 26 | border-right-color: transparent; 27 | border-radius: 50%; 28 | vertical-align: -0.125em; 29 | animation: 0.75s linear infinite spinner-border; 30 | } 31 | 32 | @keyframes spinner-border { 33 | from { 34 | transform: rotate( 0deg ); 35 | } 36 | to { 37 | transform: rotate( 359deg ); 38 | } 39 | } 40 | `; 41 | 42 | static template = ` 43 |
44 | 45 | `; 46 | static refs = { 47 | "$img": `img-src`, 48 | "$loading": `.spinner-border`, 49 | }; 50 | 51 | 52 | // Element constants 53 | 54 | static properties = { 55 | "os-path": { 56 | async updateDOM () { 57 | const path = this['os-path']; 58 | const bytes = await openstate.read( path ); 59 | 60 | this.$img.bytes = bytes; 61 | 62 | this.$loading.style.display = "none"; 63 | this.$img.style.display = "initial"; 64 | }, 65 | }, 66 | "mime-type": { 67 | async updateDOM () { 68 | this.$img.setAttribute( "mime-type", this['mime-type'] ); 69 | }, 70 | }, 71 | }; 72 | 73 | constructor () { 74 | super(); 75 | } 76 | 77 | 78 | // Property/attribute controllers 79 | 80 | 81 | // Methods 82 | 83 | 84 | // Event handlers 85 | } 86 | 87 | customElements.define("holochain-img", HTMLHolochainImgElement ); 88 | -------------------------------------------------------------------------------- /src/openstate.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("openstate"); 3 | 4 | const OpenState = require('openstate'); 5 | const appstore_config = require('./openstate_configs/appstore.js'); 6 | 7 | const { reactive } = Vue; 8 | const { EntityArchitect } = CruxPayloadParser; 9 | const { Entity } = EntityArchitect; 10 | const { HoloHash, 11 | AgentPubKey } = holohash; 12 | 13 | module.exports = async function ([ appstore ]) { 14 | const openstate = new OpenState.create({ 15 | reactive, 16 | "globalDefaults": { 17 | adapter ( value ) { 18 | if ( value instanceof Entity ) { 19 | if ( value.author ) 20 | value.author = new AgentPubKey( value.author ); 21 | if ( value.published_at ) 22 | value.published_at = new Date( value.published_at ); 23 | if ( value.last_updated ) 24 | value.last_updated = new Date( value.last_updated ); 25 | } 26 | }, 27 | toMutable ( value ) { 28 | if ( value instanceof Entity ) { 29 | value = value.toJSON().content; 30 | 31 | if ( value.published_at instanceof Date ) 32 | value.published_at = (new Date( value.published_at )).toISOString(); 33 | if ( value.last_updated instanceof Date ) 34 | value.last_updated = (new Date( value.last_updated )).toISOString(); 35 | } 36 | return value; 37 | }, 38 | }, 39 | }); 40 | 41 | const devhub = { 42 | async call ( dna_hash, zome, func, payload, timeout ) { 43 | const available_host = await openstate.get(`devhub/hosts/${dna_hash}/${zome}/${func}/any`); 44 | const call_details = { 45 | "dna": dna_hash, 46 | "zome": zome, 47 | "function": func, 48 | "payload": payload, 49 | }; 50 | return await appstore.call("portal", "portal_api", "custom_remote_call", { 51 | "host": available_host.author, 52 | "call": call_details, 53 | }, timeout ); 54 | } 55 | }; 56 | 57 | openstate.addHandlers({ 58 | ...appstore_config( appstore, devhub ), 59 | }); 60 | 61 | return openstate; 62 | }; 63 | -------------------------------------------------------------------------------- /static/web-components/input-file.js: -------------------------------------------------------------------------------- 1 | class HTMLInputFileElement extends HTMLElementTemplate { 2 | static CSS = ` 3 | :host { 4 | } 5 | `; 6 | 7 | static template = ` 8 | 9 | 10 | `; 11 | static refs = { 12 | "$input": `input`, 13 | }; 14 | 15 | 16 | // Element constants 17 | 18 | static properties = { 19 | "accept": { 20 | async updateDOM () { 21 | this.$input.setAttribute("accept", this.accept ); 22 | }, 23 | }, 24 | }; 25 | 26 | constructor () { 27 | super(); 28 | 29 | this.addEventListener("click", event => { 30 | this.$input.click(); 31 | }); 32 | 33 | this.$input.addEventListener("input", this.loadFile.bind(this) ); 34 | } 35 | 36 | // connectedCallback() { 37 | // } 38 | 39 | 40 | // Property/attribute controllers 41 | 42 | 43 | // Methods 44 | updateValue ( value ) { 45 | this.value = value; 46 | const input = new InputEvent('input'); 47 | this.dispatchEvent( input ); 48 | 49 | const change = new InputEvent('change'); 50 | this.dispatchEvent( change ); 51 | } 52 | 53 | // Event handlers 54 | 55 | loadFile ( event ) { 56 | if ( event.target.files.length === 0 ) 57 | return this.updateValue( null ); 58 | 59 | const $this = this; 60 | const file = event.target.files[0]; 61 | const reader = new FileReader(); 62 | 63 | reader.readAsArrayBuffer( file ); 64 | reader.onerror = function (err) { 65 | console.error("FileReader error event:", err ); 66 | }; 67 | reader.onload = function (evt) { 68 | let result = new Uint8Array( evt.target.result ); 69 | result.file = file; 70 | $this.updateValue( result ); 71 | }; 72 | reader.onprogress = function (p) { 73 | // console.log("progress:", p ); 74 | }; 75 | } 76 | 77 | attributeCallback ( name, _, value ) { 78 | if ( !["accept"].includes( name ) ) 79 | return; 80 | 81 | if ( value === null ) 82 | this.$input.removeAttribute( name ); 83 | else 84 | this.$input.setAttribute( name, value ); 85 | } 86 | } 87 | 88 | customElements.define("input-file", HTMLInputFileElement ); 89 | -------------------------------------------------------------------------------- /static/anchor-icon.js: -------------------------------------------------------------------------------- 1 | 2 | class AnchorIcon extends LitElement { 3 | static get properties () { 4 | return { 5 | "href": { 6 | "type": String, 7 | "reflect": true, 8 | }, 9 | }; 10 | } 11 | 12 | static styles = [ 13 | css` 14 | :host { 15 | } 16 | 17 | a { 18 | 19 | position: relative !important; 20 | 21 | width: 2em !important; 22 | height: 2em !important; 23 | padding: 0; 24 | border-radius: 50%; 25 | border: 0; 26 | 27 | font-size: 1.5em !important; 28 | 29 | display: inline-block; 30 | color: var(--bs-btn-color); 31 | text-align: center; 32 | text-decoration: none; 33 | vertical-align: middle; 34 | cursor: pointer; 35 | user-select: none; 36 | background-color: var(--bs-btn-bg); 37 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 38 | } 39 | 40 | a > ::slotted(i) { 41 | color: white !important; 42 | 43 | position: absolute !important; 44 | top: 50%; 45 | left: 50%; 46 | transform: translate(-50%, -50%); 47 | } 48 | 49 | a:hover { 50 | color: var(--bs-btn-hover-color); 51 | background-color: var(--bs-btn-hover-bg); 52 | border-color: var(--bs-btn-hover-border-color); 53 | } 54 | 55 | a:active { 56 | color: var(--bs-btn-active-color); 57 | background-color: var(--bs-btn-active-bg); 58 | border-color: var(--bs-btn-active-border-color); 59 | } 60 | 61 | :host.disabled a { 62 | color: var(--bs-btn-disabled-color); 63 | pointer-events: none; 64 | background-color: var(--bs-btn-disabled-bg); 65 | border-color: var(--bs-btn-disabled-border-color); 66 | opacity: var(--bs-btn-disabled-opacity); 67 | } 68 | `, 69 | ]; 70 | 71 | constructor () { 72 | super(); 73 | } 74 | 75 | anchorWithRef () { 76 | return html` 77 | 78 | 79 | 80 | `; 81 | } 82 | 83 | anchorWithoutRef () { 84 | return html` 85 | 86 | 87 | 88 | `; 89 | } 90 | 91 | render () { 92 | return this.href 93 | ? this.anchorWithRef() 94 | : this.anchorWithoutRef(); 95 | } 96 | } 97 | 98 | customElements.define("a-icon", AnchorIcon ); 99 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("client"); 3 | 4 | const { DnaHash, 5 | AgentPubKey } = holohash; 6 | const { AgentClient } = HolochainClient; 7 | const { CruxConfig, 8 | Translator } = CruxPayloadParser; 9 | 10 | 11 | module.exports = async function ( CONDUCTOR_URI, APP_ID ) { 12 | const { AgentClient } = await HolochainClient; 13 | const crux_config = new CruxConfig(); 14 | const interpreter = new Translator([]); 15 | 16 | const appstore = await AgentClient.createFromAppInfo( APP_ID, CONDUCTOR_URI ); 17 | 18 | log.normal("App Store client (Cell %s): %s", appstore.cellAgent(), appstore.capabilityAgent() ); 19 | 20 | // console.log( appstore ); 21 | 22 | appstore.addProcessor("input", async function (input) { 23 | let keys = input ? `{ ${Object.keys( input ).join(", ")} }` : ""; 24 | log.trace("Calling %s::%s->%s(%s)", this.dna, this.zome, this.func, keys ); 25 | return input; 26 | }); 27 | appstore.addProcessor("output", async function (output) { 28 | log.trace("Response for %s::%s->%s(%s)", this.dna, this.zome, this.func, this.input ? " ... " : "", output ); 29 | return output; 30 | }); 31 | 32 | appstore.addProcessor("output", (essence, req) => { 33 | if ( !( req.dna === "portal" 34 | && req.zome === "portal_api" 35 | && req.func === "custom_remote_call" 36 | ) ) 37 | return essence; 38 | 39 | let pack; 40 | try { 41 | log.debug("Portal wrapper (%s)", essence.type, essence.payload ); 42 | pack = interpreter.parse( essence ); 43 | } catch ( err ) { 44 | log.error("Error unwrapping portal response response:", err ); 45 | return essence; 46 | } 47 | 48 | const payload = pack.value(); 49 | 50 | if ( payload instanceof Error ) 51 | throw payload; 52 | 53 | return payload; 54 | }); 55 | 56 | log.normal("App schema"); 57 | log.level.normal && Object.entries( appstore._app_schema._dnas ).forEach( ([nick, schema]) => { 58 | log.normal(" %s : %s", nick.padStart( 10 ), String( schema._hash ) ); 59 | 60 | log.level.info && Object.entries( schema._zomes ).forEach( ([name, zome_api]) => { 61 | log.info(" %s : %s", name.padStart( 10 ), zome_api._name ); 62 | }); 63 | }); 64 | 65 | crux_config.upgrade( appstore ); 66 | 67 | return [ appstore ]; 68 | } 69 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("filters"); 3 | 4 | const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 5 | const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 6 | 7 | function day ( d, length ) { 8 | return days[ d.getDay() ].slice(0, length); 9 | } 10 | function month ( d, length ) { 11 | return months[ d.getMonth() ].slice(0, length); 12 | } 13 | function date ( d ) { 14 | return `${d.getDate()} ${month(d, 3)} ${d.getFullYear()}`; 15 | } 16 | function time ( d ) { 17 | return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; 18 | } 19 | 20 | module.exports = { 21 | number ( value ) { 22 | return (new Number(value)).toLocaleString(); 23 | }, 24 | 25 | time ( value, format ) { 26 | const d = new Date(value); 27 | 28 | if ( isNaN(d) ) { 29 | log.warn("Invalid date from: %s", String(value) ); 30 | return "Invalid Date"; 31 | } 32 | 33 | switch (format) { 34 | case "weekday+date+time": 35 | format = `${day(d)}, ${date(d)} @ ${time(d)}`; // "dddd, MMMM D (YYYY) @ HH:mm:ss"; 36 | break; 37 | case "weekday+date": 38 | format = `${day(d)}, ${date(d)}`; 39 | break; 40 | case "date+time": 41 | format = `${date(d)} @ ${time(d)}`; 42 | break; 43 | case "date": 44 | format = `${date(d)}`; 45 | break; 46 | default: 47 | const delta_seconds = Math.floor( (new Date() - d) / 1000 ); 48 | 49 | let delta, term; 50 | 51 | // seconds since 52 | if ( delta_seconds < 60 ) { 53 | delta = delta_seconds; 54 | term = "second"; 55 | } 56 | // minutes since 57 | else if ( delta_seconds < 3600 ) { 58 | delta = Math.floor( delta_seconds / 60 ); 59 | term = "minute"; 60 | } 61 | // hours since 62 | else if ( delta_seconds < 86400 ) { 63 | delta = Math.floor( delta_seconds / 3600 ); 64 | term = "hour"; 65 | } 66 | // days since 67 | else if ( delta_seconds < 2592000 ) { 68 | delta = Math.floor( delta_seconds / 86400 ); 69 | term = "day"; 70 | } 71 | // months since 72 | else if ( delta_seconds < 31536000 ) { 73 | delta = Math.floor( delta_seconds / 2592000 ) 74 | term = "month"; 75 | } 76 | 77 | format = delta + " " + (delta > 1 ? `${term}s` : term) + " ago"; 78 | break; 79 | } 80 | 81 | return format; 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /static/templates/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Featured

4 |
5 | 6 | 10 | 11 |
12 |

Apps

13 | 14 |
15 |
17 | 18 |
19 |
20 |
21 | 24 |
25 |
26 |
27 |
28 | {{ app.title }} 29 |
30 |

31 | {{ app.subtitle }}  32 |

33 |

34 | Last updated {{ $filters.time( app.last_updated ) }} 35 |

36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |

Removed Apps

46 | 47 |
48 |
50 | 51 |
52 |
53 |
54 | 57 |
58 |
59 |
60 |
61 | {{ app.title }} 62 |
63 |

64 | {{ app.subtitle }}  65 |

66 |

67 | Last updated {{ $filters.time( app.last_updated ) }} 68 |

69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 | -------------------------------------------------------------------------------- /tests/templates/publishers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | App Store 38 |
39 |
40 |
41 |
42 | DevHub 43 | Add App 44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 56 |
57 |
59 | 60 |
61 | 62 |

Agent ID

63 |
64 |
65 | 66 |
67 |
68 |

Publisher(s)

69 |
70 | 71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /static/templates/profiles/single.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ agent }}

4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 |

Publisher(s)

13 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 24 |
25 |
27 | 29 |
30 | {{ publisher.name }} 31 |
32 |
33 |
34 |
35 |
36 | Loading... 37 |
38 |
39 |
40 | You do not belong to any publishers yet 41 |
42 |
43 | 44 |
45 |
46 |

App(s)

47 | 49 | 50 | 51 | 52 | 53 |
54 | 55 |
56 | 58 |
59 |
61 | 63 |
64 | {{ app.title }}: {{ app.subtitle }} 65 |
66 |
67 |
68 |
69 |
70 | Loading... 71 |
72 |
73 |
74 | You are not a contributor for any apps yet 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /src/generic_controllers.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("generic"); 3 | 4 | const common = require('./common.js'); 5 | 6 | 7 | module.exports = async function () { 8 | 9 | async function main () { 10 | return { 11 | "template": await common.load_html("/templates/main.html"), 12 | "data": function() { 13 | return { 14 | }; 15 | }, 16 | async created () { 17 | this.mustGet(async () => { 18 | await this.refresh(); 19 | }); 20 | }, 21 | "computed": { 22 | ...common.scopedPathComputed( `apps`, "apps" ), 23 | ...common.scopedPathComputed( `apps/removed`, "removed_apps" ), 24 | }, 25 | "methods": { 26 | async refresh () { 27 | this.$openstate.read( "apps/removed" ); 28 | await this.$openstate.read( "apps" ); 29 | }, 30 | }, 31 | }; 32 | }; 33 | 34 | async function create () { 35 | return { 36 | "template": await common.load_html("/templates/profiles/create.html"), 37 | "data": function() { 38 | return { 39 | }; 40 | }, 41 | "computed": { 42 | }, 43 | "methods": { 44 | }, 45 | }; 46 | }; 47 | 48 | async function single () { 49 | return { 50 | "template": await common.load_html("/templates/profiles/single.html"), 51 | "data": function() { 52 | return { 53 | "publishers_datapath": `agent/me/publishers`, 54 | "apps_datapath": `agent/me/apps`, 55 | }; 56 | }, 57 | async created () { 58 | this.mustGet( this.refresh ); 59 | }, 60 | "computed": { 61 | ...common.scopedPathComputed( c => c.publishers_datapath, "publishers" ), 62 | ...common.scopedPathComputed( c => c.apps_datapath, "apps" ), 63 | 64 | agent () { 65 | return this.$root.agent?.pubkey.initial; 66 | }, 67 | }, 68 | "methods": { 69 | async refresh () { 70 | await Promise.all([ 71 | this.$openstate.read( this.publishers_datapath ), 72 | this.$openstate.read( this.apps_datapath ), 73 | ]); 74 | }, 75 | }, 76 | }; 77 | }; 78 | 79 | async function update () { 80 | return { 81 | "template": await common.load_html("/templates/profiles/update.html"), 82 | "data": function() { 83 | const id = this.getPathId("id"); 84 | 85 | return { 86 | id, 87 | "datapath": `profile/${id}`, 88 | "new_icon": null, 89 | }; 90 | }, 91 | "computed": { 92 | ...common.scopedPathComputed( c => c.datapath, "profile" ), 93 | }, 94 | async created () { 95 | this.mustGet(async () => { 96 | await this.$openstate.get( this.datapath ); 97 | }); 98 | }, 99 | "methods": { 100 | }, 101 | }; 102 | }; 103 | 104 | return { 105 | main, 106 | create, 107 | update, 108 | single, 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /static/web-components/notification-toast.js: -------------------------------------------------------------------------------- 1 | class HTMLNotificationToastElement extends HTMLElementTemplate { 2 | static CSS = ` 3 | :host { 4 | box-sizing: border-box; 5 | display: flex; 6 | align-items: center; 7 | width: 100%; 8 | background-color: rgba(255, 255, 255, 0.9); 9 | background-clip: padding-box; 10 | box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 11 | } 12 | .toast { 13 | flex-grow: 1; 14 | } 15 | 16 | .toast > i { 17 | margin-right: 8px; 18 | } 19 | 20 | .dismiss { 21 | box-sizing: content-box; 22 | width: 1em; 23 | height: 1em; 24 | margin: auto; 25 | margin-right: 0; 26 | padding: 0.25em 0.25em; 27 | color: #000; 28 | background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; 29 | border: 0; 30 | border-radius: 0.375rem; 31 | opacity: 0.5; 32 | cursor: pointer; 33 | } 34 | `; 35 | 36 | static template = ` 37 |
38 | 39 |
40 | 41 | `; 42 | static refs = { 43 | "$toast": `.toast`, 44 | "$body": `.toast-body`, 45 | "$dismiss": `.dismiss`, 46 | }; 47 | 48 | 49 | // Element constants 50 | 51 | static properties = {}; 52 | 53 | 54 | constructor () { 55 | super(); 56 | 57 | this.$dismiss.addEventListener("click", event => { 58 | this.dispatchEvent( new Event('dismiss') ); 59 | }); 60 | } 61 | } 62 | 63 | customElements.define("notification-toast", HTMLNotificationToastElement ); 64 | 65 | 66 | class HTMLNotificationBoxElement extends HTMLElementTemplate { 67 | static CSS = ` 68 | :host { 69 | position: fixed; 70 | z-index: 1090; 71 | 72 | box-sizing: border-box; 73 | width: 50vw; 74 | max-width: 100%; 75 | } 76 | `; 77 | 78 | static template = ` 79 | 80 | `; 81 | static refs = {}; 82 | 83 | 84 | // Element constants 85 | 86 | static observedAttributes = [ 87 | "top", 88 | "bottom", 89 | "left", 90 | "right", 91 | ]; 92 | 93 | static properties = { 94 | "top": { 95 | updateDOM () { 96 | this.style.top = this.top || 0; 97 | }, 98 | }, 99 | "bottom": { 100 | updateDOM () { 101 | this.style.bottom = this.bottom || 0; 102 | }, 103 | }, 104 | "left": { 105 | updateDOM () { 106 | this.style.left = this.left || 0; 107 | }, 108 | }, 109 | "right": { 110 | updateDOM () { 111 | this.style.right = this.right || 0; 112 | }, 113 | }, 114 | }; 115 | } 116 | 117 | customElements.define("notification-box", HTMLNotificationBoxElement ); 118 | -------------------------------------------------------------------------------- /static/googlefonts/mulish.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Mulish'; 3 | font-style: italic; 4 | font-weight: 200; 5 | font-display: swap; 6 | src: url(mulish-italic.ttf) format('truetype'); 7 | } 8 | @font-face { 9 | font-family: 'Mulish'; 10 | font-style: italic; 11 | font-weight: 300; 12 | font-display: swap; 13 | src: url(mulish-italic.ttf) format('truetype'); 14 | } 15 | @font-face { 16 | font-family: 'Mulish'; 17 | font-style: italic; 18 | font-weight: 400; 19 | font-display: swap; 20 | src: url(mulish-italic.ttf) format('truetype'); 21 | } 22 | @font-face { 23 | font-family: 'Mulish'; 24 | font-style: italic; 25 | font-weight: 500; 26 | font-display: swap; 27 | src: url(mulish-italic.ttf) format('truetype'); 28 | } 29 | @font-face { 30 | font-family: 'Mulish'; 31 | font-style: italic; 32 | font-weight: 600; 33 | font-display: swap; 34 | src: url(mulish-italic.ttf) format('truetype'); 35 | } 36 | @font-face { 37 | font-family: 'Mulish'; 38 | font-style: italic; 39 | font-weight: 700; 40 | font-display: swap; 41 | src: url(mulish-italic.ttf) format('truetype'); 42 | } 43 | @font-face { 44 | font-family: 'Mulish'; 45 | font-style: italic; 46 | font-weight: 800; 47 | font-display: swap; 48 | src: url(mulish-italic.ttf) format('truetype'); 49 | } 50 | @font-face { 51 | font-family: 'Mulish'; 52 | font-style: italic; 53 | font-weight: 900; 54 | font-display: swap; 55 | src: url(mulish-italic.ttf) format('truetype'); 56 | } 57 | @font-face { 58 | font-family: 'Mulish'; 59 | font-style: normal; 60 | font-weight: 200; 61 | font-display: swap; 62 | src: url(mulish-normal.ttf) format('truetype'); 63 | } 64 | @font-face { 65 | font-family: 'Mulish'; 66 | font-style: normal; 67 | font-weight: 300; 68 | font-display: swap; 69 | src: url(mulish-normal.ttf) format('truetype'); 70 | } 71 | @font-face { 72 | font-family: 'Mulish'; 73 | font-style: normal; 74 | font-weight: 400; 75 | font-display: swap; 76 | src: url(mulish-normal.ttf) format('truetype'); 77 | } 78 | @font-face { 79 | font-family: 'Mulish'; 80 | font-style: normal; 81 | font-weight: 500; 82 | font-display: swap; 83 | src: url(mulish-normal.ttf) format('truetype'); 84 | } 85 | @font-face { 86 | font-family: 'Mulish'; 87 | font-style: normal; 88 | font-weight: 600; 89 | font-display: swap; 90 | src: url(mulish-normal.ttf) format('truetype'); 91 | } 92 | @font-face { 93 | font-family: 'Mulish'; 94 | font-style: normal; 95 | font-weight: 700; 96 | font-display: swap; 97 | src: url(mulish-normal.ttf) format('truetype'); 98 | } 99 | @font-face { 100 | font-family: 'Mulish'; 101 | font-style: normal; 102 | font-weight: 800; 103 | font-display: swap; 104 | src: url(mulish-normal.ttf) format('truetype'); 105 | } 106 | @font-face { 107 | font-family: 'Mulish'; 108 | font-style: normal; 109 | font-weight: 900; 110 | font-display: swap; 111 | src: url(mulish-normal.ttf) format('truetype'); 112 | } 113 | -------------------------------------------------------------------------------- /static/templates/admin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Admin Dashboard

4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |

Admin(s)

23 |
24 | 25 |
27 | 28 | 29 | 32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 | 43 | 44 | 45 |
46 | 47 | 48 |
49 |

Member(s)

50 |
51 | 52 |
54 | 55 | 56 | 59 | 60 | 61 |
62 | 63 |
64 | No members 65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | 74 | 75 | 76 |
77 |
78 | 79 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /static/templates/publishers/single.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ publisher.name }}

4 | 5 | {{ publisher.location.country }} 6 | , {{ publisher.location.region }}, {{ publisher.location.city }} 7 | 8 | 9 |
10 | 13 |
14 | 15 |
16 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 |

Details

27 | 28 | 36 | 37 |
38 | 39 |

{{ publisher.description }}

40 |
41 | 42 |
43 | 44 |

{{ publisher.email }}

45 |
46 | 47 |
48 | 49 |

{{ publisher.website.url }}

50 |
51 |
52 | 53 |
54 |
55 |

Editor(s)

56 |
57 | 58 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 | Deprecate 68 |
69 |
70 | 71 | 95 |
96 | -------------------------------------------------------------------------------- /static/web-components/layout-banner.js: -------------------------------------------------------------------------------- 1 | 2 | class HTMLLayoutBannerElement extends HTMLElementTemplate { 3 | static CSS = ` 4 | :host { 5 | max-width: 100vw; 6 | } 7 | 8 | .exhibit { 9 | width: 100%; 10 | height: 300px; 11 | } 12 | .exhibit::before { 13 | content: ""; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | background-image: linear-gradient(10deg, rgba(65,235,217,.7) 0%, rgba(142,59,249,0.7) 100%); 20 | } 21 | 22 | #banner { 23 | width: 100%; 24 | display: flex !important; 25 | align-items: center !important; 26 | flex-direction: column-reverse !important; 27 | } 28 | 29 | #banner-container { 30 | position: relative; 31 | padding-top: var(--bs-2-size); 32 | padding-bottom: var(--bs-2-size); 33 | } 34 | #banner-spotlight { 35 | position: absolute; 36 | width: 300px; 37 | height: 300px; 38 | background-color: #808080; 39 | 40 | border-radius: 50% !important; 41 | --bs-border-width: 2px; 42 | --bs-border-opacity: 1; 43 | border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; 44 | border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; 45 | overflow: hidden !important; 46 | } 47 | slot[name=spotlight]::slotted(*) { 48 | height: 300px; 49 | } 50 | #banner-actions { 51 | position: absolute; 52 | } 53 | 54 | .container { 55 | max-width: 1000px; 56 | box-sizing: border-box; 57 | 58 | --bs-gutter-x: 1.5rem; 59 | --bs-gutter-y: 0; 60 | width: 100%; 61 | padding-right: calc(var(--bs-gutter-x) * 0.5); 62 | padding-left: calc(var(--bs-gutter-x) * 0.5); 63 | margin-right: auto; 64 | margin-left: auto; 65 | } 66 | 67 | @media (max-width: 1000px) { 68 | #banner { 69 | height: 200px; 70 | } 71 | #banner-spotlight { 72 | bottom: -50px; 73 | right: var(--bs-2-size); 74 | } 75 | #banner-actions { 76 | top: 0; 77 | left: 0; 78 | padding-top: var(--bs-2-size); 79 | padding-left: var(--bs-2-size); 80 | } 81 | } 82 | 83 | @media (min-width: 1000px) and (max-width: 1760px) { 84 | #banner { 85 | height: 300px; 86 | } 87 | #banner-spotlight { 88 | bottom: -100px; 89 | right: var(--bs-2-size); 90 | } 91 | #banner-actions { 92 | top: 0; 93 | right: 0; 94 | padding-top: var(--bs-2-size); 95 | padding-right: var(--bs-2-size); 96 | } 97 | } 98 | 99 | @media (min-width: 1760px) { 100 | #banner { 101 | height: 400px; 102 | } 103 | #banner-spotlight { 104 | bottom: -100px; 105 | left: -330px; 106 | } 107 | #banner-actions { 108 | bottom: 0; 109 | right: 0; 110 | padding-bottom: var(--bs-3-size); 111 | padding-right: var(--bs-5-size); 112 | } 113 | } 114 | `; 115 | static template = ` 116 | 128 | `; 129 | static refs = { 130 | "$container": `identicon-container`, 131 | }; 132 | 133 | 134 | // Element constants 135 | 136 | static properties = { 137 | "seed":{ 138 | updateDOM () { 139 | this.$container.seed = this.seed; 140 | }, 141 | }, 142 | }; 143 | } 144 | 145 | customElements.define("layout-banner", HTMLLayoutBannerElement ); 146 | -------------------------------------------------------------------------------- /static/custom-elements.js: -------------------------------------------------------------------------------- 1 | function fallbackCopyTextToClipboard ( text ) { 2 | let textArea = document.createElement("textarea"); 3 | textArea.value = text; 4 | 5 | textArea.style.bottom = "0"; 6 | textArea.style.right = "0"; 7 | textArea.style.position = "fixed"; 8 | 9 | document.body.appendChild( textArea ); 10 | textArea.focus(); 11 | textArea.select(); 12 | 13 | try { 14 | if ( !document.execCommand('copy') ) 15 | throw new Error(`Unable to copy to clipboard`); 16 | } finally { 17 | document.body.removeChild( textArea ); 18 | } 19 | } 20 | 21 | class IdenticonImg extends LitElement { 22 | static DEFAULT_COLOR = false; 23 | static DEFAULT_SIZE = 25; 24 | 25 | static get properties () { 26 | return { 27 | "seed": { 28 | "type": String, 29 | "reflect": true, 30 | }, 31 | "size": { 32 | "reflect": true, 33 | }, 34 | "color": { 35 | "type": Boolean, 36 | "reflect": true, 37 | }, 38 | }; 39 | } 40 | 41 | static styles = [ 42 | css`.identicon { 43 | border-radius: 50%; 44 | }`, 45 | ]; 46 | 47 | constructor () { 48 | super(); 49 | 50 | this.color = this.constructor.DEFAULT_COLOR; 51 | this.size = this.constructor.DEFAULT_SIZE; 52 | } 53 | 54 | async copyToClipboard () { 55 | if ( !navigator.clipboard ) 56 | return fallbackCopyTextToClipboard( String(this.seed) ); 57 | 58 | await navigator.clipboard.writeText( String(this.seed) ); 59 | } 60 | 61 | render () { 62 | this.style.width = this.size + "px"; 63 | this.style.height = this.size + "px"; 64 | 65 | const size = parseInt( this.size ); 66 | const identicon = Identicons.renderDiscs({ 67 | "seed": String(this.seed), 68 | "width": size, 69 | "height": size, 70 | "colorRange": 15, 71 | "grayscale": this.color !== true, 72 | }); 73 | 74 | const dynamic_css = unsafeCSS(` 75 | :host { 76 | max-width: ${this.size}px; 77 | max-height: ${this.size}px; 78 | } 79 | `); 80 | 81 | return html` 82 | 83 | 84 | 85 | `; 86 | } 87 | } 88 | 89 | customElements.define("identicon-img", IdenticonImg ); 90 | 91 | 92 | 93 | class IdenticonContainer extends LitElement { 94 | static get properties () { 95 | return { 96 | "seed": { 97 | "type": String, 98 | "reflect": true, 99 | }, 100 | "color": { 101 | "type": Boolean, 102 | "reflect": true, 103 | }, 104 | "color-base": { 105 | "type": Number, 106 | "reflect": true, 107 | }, 108 | }; 109 | } 110 | 111 | static styles = [ 112 | css` 113 | :host { 114 | display: block; 115 | position: relative; 116 | } 117 | `, 118 | ]; 119 | 120 | constructor () { 121 | super(); 122 | 123 | this.color = false; 124 | 125 | window.addEventListener( "resize", event => { 126 | this.requestUpdate(); 127 | }); 128 | } 129 | 130 | render () { 131 | if ( !(this.offsetWidth && this.offsetHeight) ) 132 | return html`Invisible`; 133 | 134 | if ( !this.seed ) 135 | return html``; 136 | 137 | const identicon = Identicons.renderDiscs({ 138 | "seed": this.seed, 139 | "width": this.offsetWidth, 140 | "height": this.offsetHeight, 141 | "colorRange": 15, 142 | "base": this["color-base"], 143 | "grayscale": !this.color, 144 | }); 145 | 146 | // rgb(65,235,217) 147 | // rgb(142,59,249) 148 | const dynamic_css = css` 149 | :host { 150 | background-image: url(${unsafeCSS(identicon.dataURL)}); 151 | } 152 | `; 153 | // width: ${this.width}; 154 | // height: ${this.height}; 155 | 156 | return html` 157 | 158 | 159 | 160 | `; 161 | } 162 | } 163 | 164 | customElements.define("identicon-container", IdenticonContainer ); 165 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const log = require('@whi/stdlog')(path.basename( __filename ), { 3 | level: process.env.LOG_LEVEL || 'fatal', 4 | }); 5 | 6 | global.WebSocket = require('ws'); 7 | 8 | const fs = require('fs'); 9 | const { AdminClient, 10 | AgentClient, 11 | ConductorError, 12 | ...hc_client } = require('@whi/holochain-client'); 13 | 14 | if ( process.env.LOG_LEVEL ) 15 | hc_client.logging(); 16 | 17 | const APPSTORE_HAPP = path.resolve( __dirname, "appstore.happ" ); 18 | const DEVHUB_HAPP = path.resolve( __dirname, "devhub.happ" ); 19 | // const APPSTORE_DEVHUB_HAPP = path.resolve( __dirname, "appstore-devhub.happ" ); 20 | const HAPPS = { 21 | "app-store": APPSTORE_HAPP, 22 | "devhub": DEVHUB_HAPP, 23 | // "app-store": APPSTORE_DEVHUB_HAPP, 24 | }; 25 | 26 | const PORT = 35678; 27 | const APP_PORT = 44001 28 | const admin = new AdminClient( PORT ); 29 | 30 | function print ( msg, ...args ) { 31 | console.log(`\x1b[37m${msg}\x1b[0m`, ...args ); 32 | } 33 | 34 | (async function () { 35 | try { 36 | const APP_PREFIX = process.argv[2]; 37 | const AGENT_NICKNAME = process.argv[3] || null; 38 | const AGENT_FILENAME = AGENT_NICKNAME === null 39 | ? "AGENT" : `AGENT_${AGENT_NICKNAME}`; 40 | 41 | try { 42 | await admin.attachAppInterface( APP_PORT ); 43 | } catch (err) { 44 | if ( !( err instanceof ConductorError 45 | && err.message.includes("Address already in use") ) ) 46 | throw err; 47 | } 48 | 49 | const agent_file = path.resolve( __dirname, AGENT_FILENAME ); 50 | let agent_hash; 51 | 52 | if ( fs.existsSync( agent_file ) ) { 53 | print("Not creating Agent because `%s` already exists", agent_file ) 54 | agent_hash = new hc_client.HoloHash( fs.readFileSync( agent_file, "utf8" ) ); 55 | } else { 56 | agent_hash = await admin.generateAgent(); 57 | print("Agent hash: %s (%s)", agent_hash.toString(), AGENT_NICKNAME ); 58 | fs.writeFileSync( agent_file, agent_hash.toString() ); 59 | } 60 | console.log( agent_hash ); 61 | 62 | 63 | const INSTALLED_APPS = await admin.listApps(); 64 | console.log( JSON.stringify( INSTALLED_APPS.map(info => info.installed_app_id), null, 4 ) ); 65 | const HAPP_FILE = HAPPS[ APP_PREFIX ]; 66 | 67 | const APP_ID = AGENT_NICKNAME === null 68 | ? APP_PREFIX : `${APP_PREFIX}-${AGENT_NICKNAME}`; 69 | let installation = INSTALLED_APPS.find( app => app.installed_app_id === APP_ID ); 70 | 71 | try { 72 | if ( !installation ) { 73 | installation = await admin.installApp( 74 | APP_ID, agent_hash, HAPP_FILE, { 75 | "network_seed": "test-network", 76 | } 77 | ); 78 | } 79 | } catch (err) { 80 | if ( err instanceof ConductorError 81 | && err.message.includes("AppAlreadyInstalled") ) 82 | print("App '%s' is already installed", APP_ID ); 83 | else 84 | throw err; 85 | } 86 | 87 | try { 88 | await admin.enableApp( APP_ID ); 89 | } catch (err) { 90 | if ( err instanceof ConductorError 91 | && err.message.includes("AppNotInstalled") ) // already active 92 | print("App '%s' is already activated", APP_ID ); 93 | else 94 | throw err; 95 | } 96 | 97 | const apps = (await admin.listApps( "enabled" )) 98 | .reduce( (acc, info) => { 99 | acc[info.installed_app_id] = info; 100 | return acc; 101 | }, {}); 102 | const app_info = apps[ APP_ID ]; 103 | 104 | print("Creating unrestricted cap grant for testing"); 105 | for ( let role_name in app_info.roles ) { 106 | const dna_hash = app_info.roles[ role_name ].cell_id[0]; 107 | log.normal("DNA hash for '%s': %s", role_name, dna_hash ); 108 | await admin.grantUnrestrictedCapability( "testing", agent_hash, dna_hash, "*" ); 109 | 110 | const filename = path.resolve( __dirname, `DNA_${role_name.toUpperCase()}` ); 111 | fs.writeFileSync( filename, `${dna_hash}\n` ); 112 | } 113 | } catch (err) { 114 | console.log("Setup exiting in failure:"); 115 | console.error( err ); 116 | } finally { 117 | await admin.close(); 118 | } 119 | })(); 120 | -------------------------------------------------------------------------------- /static/templates/publishers/update.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |

{{ publisher.name }}

5 | 6 | {{ publisher.location.country }} 7 | , {{ publisher.location.region }}, {{ publisher.location.city }} 8 | 9 | 10 |
11 | 13 | 15 | 16 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |

Details

28 | 29 |
30 | 31 | 33 |
34 | 35 |
36 | 37 | 39 |
40 | 41 |
42 |
43 |
44 | 45 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 | 58 |
59 |
60 |
61 |
62 | 63 | 65 |
66 |
67 |
68 | 69 |
70 | 71 | 73 |
74 | 75 |
76 | 77 | 79 |
80 |
81 | 82 |
83 |
84 |

Editor(s)

85 |
86 | 87 |
89 | 90 | 91 |
92 |
93 | 94 |
95 | 111 |
112 | 113 |
114 | -------------------------------------------------------------------------------- /static/templates/publishers/create.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 6 | 7 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |

Details

20 | 21 |
22 | 23 | 25 |
26 | 27 |
28 | 29 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 40 | 41 | 42 | 43 |
44 |
45 |
46 |
47 | 48 | 50 |
51 |
52 |
53 |
54 | 55 | 57 |
58 |
59 |
60 | 61 |
62 | 63 | 65 |
66 | 67 |
68 | 69 | 71 |
72 |
73 | 74 |
75 |
76 |

Editor(s)

77 | 79 | 80 | 81 |
82 | 83 |
84 | 85 | 86 |
87 | 88 |
90 |
91 |
92 | 93 | 95 |
96 | 98 | 99 | 100 |
101 |
102 |
103 | 104 |
105 | 116 |
117 | 118 |
119 | -------------------------------------------------------------------------------- /tests/templates/single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | App Store 38 |
39 |
40 |
41 |
42 | DevHub 43 | Add App 44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 56 |
57 |
59 | 60 |
61 | 62 |

Publisher Name

63 | 64 | Canada, Alberta, Edmonton 65 | 66 |
67 |
68 | 69 | 70 | 71 |
72 |
73 | 74 |
75 |

Details

76 | 77 |
78 | 79 |

publisheremail@email.com

80 |
81 | 82 |
83 | 84 |

http://publisherwebsite.com

85 |
86 | 87 |
88 | 89 |

http://github.com/PublisherName

90 |
91 |
92 | 93 |
94 |
95 |

Editor(s)

96 |
97 | 98 |
99 | 100 | 101 |
102 |
103 | 104 | 112 | 113 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /tests/templates/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | App Store 38 |
39 |
40 |
41 |
42 | DevHub 43 | Add App 44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
64 | 65 | 66 | 67 | 68 | 69 |
70 | 71 |

Publisher Name

72 | 73 | Canada, Alberta, Edmonton 74 | 75 |
76 |
77 | 78 |
79 |

Details

80 | 81 |
82 | 83 | 84 |
85 | 86 |
87 |
88 |
89 | 90 | 93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 |
102 |
103 | 104 | 105 |
106 |
107 |
108 | 109 |
110 | 111 | 112 |
113 | 114 |
115 | 116 | 117 |
118 | 119 |
120 | 121 | 122 |
123 |
124 | 125 |
126 |
127 |

Editor(s)

128 |
129 | 130 |
131 | 132 | 133 |
134 |
135 | 136 |
137 | 145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /tests/templates/create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | App Store 38 |
39 |
40 |
41 |
42 | DevHub 43 | Add App 44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 69 | 70 |
71 |

Details

72 | 73 |
74 | 75 | 76 |
77 | 78 |
79 |
80 |
81 | 82 | 85 |
86 |
87 |
88 |
89 | 90 | 91 |
92 |
93 |
94 |
95 | 96 | 97 |
98 |
99 |
100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 | 108 | 109 |
110 | 111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |

Editor(s)

120 | 121 | 122 | 123 |
124 | 125 |
126 | 127 | 128 |
129 | 130 |
131 |
132 |
133 | 134 | 135 |
136 | 137 | 138 | 139 |
140 |
141 |
142 | 143 |
144 | 152 |
153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /tests/e2e/test_publisher_crud.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const log = require('@whi/stdlog')(path.basename( __filename ), { 3 | level: process.env.LOG_LEVEL || 'fatal', 4 | }); 5 | 6 | const { Holochain } = require('@whi/holochain-backdrop'); 7 | 8 | require('../set_global_context.js'); 9 | 10 | const crypto = require('crypto'); 11 | const expect = require('chai').expect; 12 | 13 | const common = require('../../src/common.js'); 14 | const openstate_init = require('../../src/openstate.js'); 15 | 16 | const APPSTORE_PATH = path.join( __dirname, "../appstore.happ" ); 17 | const DEVHUB_PATH = path.join( __dirname, "../devhub.happ" ); 18 | 19 | let openstate; 20 | 21 | async function publisher_state ( datapath, read, writable ) { 22 | return common.scopedState( openstate, datapath, read, writable, mutable => { 23 | mutable.name = "Testing"; 24 | mutable.location.country = "Gibraltar"; 25 | mutable.location.region = "Gibraltar"; 26 | mutable.location.city = "Gibraltar"; 27 | mutable.website.url = "https://github.com/holo-host"; 28 | mutable.website.context = "github"; 29 | mutable.icon = new Uint8Array([1,2,3]); 30 | }); 31 | } 32 | 33 | function mvp_tests () { 34 | 35 | it("should get agent", async function () { 36 | this.timeout( 30_000 ); 37 | 38 | const info = await openstate.read(`agent/me`); 39 | 40 | expect( info.pubkey.initial ).to.be.a("AgentPubKey"); 41 | }); 42 | 43 | it("should create publisher", async function () { 44 | const datapath = `publisher/${common.randomHex()}`; 45 | const [$data, data$] = await publisher_state( datapath ); 46 | 47 | expect( $data.writable ).to.be.true; 48 | 49 | const data = await openstate.write( datapath ); 50 | 51 | expect( data.$id ).to.be.an("ActionHash"); 52 | expect( data.name ).to.equal("Testing"); 53 | expect( data.editors ).to.have.length( 1 ); 54 | 55 | expect( $data.writable ).to.be.true; 56 | }); 57 | 58 | it("should get all publishers", async function () { 59 | const datapath = `publishers`; 60 | const $data = openstate.metastate[ datapath ]; 61 | const data = await openstate.read( datapath ); 62 | 63 | expect( $data.writable ).to.be.false; 64 | 65 | expect( data ).to.have.length( 1 ); 66 | }); 67 | 68 | it("should update publisher", async function () { 69 | const publishers = await openstate.read(`publishers`); 70 | const datapath = `publisher/${publishers[0].$id}`; 71 | 72 | await openstate.read( datapath ); 73 | 74 | const $data = openstate.metastate[ datapath ]; 75 | const data$ = openstate.mutable[ datapath ]; 76 | 77 | expect( $data.writable ).to.be.true; 78 | 79 | data$.website = { 80 | "url": "https://github.com/holochain", 81 | "context": "github", 82 | }; 83 | 84 | expect( $data.changed ).to.be.true; 85 | 86 | const data = await openstate.write( datapath ); 87 | }); 88 | 89 | } 90 | 91 | function optional_input_tests () { 92 | 93 | it("should create publisher with optional input", async function () { 94 | const datapath = `publisher/${common.randomHex()}`; 95 | const [$data, data$] = await publisher_state( datapath ); 96 | 97 | expect( $data.writable ).to.be.true; 98 | 99 | data$.editors = [ 100 | new holohash.AgentPubKey( crypto.randomBytes(32) ), 101 | ]; 102 | 103 | const data = await openstate.write( datapath ); 104 | 105 | expect( data.$id ).to.be.an("ActionHash"); 106 | expect( data.name ).to.equal("Testing"); 107 | expect( data.editors ).to.have.length( 2 ); 108 | 109 | expect( $data.writable ).to.be.true; 110 | }); 111 | 112 | } 113 | 114 | function invalid_tests () { 115 | 116 | it("should have rejections", async function () { 117 | const datapath = `publisher/${common.randomHex()}`; 118 | const [ $data, data$, _, 119 | rejections ] = await publisher_state( datapath ); 120 | 121 | data$.name = null; 122 | 123 | expect( $data.valid ).to.be.false; 124 | expect( rejections ).to.have.length( 1 ); 125 | expect( rejections[0] ).to.have.string("required"); 126 | }); 127 | 128 | } 129 | 130 | 131 | describe("Openstate: Publisher", () => { 132 | const crux = new CruxPayloadParser.CruxConfig(); 133 | const holochain = new Holochain({ 134 | "default_stdout_loggers": process.env.LOG_LEVEL === "silly", 135 | "timeout": 30_000, 136 | }); 137 | 138 | let actors; 139 | 140 | before(async function () { 141 | this.timeout( 30_000 ); 142 | 143 | log.debug("Waiting for holochain to start..."); 144 | await holochain.start( 5_000 ); 145 | 146 | actors = await holochain.backdrop({ 147 | "appstore": APPSTORE_PATH, 148 | }); 149 | 150 | for ( let name in actors ) { 151 | for ( let app_prefix in actors[ name ] ) { 152 | log.info("Upgrade client for %s => %s", name, app_prefix ); 153 | crux.upgrade( actors[ name ][ app_prefix ].client ); 154 | } 155 | } 156 | 157 | openstate = await openstate_init([ actors.alice.appstore.client ]); 158 | }); 159 | 160 | after(async () => { 161 | await holochain.destroy(); 162 | }); 163 | 164 | describe("MVP", mvp_tests.bind( this ) ); 165 | describe("Optional Input", optional_input_tests.bind( this ) ); 166 | describe("Invalid", invalid_tests.bind( this ) ); 167 | 168 | }); 169 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | 113 | 114 | 115 |
116 | 118 |
119 | 120 | 121 | 122 | 125 | 128 | 129 | {{ message }} 130 | 131 | 132 |
133 | 134 | 135 | 138 | 139 | 140 | 141 | 142 | 144 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /tests/e2e/test_app_crud.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const log = require('@whi/stdlog')(path.basename( __filename ), { 3 | level: process.env.LOG_LEVEL || 'fatal', 4 | }); 5 | 6 | const { Holochain } = require('@whi/holochain-backdrop'); 7 | 8 | require('../set_global_context.js'); 9 | 10 | const crypto = require('crypto'); 11 | const expect = require('chai').expect; 12 | 13 | const common = require('../../src/common.js'); 14 | const openstate_init = require('../../src/openstate.js'); 15 | 16 | const APPSTORE_PATH = path.join( __dirname, "../appstore.happ" ); 17 | const DEVHUB_PATH = path.join( __dirname, "../devhub.happ" ); 18 | 19 | let openstate; 20 | 21 | async function app_state ( datapath, read, writable ) { 22 | return common.scopedState( openstate, datapath, read, writable, mutable => { 23 | mutable.title = "Testing"; 24 | mutable.subtitle = "For tests"; 25 | mutable.description = ""; 26 | mutable.icon = new Uint8Array([1,2,3]); 27 | mutable.publisher = new holohash.ActionHash( crypto.randomBytes(32) ); 28 | mutable.devhub_address.dna = new holohash.DnaHash( crypto.randomBytes(32) ); 29 | mutable.devhub_address.happ = new holohash.EntryHash( crypto.randomBytes(32) ); 30 | mutable.devhub_address.gui = new holohash.EntryHash( crypto.randomBytes(32) ); 31 | }); 32 | } 33 | 34 | function mvp_tests () { 35 | 36 | it("should get agent", async function () { 37 | this.timeout( 30_000 ); 38 | 39 | const info = await openstate.read(`agent/me`); 40 | 41 | expect( info.pubkey.initial ).to.be.a("AgentPubKey"); 42 | }); 43 | 44 | it("should create app", async function () { 45 | const datapath = `app/${common.randomHex()}`; 46 | const [$data, data$] = await app_state( datapath ); 47 | 48 | expect( $data.writable ).to.be.true; 49 | 50 | const data = await openstate.write( datapath ); 51 | 52 | expect( data.$id ).to.be.an("ActionHash"); 53 | expect( data.title ).to.equal("Testing"); 54 | expect( data.editors ).to.have.length( 1 ); 55 | 56 | expect( $data.writable ).to.be.true; 57 | }); 58 | 59 | it("should get all apps", async function () { 60 | const datapath = `apps`; 61 | const [$data, data] = await app_state( datapath, true, false ); 62 | 63 | expect( $data.writable ).to.be.false; 64 | 65 | expect( data ).to.have.length( 1 ); 66 | }); 67 | 68 | it("should update app", async function () { 69 | const apps = await openstate.read(`apps`); 70 | const datapath = `app/${apps[0].$id}`; 71 | const [$data, data$] = await app_state( datapath, true ); 72 | 73 | expect( $data.writable ).to.be.true; 74 | 75 | data$.description = "Something"; 76 | 77 | expect( $data.changed ).to.be.true; 78 | 79 | const data = await openstate.write( datapath ); 80 | }); 81 | 82 | } 83 | 84 | function optional_input_tests () { 85 | 86 | it("should create app with optional input", async function () { 87 | const datapath = `app/${common.randomHex()}`; 88 | const [$data, data$] = await app_state( datapath ); 89 | 90 | expect( $data.writable ).to.be.true; 91 | 92 | data$.editors = [ 93 | new holohash.AgentPubKey( crypto.randomBytes(32) ), 94 | ]; 95 | 96 | const data = await openstate.write( datapath ); 97 | 98 | expect( data.$id ).to.be.an("ActionHash"); 99 | expect( data.title ).to.equal("Testing"); 100 | expect( data.editors ).to.have.length( 2 ); 101 | 102 | expect( $data.writable ).to.be.true; 103 | }); 104 | 105 | } 106 | 107 | function invalid_tests () { 108 | 109 | it("should have rejections", async function () { 110 | const datapath = `app/${common.randomHex()}`; 111 | const [ $data, data$, _, 112 | rejections ] = await app_state( datapath ); 113 | 114 | data$.title = null; 115 | data$.editors = [ 116 | "not an AgentPubKey", 117 | ]; 118 | 119 | expect( $data.valid ).to.be.false; 120 | expect( rejections ).to.have.length( 2 ); 121 | expect( rejections[0] ).to.have.string("required"); 122 | expect( rejections[1] ).to.have.string("must be an AgentPubKey"); 123 | }); 124 | 125 | } 126 | 127 | 128 | describe("Openstate: App", () => { 129 | const crux = new CruxPayloadParser.CruxConfig(); 130 | const holochain = new Holochain({ 131 | "default_stdout_loggers": process.env.LOG_LEVEL === "silly", 132 | "timeout": 30_000, 133 | clientConstructor ( agent, roles, app_port ) { 134 | return new HolochainClient.AgentClient( agent, roles, app_port, { 135 | "timeout": 30_000, 136 | }); 137 | }, 138 | }); 139 | 140 | let actors; 141 | 142 | before(async function () { 143 | this.timeout( 30_000 ); 144 | 145 | log.debug("Waiting for holochain to start..."); 146 | await holochain.start( 5_000 ); 147 | 148 | actors = await holochain.backdrop({ 149 | "appstore": APPSTORE_PATH, 150 | }); 151 | 152 | for ( let name in actors ) { 153 | for ( let app_prefix in actors[ name ] ) { 154 | log.info("Upgrade client for %s => %s", name, app_prefix ); 155 | crux.upgrade( actors[ name ][ app_prefix ].client ); 156 | } 157 | } 158 | 159 | openstate = await openstate_init([ actors.alice.appstore.client ]); 160 | }); 161 | 162 | after(async () => { 163 | await holochain.destroy(); 164 | }); 165 | 166 | describe("MVP", mvp_tests.bind( this ) ); 167 | describe("Optional Input", optional_input_tests.bind( this ) ); 168 | describe("Invalid", invalid_tests.bind( this ) ); 169 | 170 | }); 171 | -------------------------------------------------------------------------------- /src/publisher_controllers.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("publishers"); 3 | 4 | const common = require('./common.js'); 5 | const countries = require('./countries.js'); 6 | 7 | 8 | module.exports = async function () { 9 | 10 | async function create () { 11 | return { 12 | "template": await common.load_html("/templates/publishers/create.html"), 13 | "data": function() { 14 | return { 15 | "datapath": `publisher/${common.randomHex()}`, 16 | countries, 17 | }; 18 | }, 19 | "computed": { 20 | ...common.scopedPathComputed( c => c.datapath, "publisher" ), 21 | }, 22 | "methods": { 23 | async compressIcon () { 24 | if ( !this.publisher$.icon ) 25 | return; 26 | 27 | if ( this.publisher$.icon.file.name.endsWith(".svg") ) { 28 | this.publisher$.metadata.icon_mime_type = "image/svg+xml"; 29 | return; 30 | } 31 | 32 | delete this.publisher$.metadata.icon_mime_type; 33 | 34 | const compressed = await common.compressImage( this.publisher$.icon, { 35 | "mimeType": "image/jpeg", 36 | "maxWidth": 512, 37 | "maxHeight": 512, 38 | "convertSize": 50_000, 39 | }); 40 | this.publisher$.icon = compressed.result; 41 | }, 42 | async create () { 43 | console.log("Writing", this.publisher$ ); 44 | await this.$openstate.write( this.datapath ); 45 | 46 | const new_id = this.publisher.$id; 47 | this.$openstate.purge( this.datapath ); 48 | 49 | await this.$openstate.read("publishers"); 50 | await this.$openstate.read("agent/me/publishers"); 51 | 52 | this.$router.push( "/publishers/" + new_id ); 53 | }, 54 | 55 | actionErrors () { 56 | const errors = []; 57 | errors.push( ...this.publisher_rejections ); 58 | 59 | const error = this.publisher_errors.write; 60 | if ( error ) { 61 | const name = error.name; 62 | if ( name === "RibosomeDeserializeError" ) { 63 | const message = error.message.split('[')[0]; 64 | const json = this.$debug( error.data ); 65 | errors.push( `${name}: ${message}\n\n${json}` ); 66 | } 67 | else 68 | errors.push( this.publisher_errors.write ); 69 | } 70 | 71 | return errors; 72 | }, 73 | }, 74 | }; 75 | }; 76 | 77 | async function single () { 78 | return { 79 | "template": await common.load_html("/templates/publishers/single.html"), 80 | "data": function() { 81 | const id = this.getPathId("id"); 82 | 83 | return { 84 | id, 85 | "datapath": `publisher/${id}`, 86 | }; 87 | }, 88 | async created () { 89 | this.mustGet(async () => { 90 | await this.$openstate.read( this.datapath ); 91 | }); 92 | }, 93 | "computed": { 94 | ...common.scopedPathComputed( c => c.datapath, "publisher" ), 95 | 96 | deprecationModal () { 97 | return new bootstrap.Modal( this.$refs["deprecation-modal"], { 98 | "backdrop": "static", 99 | "keyboard": false, 100 | }); 101 | }, 102 | }, 103 | "methods": { 104 | refresh () { 105 | this.$openstate.read( this.datapath ); 106 | }, 107 | async confirmDeprecation () { 108 | log.normal("Deprecating Publisher %s", this.publisher.name ); 109 | await this.$openstate.write( this.datapath, "deprecation" ); 110 | 111 | this.deprecationModal.hide(); 112 | }, 113 | }, 114 | }; 115 | }; 116 | 117 | async function update () { 118 | return { 119 | "template": await common.load_html("/templates/publishers/update.html"), 120 | "data": function() { 121 | const id = this.getPathId("id"); 122 | 123 | return { 124 | id, 125 | countries, 126 | "datapath": `publisher/${id}`, 127 | "new_icon": null, 128 | }; 129 | }, 130 | "computed": { 131 | ...common.scopedPathComputed( c => c.datapath, "publisher" ), 132 | }, 133 | async created () { 134 | this.mustGet(async () => { 135 | await this.$openstate.get( this.datapath ); 136 | }); 137 | }, 138 | "methods": { 139 | async compressIcon () { 140 | if ( !this.new_icon ) 141 | return; 142 | 143 | if ( this.new_icon.file?.name.endsWith(".svg") ) 144 | return; 145 | 146 | const compressed = await common.compressImage( this.new_icon, { 147 | "mimeType": "image/jpeg", 148 | "maxWidth": 512, 149 | "maxHeight": 512, 150 | "convertSize": 50_000, 151 | }); 152 | this.new_icon = compressed.result; 153 | }, 154 | async update () { 155 | console.log("Writing", this.publisher$ ); 156 | 157 | let current_icon_bytes = this.$openstate.state[`appstore/memory/${this.publisher$.icon}`]; 158 | if ( this.new_icon && 159 | !common.equalUint8Arrays( this.new_icon, current_icon_bytes ) ) { 160 | this.publisher$.icon = this.new_icon; 161 | 162 | if ( this.new_icon.file?.name.endsWith(".svg") ) 163 | this.publisher$.metadata.icon_mime_type = "image/svg+xml"; 164 | else 165 | delete this.publisher$.metadata.icon_mime_type; 166 | } 167 | 168 | await this.$openstate.write( this.datapath ); 169 | 170 | this.new_icon = null; 171 | await this.$openstate.read("publishers"); 172 | 173 | this.$router.push( "/publishers/" + this.id ); 174 | }, 175 | 176 | actionErrors () { 177 | const errors = []; 178 | errors.push( ...this.publisher_rejections ); 179 | 180 | const error = this.publisher_errors.write; 181 | if ( error ) { 182 | const name = error.name; 183 | if ( name === "RibosomeDeserializeError" ) { 184 | const message = error.message.split('[')[0]; 185 | const json = this.$debug( error.data ); 186 | errors.push( `${name}: ${message}\n\n${json}` ); 187 | } 188 | else 189 | errors.push( this.publisher_errors.write ); 190 | } 191 | 192 | return errors; 193 | }, 194 | }, 195 | }; 196 | }; 197 | 198 | return { 199 | create, 200 | update, 201 | single, 202 | }; 203 | }; 204 | -------------------------------------------------------------------------------- /src/countries.js: -------------------------------------------------------------------------------- 1 | // Copied from https://gist.github.com/incredimike/1469814#file-variouscountrylistformats-js 2 | const countries = [ 3 | "Afghanistan", 4 | "Albania", 5 | "Algeria", 6 | "American Samoa", 7 | "Andorra", 8 | "Angola", 9 | "Anguilla", 10 | "Antarctica", 11 | "Antigua and Barbuda", 12 | "Argentina", 13 | "Armenia", 14 | "Aruba", 15 | "Australia", 16 | "Austria", 17 | "Azerbaijan", 18 | "The Bahamas", 19 | "Bahrain", 20 | "Bangladesh", 21 | "Barbados", 22 | "Belarus", 23 | "Belgium", 24 | "Belize", 25 | "Benin", 26 | "Bermuda", 27 | "Bhutan", 28 | "Bolivia", 29 | "Bonaire, Sint Eustatius and Saba", 30 | "Bosnia and Herzegovina", 31 | "Botswana", 32 | "Bouvet Island", 33 | "Brazil", 34 | "The British Indian Ocean Territory", 35 | "Brunei Darussalam", 36 | "Bulgaria", 37 | "Burkina Faso", 38 | "Burundi", 39 | "Cabo Verde", 40 | "Cambodia", 41 | "Cameroon", 42 | "Canada", 43 | "The Cayman Islands", 44 | "The Central African Republic", 45 | "Chad", 46 | "Chile", 47 | "China", 48 | "Christmas Island", 49 | "The Cocos (Keeling) Islands", 50 | "Colombia", 51 | "The Comoros", 52 | "The Democratic Republic of the Congo", 53 | "The Congo", 54 | "The Cook Islands", 55 | "Costa Rica", 56 | "Croatia", 57 | "Cuba", 58 | "Curaçao", 59 | "Cyprus", 60 | "Czechia", 61 | "Côte d'Ivoire", 62 | "Denmark", 63 | "Djibouti", 64 | "Dominica", 65 | "The Dominican Republic", 66 | "Ecuador", 67 | "Egypt", 68 | "El Salvador", 69 | "Equatorial Guinea", 70 | "Eritrea", 71 | "Estonia", 72 | "Eswatini", 73 | "Ethiopia", 74 | "The Falkland Islands [Malvinas]", 75 | "The Faroe Islands", 76 | "Fiji", 77 | "Finland", 78 | "France", 79 | "French Guiana", 80 | "French Polynesia", 81 | "The French Southern Territories", 82 | "Gabon", 83 | "The Gambia", 84 | "Georgia", 85 | "Germany", 86 | "Ghana", 87 | "Gibraltar", 88 | "Greece", 89 | "Greenland", 90 | "Grenada", 91 | "Guadeloupe", 92 | "Guam", 93 | "Guatemala", 94 | "Guernsey", 95 | "Guinea", 96 | "Guinea-Bissau", 97 | "Guyana", 98 | "Haiti", 99 | "Heard Island and McDonald Islands", 100 | "The Holy See", 101 | "Honduras", 102 | "Hong Kong", 103 | "Hungary", 104 | "Iceland", 105 | "India", 106 | "Indonesia", 107 | "Islamic Republic of Iran", 108 | "Iraq", 109 | "Ireland", 110 | "Isle of Man", 111 | "Israel", 112 | "Italy", 113 | "Jamaica", 114 | "Japan", 115 | "Jersey", 116 | "Jordan", 117 | "Kazakhstan", 118 | "Kenya", 119 | "Kiribati", 120 | "The Democratic People's Republic of Korea", 121 | "The Republic of Korea", 122 | "Kuwait", 123 | "Kyrgyzstan", 124 | "The Lao People's Democratic Republic", 125 | "Latvia", 126 | "Lebanon", 127 | "Lesotho", 128 | "Liberia", 129 | "Libya", 130 | "Liechtenstein", 131 | "Lithuania", 132 | "Luxembourg", 133 | "Macao", 134 | "Madagascar", 135 | "Malawi", 136 | "Malaysia", 137 | "Maldives", 138 | "Mali", 139 | "Malta", 140 | "The Marshall Islands", 141 | "Martinique", 142 | "Mauritania", 143 | "Mauritius", 144 | "Mayotte", 145 | "Mexico", 146 | "Micronesia", 147 | "Moldova", 148 | "Monaco", 149 | "Mongolia", 150 | "Montenegro", 151 | "Montserrat", 152 | "Morocco", 153 | "Mozambique", 154 | "Myanmar", 155 | "Namibia", 156 | "Nauru", 157 | "Nepal", 158 | "The Netherlands", 159 | "New Caledonia", 160 | "New Zealand", 161 | "Nicaragua", 162 | "The Niger", 163 | "Nigeria", 164 | "Niue", 165 | "Norfolk Island", 166 | "The Northern Mariana Islands", 167 | "Norway", 168 | "Oman", 169 | "Pakistan", 170 | "Palau", 171 | "Palestine, State of", 172 | "Panama", 173 | "Papua New Guinea", 174 | "Paraguay", 175 | "Peru", 176 | "The Philippines", 177 | "Pitcairn", 178 | "Poland", 179 | "Portugal", 180 | "Puerto Rico", 181 | "Qatar", 182 | "Republic of North Macedonia", 183 | "Romania", 184 | "The Russian Federation", 185 | "Rwanda", 186 | "Réunion", 187 | "Saint Barthélemy", 188 | "Saint Helena", 189 | "Saint Kitts and Nevis", 190 | "Saint Lucia", 191 | "Saint Martin", 192 | "Saint Pierre and Miquelon", 193 | "Saint Vincent and the Grenadines", 194 | "Samoa", 195 | "San Marino", 196 | "Sao Tome and Principe", 197 | "Saudi Arabia", 198 | "Senegal", 199 | "Serbia", 200 | "Seychelles", 201 | "Sierra Leone", 202 | "Singapore", 203 | "Sint Maarten", 204 | "Slovakia", 205 | "Slovenia", 206 | "Solomon Islands", 207 | "Somalia", 208 | "South Africa", 209 | "South Georgia and the South Sandwich Islands", 210 | "South Sudan", 211 | "Spain", 212 | "Sri Lanka", 213 | "The Sudan", 214 | "Suriname", 215 | "Svalbard and Jan Mayen", 216 | "Sweden", 217 | "Switzerland", 218 | "Syrian Arab Republic", 219 | "Taiwan", 220 | "Tajikistan", 221 | "Tanzania, United Republic of", 222 | "Thailand", 223 | "Timor-Leste", 224 | "Togo", 225 | "Tokelau", 226 | "Tonga", 227 | "Trinidad and Tobago", 228 | "Tunisia", 229 | "Turkey", 230 | "Turkmenistan", 231 | "The Turks and Caicos Islands", 232 | "Tuvalu", 233 | "Uganda", 234 | "Ukraine", 235 | "The United Arab Emirates", 236 | "The United Kingdom of Great Britain and Northern Ireland", 237 | "The United States Minor Outlying Islands", 238 | "The United States of America", 239 | "Uruguay", 240 | "Uzbekistan", 241 | "Vanuatu", 242 | "Venezuela", 243 | "Viet Nam", 244 | "British Virgin Islands", 245 | "U.S. Virgin Islands", 246 | "Wallis and Futuna", 247 | "Western Sahara", 248 | "Yemen", 249 | "Zambia", 250 | "Zimbabwe", 251 | ] 252 | 253 | module.exports = countries; 254 | -------------------------------------------------------------------------------- /static/web-components/notification-bar.js: -------------------------------------------------------------------------------- 1 | class HTMLNotificationBarElement extends HTMLElementTemplate { 2 | static CSS = ` 3 | :host { 4 | position: fixed; 5 | z-index: 99; 6 | top: 0; 7 | font-size: 1rem; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | 12 | width: 100%; 13 | text-align: center; 14 | background-color: #fff; 15 | box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 16 | } 17 | #details { 18 | max-width: 100%; 19 | overflow-x: auto; 20 | font-size: .9em; 21 | } 22 | #dismiss { 23 | position: absolute; 24 | top: 50%; 25 | right: 0; 26 | transform: translate(-50%, -50%) !important; 27 | 28 | box-sizing: content-box; 29 | width: 1em; 30 | height: 1em; 31 | padding: 0.25em 0.25em; 32 | color: #000; 33 | background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; 34 | border: 0; 35 | border-radius: 0.375rem; 36 | opacity: 0.5; 37 | } 38 | 39 | .flex-center { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | } 44 | .row { 45 | flex-wrap: wrap; 46 | } 47 | 48 | .expand { 49 | position: absolute; 50 | left: 0; 51 | width: 100%; 52 | } 53 | .top { 54 | top: 0; 55 | } 56 | .bottom { 57 | bottom: 0; 58 | } 59 | 60 | .chevron-expand, 61 | .chevron-collapse { 62 | display: inline-block; 63 | width: 16px; 64 | height: 16px; 65 | } 66 | .top .chevron-expand, 67 | .bottom .chevron-collapse { 68 | background: url("data:image/svg+xml;utf8,") no-repeat center; 69 | } 70 | .bottom .chevron-expand, 71 | .top .chevron-collapse { 72 | background: url("data:image/svg+xml;utf8,") no-repeat center; 73 | } 74 | a { 75 | cursor: pointer; 76 | } 77 | .hide, 78 | .collapse:not(.show) { 79 | display: none; 80 | } 81 | .collapsing { 82 | overflow: hidden; 83 | transition: height 0.35s ease; 84 | } 85 | @media (prefers-reduced-motion: reduce) { 86 | .collapsing { 87 | transition: none; 88 | } 89 | } 90 | `; 91 | 92 | static template = ` 93 | 94 | 95 |
96 | 97 | 98 | 99 |
100 |
101 |
102 | 103 |
104 |
105 |
106 | 107 | 108 |
109 | `; 110 | static refs = { 111 | "$dismiss": `#dismiss`, 112 | "$details": `#details`, 113 | "$open_btn": `#show-details`, 114 | "$close_btn": `#hide-details`, 115 | "$expand": `.expand`, 116 | }; 117 | 118 | 119 | // Element constants 120 | 121 | static properties = { 122 | "no-dismiss": { 123 | "type": Boolean, 124 | updateDOM () { 125 | const no_dismiss = this['no-dismiss']; 126 | 127 | this.$dismiss.style.display = no_dismiss ? "none" : "initial"; 128 | }, 129 | }, 130 | "bottom": { 131 | "type": Boolean, 132 | updateDOM () { 133 | console.log("bottom:", this.bottom ); 134 | if ( this.bottom ) { 135 | this.style.top = "initial"; 136 | this.style.bottom = 0; 137 | this.$expand.classList.remove("bottom"); 138 | this.$expand.classList.add("top"); 139 | } else { 140 | this.style.top = 0; 141 | this.style.bottom = "initial"; 142 | this.$expand.classList.remove("top"); 143 | this.$expand.classList.add("bottom"); 144 | } 145 | }, 146 | }, 147 | }; 148 | 149 | constructor () { 150 | super(); 151 | 152 | this.$open_btn.addEventListener("click", event => { 153 | this.$details.style.height = 0; 154 | 155 | this.$details.classList.add("collapsing"); 156 | this.$details.classList.remove("collapse"); 157 | 158 | this.$open_btn.classList.add("hide"); 159 | this.$close_btn.classList.remove("hide"); 160 | 161 | setTimeout(() => { 162 | this.$details.classList.remove("collapsing"); 163 | this.$details.classList.add("collapse", "show"); 164 | 165 | this.$details.style.height = ""; 166 | }, 350 ); 167 | 168 | this.$details.style.height = `${this.$details.scrollHeight}px`; 169 | }); 170 | this.$close_btn.addEventListener("click", event => { 171 | this.$details.style.height = `${this.$details.getBoundingClientRect().height}px`; 172 | 173 | // trigger reflow (https://stackoverflow.com/questions/21664940/force-browser-to-trigger-reflow-while-changing-css) 174 | this.$details.offsetHeight; 175 | 176 | this.$open_btn.classList.remove("hide"); 177 | this.$close_btn.classList.add("hide"); 178 | 179 | this.$details.classList.add("collapsing"); 180 | this.$details.classList.remove("collapse", "show"); 181 | 182 | setTimeout(() => { 183 | this.$details.classList.remove("collapsing"); 184 | this.$details.classList.add("collapse"); 185 | this.$details.style.height = ""; 186 | }, 350 ); 187 | 188 | this.$details.style.height = 0; 189 | }); 190 | 191 | this.$dismiss.addEventListener("click", event => { 192 | this.dispatchEvent( new Event('dismiss') ); 193 | }); 194 | } 195 | 196 | // connectedCallback() { 197 | // } 198 | 199 | mutationCallback() { 200 | if ( this.slots.details ) { 201 | this.$details.classList.remove("hide"); 202 | this.$expand.classList.remove("hide"); 203 | } else { 204 | this.$details.classList.add("hide"); 205 | this.$expand.classList.add("hide"); 206 | } 207 | } 208 | 209 | 210 | // Property/attribute controllers 211 | 212 | 213 | // Methods 214 | 215 | 216 | // Event handlers 217 | } 218 | 219 | customElements.define("notification-bar", HTMLNotificationBarElement ); 220 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SHELL = bash 3 | PROJECT_NAME = appstore 4 | 5 | APPSTORE_WEBHAPP = appstore.webhapp 6 | APPSTORE_HAPP = tests/appstore.happ 7 | DEVHUB_HAPP = tests/devhub.happ 8 | HAPPS = $(APPSTORE_HAPP) $(DEVHUB_HAPP) 9 | 10 | 11 | # 12 | # Runtime Setup 13 | # 14 | run-holochain: node_modules 15 | npx holochain-backdrop --admin-port 35678 --config holochain/config.yaml -v 16 | reset-holochain: 17 | rm -rf holochain/databases holochain/config.yaml tests/*_HASH 18 | reset-lair: 19 | rm -rf holochain/lair tests/AGENT* 20 | reset-all: reset-holochain reset-lair 21 | setup: $(HAPPS) 22 | node tests/setup.js app-store 23 | node tests/setup.js devhub bobby 24 | 25 | $(DEVHUB_HAPP): 26 | $(error Download missing hApp into location ./$@) 27 | $(APPSTORE_HAPP): 28 | $(error Download missing hApp into location ./$@) 29 | copy-devhub-from-local: 30 | cp ../devhub-dnas/devhub.happ $(DEVHUB_HAPP) 31 | copy-appstore-from-local: 32 | cp ../app-store-dnas/appstore.happ $(APPSTORE_HAPP) 33 | 34 | 35 | # 36 | # Testing 37 | # 38 | build: static-links 39 | npx webpack 40 | build-watch: static-links 41 | WEBPACK_MODE=development npx webpack --watch 42 | 43 | test-e2e: node_modules $(HAPPS) 44 | npx mocha -t 5000 tests/e2e/ 45 | test-e2e-debug: node_modules $(HAPPS) 46 | LOG_LEVEL=silly npx mocha -t 5000 tests/e2e/ 47 | test-e2e-crud-publishers: node_modules $(HAPPS) 48 | LOG_LEVEL=silly npx mocha -t 5000 tests/e2e/test_publisher_crud.js 49 | test-e2e-crud-app: node_modules $(HAPPS) 50 | LOG_LEVEL=silly npx mocha -t 5000 tests/e2e/test_app_crud.js 51 | 52 | user-testing-setup: 53 | node ../devhub-gui/tests/create_devhub_assets.js devhub-bobby 54 | node create_viewpoint.js $$(cat ./tests/AGENT) 55 | node tests/setup.js app-store andie 56 | make build-watch 57 | 58 | # 59 | # HTTP Server 60 | # 61 | /etc/nginx/sites-available/$(PROJECT_NAME): tests/nginx/$(PROJECT_NAME) 62 | sed -e "s|PWD|$(shell pwd)|g" \ 63 | < $< | sudo tee $@; 64 | echo " ... Wrote new $@ (from $<)"; 65 | sudo ln -fs ../sites-available/$(PROJECT_NAME) /etc/nginx/sites-enabled/ 66 | sudo systemctl reload nginx.service 67 | systemctl status nginx 68 | 69 | 70 | # 71 | # Project 72 | # 73 | package-lock.json: package.json 74 | npm install 75 | touch $@ 76 | node_modules: package-lock.json 77 | npm install 78 | touch $@ 79 | dist: static-links static/dist/webpacked.app.js 80 | static/dist/webpacked.app.js: node_modules webpack.config.js Makefile 81 | make build 82 | touch $@ 83 | static-links: node_modules\ 84 | static/dependencies\ 85 | static/dependencies/identicons.bundled.js\ 86 | static/dependencies/holochain-client/holochain-client.js\ 87 | static/dependencies/crux-payload-parser.js\ 88 | static/dependencies/crux-payload-parser.js.map\ 89 | static/dependencies/holo-hash.js\ 90 | static/dependencies/holo-hash.js.map\ 91 | static/dependencies/showdown.js\ 92 | static/dependencies/compressor.js\ 93 | static/dependencies/pako.js\ 94 | static/dependencies/msgpack.js\ 95 | static/dependencies/vue.js\ 96 | static/dependencies/vuex.js\ 97 | static/dependencies/vue-router.js\ 98 | static/web-components/purewc-template.js\ 99 | static/web-components/purewc-select-search.js 100 | static/dependencies: 101 | mkdir -p $@ 102 | 103 | static/dependencies/identicons.bundled.js: node_modules/@whi/identicons/dist/identicons.bundled.js Makefile 104 | cp $< $@ 105 | cp $<.map $@.map 106 | 107 | static/dependencies/holochain-client/holochain-client.js: node_modules/@whi/holochain-client/dist/holochain-client.js Makefile 108 | ln -fs ../../node_modules/@whi/holochain-client/dist/ static/dependencies/holochain-client 109 | 110 | static/dependencies/crux-payload-parser.js: node_modules/@whi/crux-payload-parser/dist/crux-payload-parser.js Makefile 111 | cp $< $@ 112 | cp $<.map $@.map 113 | 114 | static/dependencies/holo-hash.js: node_modules/@whi/holo-hash/dist/holo-hash.js Makefile 115 | cp $< $@ 116 | cp $<.map $@.map 117 | 118 | static/dependencies/showdown.js: node_modules/showdown/dist/showdown.js Makefile 119 | cp $< $@ 120 | cp $<.map $@.map 121 | 122 | static/dependencies/compressor.js: node_modules/compressorjs/dist/compressor.js Makefile 123 | cp $< $@ 124 | 125 | static/dependencies/pako.js: node_modules/pako/dist/pako.esm.mjs Makefile 126 | cp $< $@ 127 | 128 | static/dependencies/msgpack.js: node_modules/@msgpack/msgpack/dist.es5+umd/msgpack.js Makefile 129 | cp $< $@ 130 | cp $<.map $@.map 131 | 132 | static/dependencies/vue.js: node_modules/vue/dist/vue.global.js Makefile 133 | cp $< $@ 134 | 135 | static/dependencies/vuex.js: node_modules/vuex/dist/vuex.global.js Makefile 136 | cp $< $@ 137 | 138 | static/dependencies/vue-router.js: node_modules/vue-router/dist/vue-router.global.js Makefile 139 | cp $< $@ 140 | static/web-components/purewc-template.js: node_modules/@purewc/template/dist/purewc-template.js Makefile 141 | cp $< $@ 142 | cp $<.map $@.map 143 | static/web-components/purewc-select-search.js: node_modules/@purewc/select-search/dist/purewc-select-search.js Makefile 144 | cp $< $@ 145 | cp $<.map $@.map 146 | 147 | use-local-crux: 148 | npm uninstall @whi/crux-payload-parser 149 | npm install ../js-crux-payload-parser 150 | use-npm-crux: 151 | npm uninstall @whi/crux-payload-parser 152 | npm install @whi/crux-payload-parser 153 | use-local-client: 154 | npm uninstall @whi/holochain-client 155 | npm install --save ../holochain-client-js/ 156 | use-npm-client: 157 | npm uninstall @whi/holochain-client 158 | npm install --save @whi/holochain-client 159 | 160 | use-local-backdrop: 161 | npm uninstall @whi/holochain-backdrop 162 | npm install --save ../node-holochain-backdrop/ 163 | use-npm-backdrop: 164 | npm uninstall @whi/holochain-backdrop 165 | npm install --save @whi/holochain-backdrop 166 | 167 | use-local-openstate: 168 | npm uninstall openstate 169 | npm install --save ../openstate-js/ 170 | use-npm-openstate: 171 | npm uninstall openstate 172 | npm install --save openstate 173 | $(APPSTORE_WEBHAPP): web_assets.zip tests/appstore.happ 174 | hc web pack -o $@ ./bundled 175 | cp $@ ~/ 176 | web_assets.zip: Makefile static/* static/*/* src/* src/*/* 177 | make build 178 | cd static; zip -r ../web_assets.zip ./* 179 | 180 | 181 | # 182 | # Repository 183 | # 184 | clean-remove-chaff: 185 | @find . -name '*~' -exec rm {} \; 186 | clean-files: clean-remove-chaff 187 | git clean -nd 188 | clean-files-force: clean-remove-chaff 189 | git clean -fd 190 | clean-files-all: clean-remove-chaff 191 | git clean -ndx 192 | clean-files-all-force: clean-remove-chaff 193 | git clean -fdx 194 | -------------------------------------------------------------------------------- /tests/templates/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App Store 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 59 | 60 |
61 |
62 |
63 | 64 | App Store 65 |
66 |
67 |
68 |
69 | DevHub 70 | Add App 71 |
72 | 73 | 74 |
75 |
76 |
77 |
78 | 79 | 96 | 97 |
98 |
99 | 111 |
112 | 113 |

Details

114 | 115 |
116 | 117 | 118 |
119 | 120 | Input Error 121 |
122 |
123 | 124 |
125 |
126 |
127 | 128 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 |
140 |
141 | 142 | 143 |
144 |
145 |
146 | 147 |
148 | 149 | 150 |
151 | 152 |
153 | 154 | 155 |
156 | 157 |
158 | 159 | 160 |
161 |
162 | 163 |
164 |
165 |

Editor(s)

166 | 167 | 168 | 169 |
170 | 171 |
172 | 173 | 174 |
175 | 176 |
177 |
178 |
179 | 180 | 181 |
182 | 183 | 184 | 185 |
186 |
187 |
188 | 189 |
190 | 198 |
199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /static/templates/apps/update.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |

{{ app.title }}: {{ app.subtitle }}

5 | 6 | {{ app.description }} 7 | 8 | 9 | 10 |
11 | 13 | 15 | 16 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |

hApp HRL from DevHub

28 | 29 |
30 | 31 | 35 |
36 | {{ invalid_happ_hrl }} 37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |

48 | {{ happ.title }}: 49 | {{ happ.subtitle }} 50 | (deprecated) 51 |
52 | {{ happ.description || 'No description' }} 53 |

54 | 55 | hApp has no releases 56 | 57 | 58 | Latest Release: {{ happ_release?.version }} 59 |
60 | 61 | Latest Release does not have an official GUI 62 | 63 |
64 |

65 | 66 |
67 |
68 | {{ happ_errors.read.message }} 69 |
70 |
71 |
72 |
73 |
74 | 75 |
76 | Use hApp's Official GUI 77 |
78 | 79 | 122 |
123 | 124 | 125 |

Details

126 | 127 |
128 | 129 | 130 |
131 | 132 |
133 | 134 | 135 |
136 | 137 |
138 | 139 | 140 |
141 | 142 |
143 | 144 | 147 | 148 | 152 | 153 |
154 |
155 | 156 |
157 |
158 |

Editor(s)

159 |
160 | 161 |
163 | 164 | 165 |
166 |
167 | 168 |
169 | 185 |
186 |
187 | -------------------------------------------------------------------------------- /static/templates/apps/create.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 6 | 7 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |

hApp HRL from DevHub

20 | 21 |
22 | 23 | 27 |
28 | {{ invalid_happ_hrl }} 29 |
30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 |
39 |

40 | {{ happ.title }}: 41 | {{ happ.subtitle }} 42 | (deprecated) 43 |
44 | {{ happ.description || 'No description' }} 45 |

46 | 47 | hApp has no releases 48 | 49 | 50 | Latest Release: {{ happ_release?.version }} 51 |
52 | 53 | Latest Release does not have an official GUI 54 | 55 |
56 |

57 | 58 |
59 |
60 | {{ happ_errors.read.message }} 61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 | Use hApp's Official GUI 69 |
70 | 71 | 114 |
115 | 116 |

Details

117 | 118 |
119 | 120 | 122 |
123 | 124 |
125 | 126 | 128 |
129 | 130 |
131 | 132 | 134 |
135 | 136 |
137 | 138 | 141 | 142 | 146 | 147 |
148 |
149 | 150 |
151 |
152 |

Editor(s)

153 | 155 | 156 | 157 |
158 | 159 |
160 | 161 | 162 |
163 | 164 |
166 |
167 |
168 | 169 | 170 |
171 | 173 | 174 | 175 |
176 |
177 |
178 | 179 |
180 | 190 |
191 | 192 |
193 | -------------------------------------------------------------------------------- /static/templates/apps/single.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ app.title }}: {{ app.subtitle }}

4 | 5 | {{ app.description }} 6 | 7 | 8 | 9 |
10 | 13 |
14 | 15 |
16 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 39 | 40 |
41 |

Details

42 | 44 |
46 | Loading... 47 |
48 | 49 |
50 |
51 | 52 | 60 | 61 | 64 |
65 | 66 |

67 | {{ publisher.name }} 68 | ({{ publisher.location.region }}, {{ publisher.location.country }}) 69 |

70 |
71 |
72 | 73 |
DevHub Assets
74 | 75 |
76 |
78 |

79 | {{ happ.title }}: 80 | {{ happ.subtitle }} 81 | (deprecated) 82 |
83 | {{ happ.description || 'No description' }} 84 |

85 | Latest Release: {{ happ_release?.version }} 86 |

87 | 88 |
89 |
90 |
91 | Loading... 92 |
93 |
94 |
95 | 96 |
97 |
99 |

100 | {{ gui.name }} 101 | (deprecated) 102 |
103 | {{ gui.description || 'No description' }} 104 |

105 | Latest Release: {{ gui_release?.version }} 106 |

107 | 108 |
109 |
110 |
111 | Loading... 112 |
113 |
114 |
115 |
116 | 117 |
118 |
119 |

Editor(s)

120 |
121 | 122 |
124 | 125 | 126 |
127 |
128 | 129 |
131 | 143 | 144 |
145 | Deprecate 146 |
147 |
148 | 149 |
150 |
151 |

Moderator Actions

152 |
153 | 154 |
157 |
158 |
159 |
Authored by {{ action.author }}
160 | at {{ action.published_at }} 161 |
162 |
163 | 164 |
165 |
166 |
167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
ControlSetting
{{ key }}{{ value }}
182 |
183 |
184 | Message: 185 |
{{ action.message }}
186 |
187 |
188 |
189 |
190 | No results 191 |
192 |
193 | 194 | 218 | 219 | 258 | 259 |
260 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("main"); 3 | 4 | const json = require('@whi/json'); 5 | const { invoke } = require('@tauri-apps/api/tauri'); 6 | const { EntityArchitect, 7 | ...crux } = CruxPayloadParser; 8 | const { AgentPubKey } = holohash; 9 | 10 | Error.stackTraceLimit = Infinity; 11 | 12 | 13 | const HISTORY_PUSH_STATE = window.localStorage.getItem("PUSH_STATE"); 14 | // log.level.trace && crux.log.setLevel("trace"); 15 | 16 | const client_init = require('./client.js'); 17 | const openstate_init = require('./openstate.js'); 18 | const common = require('./common.js'); 19 | const filters = require('./filters.js'); 20 | 21 | const generics_init = require('./generic_controllers.js'); 22 | const admin_init = require('./admin_controllers.js'); 23 | const publishers_init = require('./publisher_controllers.js'); 24 | const apps_init = require('./app_controllers.js'); 25 | 26 | 27 | const HOST_VALUE = localStorage.getItem("APP_HOST"); 28 | const PORT_VALUE = localStorage.getItem("APP_PORT"); 29 | const APP_PORT = parseInt( PORT_VALUE ) || 44001; 30 | const APP_HOST = HOST_VALUE || "127.0.0.1"; 31 | const CONDUCTOR_URI = `${APP_HOST}:${APP_PORT}`; 32 | const APP_ID = localStorage.getItem("APP_ID") || "app-store"; 33 | 34 | 35 | if ( isNaN( APP_PORT ) ) 36 | throw new Error(`Invalid 'APP_PORT' (${PORT_VALUE}); run 'localStorage.setItem( "APP_PORT", "" );`); 37 | 38 | 39 | async function setup_clients() { 40 | try { 41 | const launcher_config = window.__HC_LAUNCHER_ENV__; 42 | 43 | if ( !launcher_config ) 44 | throw new TypeError(`No launcher config @ window.__HC_LAUNCHER_ENV__`); 45 | 46 | const [appstore] = await client_init( 47 | `${APP_HOST}:${launcher_config.APP_INTERFACE_PORT}`, 48 | launcher_config.INSTALLED_APP_ID 49 | ); 50 | 51 | appstore.setSigningHandler( async zomeCallUnsigned => { 52 | zomeCallUnsigned.provenance = Array.from( zomeCallUnsigned.provenance ); 53 | zomeCallUnsigned.cell_id = [ 54 | Array.from( zomeCallUnsigned.cell_id[0] ), 55 | Array.from( zomeCallUnsigned.cell_id[1] ), 56 | ]; 57 | zomeCallUnsigned.payload = Array.from( zomeCallUnsigned.payload ); 58 | zomeCallUnsigned.nonce = Array.from( zomeCallUnsigned.nonce ); 59 | 60 | const signedZomeCall = await invoke("sign_zome_call", { 61 | zomeCallUnsigned, 62 | }); 63 | 64 | signedZomeCall.cap_secret = null; 65 | signedZomeCall.provenance = Uint8Array.from( signedZomeCall.provenance ); 66 | signedZomeCall.cell_id = [ 67 | Uint8Array.from( signedZomeCall.cell_id[0] ), 68 | Uint8Array.from( signedZomeCall.cell_id[1] ), 69 | ]; 70 | signedZomeCall.payload = Uint8Array.from( signedZomeCall.payload ); 71 | signedZomeCall.signature = Uint8Array.from( signedZomeCall.signature || [] ); 72 | signedZomeCall.nonce = Uint8Array.from( signedZomeCall.nonce ); 73 | 74 | log.trace("Signed request input:", signedZomeCall ); 75 | return signedZomeCall; 76 | }); 77 | 78 | return [appstore]; 79 | } catch (err) { 80 | log.warn("Using hard-coded configuration because launcher config produced error: %s", err.toString() ); 81 | } 82 | 83 | return await client_init( CONDUCTOR_URI, APP_ID ); 84 | } 85 | 86 | 87 | (async function(global) { 88 | const [appstore] = await setup_clients(); 89 | 90 | log.warn("Pre-load WASMs with 30s timeout"); 91 | await appstore.call("appstore", "appstore_api", "whoami", null, 30_000 ); 92 | 93 | const openstate = await openstate_init([ appstore ]); 94 | const generic_controllers = await generics_init(); 95 | const admin_controllers = await admin_init(); 96 | const publisher_controllers = await publishers_init(); 97 | const app_controllers = await apps_init(); 98 | 99 | window.appstore_client = appstore; 100 | window.openstate = openstate; 101 | 102 | const route_components = [ 103 | [ "/", generic_controllers.main, "Main" ], 104 | [ "/profile", generic_controllers.single, "Profile" ], 105 | [ "/admin", admin_controllers.dashboard, "Admin" ], 106 | 107 | [ "/publishers", publisher_controllers.list, "All publishers" ], 108 | [ "/publishers/new", publisher_controllers.create, "Add publisher" ], 109 | [ "/publishers/:id", publisher_controllers.single, "publisher Info" ], 110 | [ "/publishers/:id/update", publisher_controllers.update, "Edit publisher" ], 111 | 112 | [ "/apps", app_controllers.list, "All Apps" ], 113 | [ "/apps/new", app_controllers.create, "Add App" ], 114 | [ "/apps/:id", app_controllers.single, "App Info" ], 115 | [ "/apps/:id/update", app_controllers.update, "Edit App" ], 116 | ]; 117 | 118 | const breadcrumb_mapping = {}; 119 | const routes = []; 120 | for (let [ path, component, name ] of route_components ) { 121 | log.trace("Adding route path: %s", path ); 122 | 123 | if ( /\/(:[A-Za-z-_+]+)/.test( path ) ) { 124 | const re = "^" + path.replace(/\/(:[A-Za-z-_+]+)/g, "/[A-Za-z0-9-_+]+") + "$"; 125 | breadcrumb_mapping[ re ] = name; 126 | } 127 | else 128 | breadcrumb_mapping[ path ] = name; 129 | 130 | routes.push({ 131 | path, 132 | component, 133 | }); 134 | } 135 | log.normal("Configured %s routes for App", routes.length ); 136 | 137 | const router = VueRouter.createRouter({ 138 | "history": HISTORY_PUSH_STATE === "true" 139 | ? VueRouter.createWebHistory() 140 | : VueRouter.createWebHashHistory(), 141 | routes, 142 | "linkActiveClass": "parent-active", 143 | "linkExactActiveClass": "active", 144 | }); 145 | 146 | let $root; 147 | const app = Vue.createApp({ 148 | data () { 149 | return { 150 | "show_copied_message": false, 151 | "status_view_data": null, 152 | "status_view_html": null, 153 | "errors": [], 154 | }; 155 | }, 156 | "computed": { 157 | ...common.scopedPathComputed( "viewpoint/group", "viewpoint"), 158 | ...common.scopedPathComputed( "agent/me", "agent" ), 159 | }, 160 | async created () { 161 | $root = this; 162 | const { TimeoutError } = await HolochainClient; 163 | 164 | this.$router.afterEach( (to, from, failure) => { 165 | if ( failure instanceof Error ) 166 | return log.error("Failed to Navigate:", failure.message ); 167 | 168 | log.normal("Navigated to:", to.path, from.path ); 169 | 170 | if ( to.matched.length === 0 ) 171 | return this.showStatusView( 404 ); 172 | 173 | this.showStatusView( false ); 174 | }); 175 | 176 | await this.$openstate.read("viewpoint/group"); 177 | 178 | try { 179 | await this.$openstate.get("agent/me"); 180 | } catch (err) { 181 | if ( err instanceof TimeoutError ) 182 | return this.showStatusView( 408, { 183 | "title": "Connection Timeout", 184 | "message": `Request Timeout - Client could not connect to the Conductor interface`, 185 | "details": [ 186 | `${err.name}: ${err.message}`, 187 | ], 188 | }); 189 | else 190 | console.error( err ); 191 | } 192 | 193 | }, 194 | "methods": { 195 | dismissError ( index ) { 196 | this.errors.splice( index, 1 ); 197 | }, 198 | }, 199 | }); 200 | 201 | app.mixin({ 202 | data () { 203 | return { 204 | "json": json, 205 | "Entity": EntityArchitect.Entity, 206 | "Collection": EntityArchitect.Collection, 207 | 208 | ...holohash, 209 | 210 | console, 211 | }; 212 | }, 213 | "computed": { 214 | }, 215 | "methods": { 216 | $debug ( value ) { 217 | log.trace("JSON debug for value:", value ); 218 | return json.debug( value ); 219 | }, 220 | navigateBack () { 221 | if ( history.length > 2 ) 222 | history.back(); 223 | else 224 | this.$router.push("/"); 225 | }, 226 | async mustGet ( callback ) { 227 | try { 228 | await callback(); 229 | } catch (err) { 230 | this.catchStatusCodes([ 404, 500 ], err ); 231 | log.error("Failed to get required resource(s): %s", err.message, err ); 232 | } 233 | }, 234 | async catchStatusCodes ( status_codes, err ) { 235 | if ( !Array.isArray(status_codes) ) 236 | status_codes = [ status_codes ]; 237 | 238 | status_codes.forEach( (code, i) => { 239 | status_codes[i] = parseInt( code ); 240 | }); 241 | 242 | if ( status_codes.includes( 404 ) && err.name === "EntryNotFoundError" ) 243 | this.$root.showStatusView( 404 ); 244 | else if ( status_codes.includes( 500 ) ) 245 | this.$root.showStatusView( 500 ); 246 | }, 247 | async showStatusView ( status, data = null ) { 248 | if ( !status ) { // reset status view 249 | this.$root.status_view_html = null; 250 | return; 251 | } 252 | 253 | if ( data ) { 254 | this.$root.status_view_data = Object.assign( { 255 | "code": status, 256 | "title": "It's not me, it's you", 257 | "message": "Default HTTP Code Name", 258 | "details": null, 259 | }, data ); 260 | this.$root.status_view_html = true; 261 | 262 | return; 263 | } 264 | 265 | try { 266 | this.$root.status_view_html = await common.load_html(`/templates/${status}.html`); 267 | } catch (err) { 268 | log.error("%s", err.message, err ); 269 | this.$root.status_view_html = await common.load_html(`/templates/500.html`); 270 | } 271 | }, 272 | getPathId ( key ) { 273 | const path_id = this.$route.params[key]; 274 | 275 | try { 276 | return new holohash.ActionHash( path_id ); 277 | } catch (err) { 278 | if ( err instanceof holohash.HoloHashError ) { 279 | this.showStatusView( 400, { 280 | "title": "Invalid Identifier", 281 | "message": `Invalid Holo Hash in URL path`, 282 | "details": [ 283 | `${path_id}`, 284 | `${err.name}: ${err.message}`, 285 | ], 286 | }); 287 | } 288 | 289 | throw err; 290 | } 291 | }, 292 | async createMereMemoryEntry ( bytes ) { 293 | const path = `appstore/memory/${common.randomHex()}`; 294 | this.$openstate.mutable[path] = bytes; 295 | const addr = await this.$openstate.write( path ); 296 | this.$openstate.purge( path ); 297 | return addr; 298 | }, 299 | 300 | ...common, 301 | 302 | isViewpointAdmin () { 303 | if ( !this.$root.viewpoint ) 304 | return false; 305 | if ( !this.$root.agent ) 306 | return false; 307 | 308 | return common.isViewpointAdmin( this.$root.viewpoint, this.$root.agent.pubkey.initial ) 309 | }, 310 | isViewpointMember () { 311 | if ( !this.$root.viewpoint ) 312 | return false; 313 | if ( !this.$root.agent ) 314 | return false; 315 | 316 | return common.isViewpointMember( this.$root.viewpoint, this.$root.agent.pubkey.initial ) 317 | }, 318 | }, 319 | }); 320 | 321 | Object.assign( app.config.globalProperties, { 322 | window, 323 | document, 324 | history, 325 | location, 326 | "$clients": [appstore], 327 | "$filters": filters, 328 | "$openstate": openstate, 329 | breadcrumb_mapping, 330 | }); 331 | 332 | app.config.compilerOptions.isCustomElement = (tag) => { 333 | if ( tag.startsWith("router") ) 334 | return false; 335 | 336 | return tag.includes('-'); 337 | }; 338 | 339 | app.config.errorHandler = function (err, vm, info) { 340 | log.error("Vue App Error (%s):", info, err, vm ); 341 | $root.errors.push( err.message ); 342 | }; 343 | 344 | window.addEventListener("unhandledrejection", (event) => { 345 | $root.errors.push( event.reason ); 346 | }); 347 | window.addEventListener("error", (err) => { 348 | $root.errors.push( err.message ); 349 | }); 350 | 351 | 352 | app.use( router ); 353 | app.mount("#app"); 354 | 355 | global._App = app; 356 | global._Router = router; 357 | 358 | log.info("Finished App configuration and mounting"); 359 | })(window); 360 | -------------------------------------------------------------------------------- /src/app_controllers.js: -------------------------------------------------------------------------------- 1 | const { Logger } = require('@whi/weblogger'); 2 | const log = new Logger("apps"); 3 | 4 | const common = require('./common.js'); 5 | const { HoloHash, 6 | DnaHash, 7 | EntryHash, 8 | AgentPubKey } = holohash; 9 | 10 | 11 | module.exports = async function () { 12 | async function create () { 13 | return { 14 | "template": await common.load_html("/templates/apps/create.html"), 15 | "data": function() { 16 | return { 17 | "datapath": `app/${common.randomHex()}`, 18 | "happ_hrl": null, 19 | "gui_hrl": null, 20 | "invalid_happ_hrl": null, 21 | "invalid_gui_hrl": null, 22 | "use_official_gui": true, 23 | }; 24 | }, 25 | "computed": { 26 | ...common.scopedPathComputed( c => c.datapath, "app" ), 27 | ...common.scopedPathComputed( `agent/me/publishers`, "publishers", { "get": true }), 28 | 29 | happ_datapath () { 30 | return this.app$.devhub_address.happ 31 | ? `${this.app$.devhub_address.dna}/happ/${this.app$.devhub_address.happ}` 32 | : this.$openstate.DEADEND; 33 | }, 34 | happ_release_datapath () { 35 | return this.app$.devhub_address.happ 36 | ? `${this.app$.devhub_address.dna}/happ/${this.app$.devhub_address.happ}/releases/latest` 37 | : this.$openstate.DEADEND; 38 | }, 39 | gui_datapath () { 40 | return this.app$.devhub_address.gui 41 | ? `${this.app$.devhub_address.dna}/gui/${this.app$.devhub_address.gui}` 42 | : this.$openstate.DEADEND; 43 | }, 44 | gui_release_datapath () { 45 | return this.app$.devhub_address.gui 46 | ? `${this.app$.devhub_address.dna}/gui/${this.app$.devhub_address.gui}/releases/latest` 47 | : this.$openstate.DEADEND; 48 | }, 49 | ...common.scopedPathComputed( c => c.happ_datapath, "happ" ), 50 | ...common.scopedPathComputed( c => c.happ_release_datapath, "happ_release" ), 51 | ...common.scopedPathComputed( c => c.gui_datapath, "gui" ), 52 | ...common.scopedPathComputed( c => c.gui_release_datapath, "gui_release" ), 53 | }, 54 | "methods": { 55 | refreshDevHubPaths () { 56 | this.readDevHubHapp(); 57 | this.readDevHubGUI(); 58 | }, 59 | async readDevHubHapp () { 60 | if ( this.happ_datapath === this.$openstate.DEADEND ) 61 | return; 62 | 63 | const happ = await this.$openstate.read( this.happ_datapath ); 64 | 65 | if ( !this.app$.title ) 66 | this.app$.title = happ.title; 67 | if ( !this.app$.subtitle ) 68 | this.app$.subtitle = happ.subtitle; 69 | if ( !this.app$.description ) 70 | this.app$.description = happ.description; 71 | 72 | await this.$openstate.read( this.happ_release_datapath ); 73 | }, 74 | async readDevHubGUI () { 75 | if ( this.gui_datapath === this.$openstate.DEADEND ) 76 | return; 77 | 78 | await this.$openstate.read( this.gui_datapath ); 79 | await this.$openstate.read( this.gui_release_datapath ); 80 | }, 81 | clearErrors () { 82 | this.app_errors.write = null; 83 | }, 84 | handleHappHRL ( hrl ) { 85 | if ( !hrl ) { 86 | this.invalid_happ_hrl = "HRL is required"; 87 | return; 88 | } 89 | 90 | log.info("hApp HRL:", hrl ); 91 | try { 92 | let [dna_hash, happ_hash] = common.parseHRL( hrl ); 93 | 94 | this.app$.devhub_address.dna = dna_hash; 95 | this.setDevHubHapp( happ_hash ); 96 | this.invalid_happ_hrl = false; 97 | } catch (err) { 98 | this.invalid_happ_hrl = String(err); 99 | } 100 | }, 101 | handleGUIHRL ( hrl ) { 102 | if ( !hrl ) { 103 | this.invalid_gui_hrl = "HRL is required"; 104 | return; 105 | } 106 | 107 | log.info("GUI HRL:", hrl ); 108 | try { 109 | let [dna_hash, gui_hash] = common.parseHRL( hrl ); 110 | 111 | if ( String(dna_hash) !== String(this.app$.devhub_address.dna) ) 112 | throw new Error(`DNA hash from GUI HRL does not match the DNA hash in the hApp HRL`); 113 | 114 | this.setDevHubGUI( gui_hash ); 115 | this.invalid_gui_hrl = false; 116 | } catch (err) { 117 | this.invalid_gui_hrl = String(err); 118 | } 119 | }, 120 | setDevHubHapp ( happ_id ) { 121 | this.app$.devhub_address.happ = happ_id; 122 | this.readDevHubHapp(); 123 | }, 124 | setDevHubGUI ( gui_id ) { 125 | this.app$.devhub_address.gui = gui_id; 126 | this.readDevHubGUI(); 127 | }, 128 | resetDevHubAddress () { 129 | this.app$.devhub_address.happ = null; 130 | this.app$.devhub_address.gui = null; 131 | this.gui_hrl = null; 132 | this.use_official_gui = true; 133 | }, 134 | async compressIcon () { 135 | if ( !this.app$.icon ) 136 | return; 137 | 138 | if ( this.app$.icon.file.name.endsWith(".svg") ) { 139 | this.app$.metadata.icon_mime_type = "image/svg+xml"; 140 | return; 141 | } 142 | 143 | delete this.app$.metadata.icon_mime_type; 144 | 145 | const compressed = await common.compressImage( this.app$.icon, { 146 | "mimeType": "image/jpeg", 147 | "maxWidth": 512, 148 | "maxHeight": 512, 149 | "convertSize": 50_000, 150 | }); 151 | this.app$.icon = compressed.result; 152 | }, 153 | async create () { 154 | console.log("Writing", this.app$ ); 155 | await this.$openstate.write( this.datapath ); 156 | 157 | const new_id = this.app.$id; 158 | this.$openstate.purge( this.datapath ); 159 | 160 | await this.$openstate.read("apps"); 161 | 162 | this.$router.push( "/apps/" + new_id ); 163 | }, 164 | 165 | actionErrors () { 166 | const errors = []; 167 | errors.push( ...this.app_rejections ); 168 | 169 | const error = this.app_errors.write; 170 | if ( error ) { 171 | const name = error.name; 172 | if ( name === "RibosomeDeserializeError" ) { 173 | const message = error.message.split('[')[0]; 174 | const json = this.$debug( error.data ); 175 | errors.push( `${name}: ${message}\n\n${json}` ); 176 | } 177 | else 178 | errors.push( this.app_errors.write ); 179 | } 180 | 181 | return errors; 182 | }, 183 | }, 184 | }; 185 | }; 186 | 187 | async function single () { 188 | return { 189 | "template": await common.load_html("/templates/apps/single.html"), 190 | "data": function() { 191 | const id = this.getPathId("id"); 192 | 193 | return { 194 | id, 195 | "datapath": `app/${id}`, 196 | "publisher_datapath": `app/${id}/publisher`, 197 | "package_datapath": `app/${id}/package`, 198 | "ma_history_datapath": `app/${id}/moderator/actions`, 199 | "ma_datapath": `app/${id}/moderator/state`, 200 | }; 201 | }, 202 | async created () { 203 | await this.mustGet(async () => { 204 | this.$openstate.read( this.publisher_datapath ); 205 | await this.$openstate.read( this.datapath ); 206 | await this.$openstate.get( this.ma_datapath ); 207 | }); 208 | 209 | this.readDevHubHapp(); 210 | if ( this.app.devhub_address.gui ) 211 | this.readDevHubGUI(); 212 | }, 213 | "computed": { 214 | happ_datapath () { 215 | return this.app?.devhub_address.happ 216 | ? `${this.app.devhub_address.dna}/happ/${this.app.devhub_address.happ}` 217 | : this.$openstate.DEADEND; 218 | }, 219 | happ_release_datapath () { 220 | return this.app?.devhub_address.happ 221 | ? `${this.app.devhub_address.dna}/happ/${this.app.devhub_address.happ}/releases/latest` 222 | : this.$openstate.DEADEND; 223 | }, 224 | gui_datapath () { 225 | return this.app?.devhub_address.gui 226 | ? `${this.app.devhub_address.dna}/gui/${this.app.devhub_address.gui}` 227 | : this.$openstate.DEADEND; 228 | }, 229 | gui_release_datapath () { 230 | return this.app?.devhub_address.gui 231 | ? `${this.app.devhub_address.dna}/gui/${this.app.devhub_address.gui}/releases/latest` 232 | : this.$openstate.DEADEND; 233 | }, 234 | ...common.scopedPathComputed( c => c.datapath, "app" ), 235 | ...common.scopedPathComputed( c => c.publisher_datapath, "publisher" ), 236 | ...common.scopedPathComputed( c => c.package_datapath, "package" ), 237 | ...common.scopedPathComputed( c => c.happ_datapath, "happ" ), 238 | ...common.scopedPathComputed( c => c.happ_release_datapath, "happ_release" ), 239 | ...common.scopedPathComputed( c => c.gui_datapath, "gui" ), 240 | ...common.scopedPathComputed( c => c.gui_release_datapath, "gui_release" ), 241 | ...common.scopedPathComputed( c => c.ma_history_datapath, "moderator_actions" ), 242 | ...common.scopedPathComputed( c => c.ma_datapath, "moderator_action" ), 243 | 244 | deprecationModal () { 245 | return new bootstrap.Modal( this.$refs["deprecation-modal"], { 246 | "backdrop": "static", 247 | "keyboard": false, 248 | }); 249 | }, 250 | 251 | moderatorModal () { 252 | return new bootstrap.Modal( this.$refs["moderator-modal"], { 253 | "backdrop": "static", 254 | "keyboard": false, 255 | }); 256 | }, 257 | }, 258 | "methods": { 259 | refresh () { 260 | this.$openstate.read( this.datapath ); 261 | this.$openstate.read( this.publisher_datapath ); 262 | }, 263 | async readDevHubHapp () { 264 | await this.$openstate.read( this.happ_datapath ); 265 | await this.$openstate.read( this.happ_release_datapath ); 266 | }, 267 | async readDevHubGUI () { 268 | await this.$openstate.read( this.gui_datapath ); 269 | await this.$openstate.read( this.gui_release_datapath ); 270 | }, 271 | async downloadApp () { 272 | if ( this.$package.reading ) 273 | return; 274 | 275 | const bytes = await this.$openstate.read( this.package_datapath, { 276 | "rememberState": false, 277 | }); 278 | 279 | console.log("App pacakge:", bytes ); 280 | this.download( `${this.app.title}.webhapp`, bytes ); 281 | }, 282 | showModeratorModal ( remove = true ) { 283 | this.moderator_action$.message = ""; 284 | this.moderator_action$.metadata.remove = remove; 285 | 286 | this.moderatorModal.show(); 287 | }, 288 | async confirmDeprecation () { 289 | log.normal("Deprecating App %s", this.app.title ); 290 | await this.$openstate.write( this.datapath, "deprecation" ); 291 | 292 | this.deprecationModal.hide(); 293 | }, 294 | async confirmModerator () { 295 | log.normal("Removing App %s", this.app.title ); 296 | await this.$openstate.write( this.ma_datapath ); 297 | await this.$openstate.resetMutable( this.ma_datapath ); 298 | 299 | this.moderatorModal.hide(); 300 | }, 301 | }, 302 | }; 303 | }; 304 | 305 | async function update () { 306 | return { 307 | "template": await common.load_html("/templates/apps/update.html"), 308 | "data": function() { 309 | const id = this.getPathId("id"); 310 | 311 | return { 312 | id, 313 | "datapath": `app/${id}`, 314 | "new_icon": null, 315 | "happ_hrl": null, 316 | "gui_hrl": null, 317 | "invalid_happ_hrl": null, 318 | "invalid_gui_hrl": null, 319 | "use_official_gui": true, 320 | }; 321 | }, 322 | "computed": { 323 | happ_datapath () { 324 | return this.app$.devhub_address.happ 325 | ? `${this.app$.devhub_address.dna}/happ/${this.app$.devhub_address.happ}` 326 | : this.$openstate.DEADEND; 327 | }, 328 | happ_release_datapath () { 329 | return this.app$.devhub_address.happ 330 | ? `${this.app$.devhub_address.dna}/happ/${this.app$.devhub_address.happ}/releases/latest` 331 | : this.$openstate.DEADEND; 332 | }, 333 | gui_datapath () { 334 | return this.app$.devhub_address.gui 335 | ? `${this.app$.devhub_address.dna}/gui/${this.app$.devhub_address.gui}` 336 | : this.$openstate.DEADEND; 337 | }, 338 | gui_release_datapath () { 339 | return this.app$.devhub_address.gui 340 | ? `${this.app$.devhub_address.dna}/gui/${this.app$.devhub_address.gui}/releases/latest` 341 | : this.$openstate.DEADEND; 342 | }, 343 | ...common.scopedPathComputed( c => c.datapath, "app" ), 344 | ...common.scopedPathComputed( `agent/me/publishers`, "publishers", { "get": true }), 345 | ...common.scopedPathComputed( c => c.happ_datapath, "happ" ), 346 | ...common.scopedPathComputed( c => c.happ_release_datapath, "happ_release" ), 347 | ...common.scopedPathComputed( c => c.gui_datapath, "gui" ), 348 | ...common.scopedPathComputed( c => c.gui_release_datapath, "gui_release" ), 349 | }, 350 | async created () { 351 | await this.mustGet(async () => { 352 | await this.$openstate.get( this.datapath ); 353 | 354 | this.use_official_gui = !this.app$.devhub_address.gui; 355 | }); 356 | 357 | this.readDevHubHapp(); 358 | if ( this.app$.devhub_address.gui ) 359 | this.readDevHubGUI(); 360 | }, 361 | "methods": { 362 | refreshDevHubPaths () { 363 | this.readDevHubHapp(); 364 | this.readDevHubGUI(); 365 | }, 366 | async readDevHubHapp () { 367 | await this.$openstate.read( this.happ_datapath ); 368 | await this.$openstate.read( this.happ_release_datapath ); 369 | }, 370 | async readDevHubGUI () { 371 | await this.$openstate.read( this.gui_datapath ); 372 | await this.$openstate.read( this.gui_release_datapath ); 373 | }, 374 | handleHappHRL ( hrl ) { 375 | if ( !hrl ) { 376 | this.invalid_happ_hrl = "HRL is required"; 377 | return; 378 | } 379 | 380 | log.info("hApp HRL:", hrl ); 381 | try { 382 | let [dna_hash, happ_hash] = common.parseHRL( hrl ); 383 | 384 | this.app$.devhub_address.dna = dna_hash; 385 | this.setDevHubHapp( happ_hash ); 386 | this.invalid_happ_hrl = false; 387 | } catch (err) { 388 | this.invalid_happ_hrl = String(err); 389 | } 390 | }, 391 | handleGUIHRL ( hrl ) { 392 | if ( !hrl ) { 393 | this.invalid_gui_hrl = "HRL is required"; 394 | return; 395 | } 396 | 397 | log.info("GUI HRL:", hrl ); 398 | try { 399 | let [dna_hash, gui_hash] = common.parseHRL( hrl ); 400 | 401 | if ( String(dna_hash) !== String(this.app$.devhub_address.dna) ) 402 | throw new Error(`DNA hash from GUI HRL does not match the DNA hash in the hApp HRL`); 403 | 404 | this.setDevHubGUI( gui_hash ); 405 | this.invalid_gui_hrl = false; 406 | } catch (err) { 407 | this.invalid_gui_hrl = String(err); 408 | } 409 | }, 410 | setDevHubHapp ( happ_id ) { 411 | this.app$.devhub_address.happ = happ_id; 412 | this.readDevHubHapp(); 413 | }, 414 | setDevHubGUI ( gui_id ) { 415 | this.app$.devhub_address.gui = gui_id; 416 | this.readDevHubGUI(); 417 | }, 418 | resetDevHubAddress () { 419 | this.app$.devhub_address.happ = null; 420 | this.app$.devhub_address.gui = null; 421 | this.gui_hrl = null; 422 | this.use_official_gui = true; 423 | }, 424 | async compressIcon () { 425 | if ( !this.new_icon ) 426 | return; 427 | 428 | if ( this.new_icon.file?.name.endsWith(".svg") ) 429 | return; 430 | 431 | const compressed = await common.compressImage( this.new_icon, { 432 | "mimeType": "image/jpeg", 433 | "maxWidth": 512, 434 | "maxHeight": 512, 435 | "convertSize": 50_000, 436 | }); 437 | this.new_icon = compressed.result; 438 | }, 439 | async update () { 440 | console.log("Writing", this.app$ ); 441 | 442 | let current_icon_bytes = this.$openstate.state[`appstore/memory/${this.app$.icon}`]; 443 | if ( this.new_icon && 444 | !common.equalUint8Arrays( this.new_icon, current_icon_bytes ) ) { 445 | this.app$.icon = this.new_icon; 446 | 447 | if ( this.new_icon.file?.name.endsWith(".svg") ) 448 | this.app$.metadata.icon_mime_type = "image/svg+xml"; 449 | else 450 | delete this.app$.metadata.icon_mime_type; 451 | } 452 | await this.$openstate.write( this.datapath ); 453 | 454 | this.new_icon = null; 455 | await this.$openstate.read("apps"); 456 | 457 | this.$router.push( "/apps/" + this.id ); 458 | }, 459 | 460 | actionErrors () { 461 | const errors = []; 462 | errors.push( ...this.app_rejections ); 463 | 464 | const error = this.app_errors.write; 465 | if ( error ) { 466 | const name = error.name; 467 | if ( name === "RibosomeDeserializeError" ) { 468 | const message = error.message.split('[')[0]; 469 | const json = this.$debug( error.data ); 470 | errors.push( `${name}: ${message}\n\n${json}` ); 471 | } 472 | else 473 | errors.push( this.app_errors.write ); 474 | } 475 | 476 | return errors; 477 | }, 478 | }, 479 | }; 480 | }; 481 | 482 | return { 483 | create, 484 | update, 485 | single, 486 | }; 487 | }; 488 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "cargo-chef": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1672901199, 7 | "narHash": "sha256-MHTuR4aQ1rQaBKx1vWDy2wbvcT0ZAzpkVB2zylSC+k0=", 8 | "owner": "LukeMathWalker", 9 | "repo": "cargo-chef", 10 | "rev": "5c9f11578a2e0783cce27666737d50f84510b8b5", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "LukeMathWalker", 15 | "ref": "main", 16 | "repo": "cargo-chef", 17 | "type": "github" 18 | } 19 | }, 20 | "cargo-rdme": { 21 | "flake": false, 22 | "locked": { 23 | "lastModified": 1675118998, 24 | "narHash": "sha256-lrYWqu3h88fr8gG3Yo5GbFGYaq5/1Os7UtM+Af0Bg4k=", 25 | "owner": "orium", 26 | "repo": "cargo-rdme", 27 | "rev": "f9dbb6bccc078f4869f45ae270a2890ac9a75877", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "orium", 32 | "ref": "v1.1.0", 33 | "repo": "cargo-rdme", 34 | "type": "github" 35 | } 36 | }, 37 | "crane": { 38 | "inputs": { 39 | "flake-compat": "flake-compat", 40 | "flake-utils": "flake-utils", 41 | "nixpkgs": [ 42 | "holochain-flake", 43 | "nixpkgs" 44 | ], 45 | "rust-overlay": "rust-overlay" 46 | }, 47 | "locked": { 48 | "lastModified": 1675475924, 49 | "narHash": "sha256-KWdfV9a6+zG6Ij/7PZYLnomjBZZUu8gdRy+hfjGrrJQ=", 50 | "owner": "ipetkov", 51 | "repo": "crane", 52 | "rev": "1bde9c762ebf26de9f8ecf502357c92105bc4577", 53 | "type": "github" 54 | }, 55 | "original": { 56 | "owner": "ipetkov", 57 | "repo": "crane", 58 | "type": "github" 59 | } 60 | }, 61 | "crate2nix": { 62 | "flake": false, 63 | "locked": { 64 | "lastModified": 1675642992, 65 | "narHash": "sha256-uDBDZuiq7qyg82Udp82/r4zg5HKfIzBQqgl2U9THiQM=", 66 | "owner": "kolloch", 67 | "repo": "crate2nix", 68 | "rev": "45fc83132c8c91c77a1cd61fe0c945411d1edba8", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "kolloch", 73 | "repo": "crate2nix", 74 | "type": "github" 75 | } 76 | }, 77 | "flake-compat": { 78 | "flake": false, 79 | "locked": { 80 | "lastModified": 1673956053, 81 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 82 | "owner": "edolstra", 83 | "repo": "flake-compat", 84 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "edolstra", 89 | "repo": "flake-compat", 90 | "type": "github" 91 | } 92 | }, 93 | "flake-compat_2": { 94 | "flake": false, 95 | "locked": { 96 | "lastModified": 1673956053, 97 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 98 | "owner": "edolstra", 99 | "repo": "flake-compat", 100 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "edolstra", 105 | "repo": "flake-compat", 106 | "type": "github" 107 | } 108 | }, 109 | "flake-parts": { 110 | "inputs": { 111 | "nixpkgs-lib": "nixpkgs-lib" 112 | }, 113 | "locked": { 114 | "lastModified": 1675295133, 115 | "narHash": "sha256-dU8fuLL98WFXG0VnRgM00bqKX6CEPBLybhiIDIgO45o=", 116 | "owner": "hercules-ci", 117 | "repo": "flake-parts", 118 | "rev": "bf53492df08f3178ce85e0c9df8ed8d03c030c9f", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "id": "flake-parts", 123 | "type": "indirect" 124 | } 125 | }, 126 | "flake-utils": { 127 | "locked": { 128 | "lastModified": 1667395993, 129 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 130 | "owner": "numtide", 131 | "repo": "flake-utils", 132 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 133 | "type": "github" 134 | }, 135 | "original": { 136 | "owner": "numtide", 137 | "repo": "flake-utils", 138 | "type": "github" 139 | } 140 | }, 141 | "flake-utils_2": { 142 | "locked": { 143 | "lastModified": 1659877975, 144 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 145 | "owner": "numtide", 146 | "repo": "flake-utils", 147 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 148 | "type": "github" 149 | }, 150 | "original": { 151 | "owner": "numtide", 152 | "repo": "flake-utils", 153 | "type": "github" 154 | } 155 | }, 156 | "holochain": { 157 | "flake": false, 158 | "locked": { 159 | "lastModified": 1694632043, 160 | "narHash": "sha256-5QWUpWnwuzUi3hROrOZyQNla8iGdr+oGCH2nniRePBE=", 161 | "owner": "holochain", 162 | "repo": "holochain", 163 | "rev": "1f59d33623031eefe76b5f3573970c9c33f21877", 164 | "type": "github" 165 | }, 166 | "original": { 167 | "owner": "holochain", 168 | "ref": "holochain-0.2.2", 169 | "repo": "holochain", 170 | "type": "github" 171 | } 172 | }, 173 | "holochain-flake": { 174 | "inputs": { 175 | "cargo-chef": "cargo-chef", 176 | "cargo-rdme": "cargo-rdme", 177 | "crane": "crane", 178 | "crate2nix": "crate2nix", 179 | "flake-compat": "flake-compat_2", 180 | "flake-parts": "flake-parts", 181 | "holochain": "holochain", 182 | "lair": "lair", 183 | "launcher": [ 184 | "holochain-flake", 185 | "versions", 186 | "launcher" 187 | ], 188 | "nix-filter": "nix-filter", 189 | "nixpkgs": "nixpkgs", 190 | "pre-commit-hooks-nix": "pre-commit-hooks-nix", 191 | "rust-overlay": "rust-overlay_2", 192 | "scaffolding": [ 193 | "holochain-flake", 194 | "versions", 195 | "scaffolding" 196 | ], 197 | "versions": "versions" 198 | }, 199 | "locked": { 200 | "lastModified": 1679465852, 201 | "narHash": "sha256-NPTidQcJn6XTmGXxN9ue2DEJqzcgmTzGtmmi8KITVJc=", 202 | "owner": "holochain", 203 | "repo": "holochain", 204 | "rev": "f6e8935c46f68f4a5ed6400838afa6a99b62e276", 205 | "type": "github" 206 | }, 207 | "original": { 208 | "owner": "holochain", 209 | "repo": "holochain", 210 | "type": "github" 211 | } 212 | }, 213 | "holochain-nix-versions": { 214 | "inputs": { 215 | "holochain": "holochain_3", 216 | "lair": "lair_3", 217 | "launcher": "launcher_2", 218 | "scaffolding": "scaffolding_2" 219 | }, 220 | "locked": { 221 | "dir": "versions/0_2", 222 | "lastModified": 1684218489, 223 | "narHash": "sha256-k6FKy1k+/8qhnXWwWZcAR5F28Ip3CV+/ERoJ1xCSsCA=", 224 | "owner": "holochain", 225 | "repo": "holochain", 226 | "rev": "e6d3e965814d0bf8f4c77a2f7c4116a27446ab4a", 227 | "type": "github" 228 | }, 229 | "original": { 230 | "dir": "versions/0_2", 231 | "owner": "holochain", 232 | "repo": "holochain", 233 | "type": "github" 234 | } 235 | }, 236 | "holochain_2": { 237 | "flake": false, 238 | "locked": { 239 | "lastModified": 1681507583, 240 | "narHash": "sha256-lRnums2gv1oXVwo4gMF2QAnzEu8prwxg1uKjUzNwJV4=", 241 | "owner": "holochain", 242 | "repo": "holochain", 243 | "rev": "ac50baed6b53e9d0552729e69e1e20312e4edc08", 244 | "type": "github" 245 | }, 246 | "original": { 247 | "owner": "holochain", 248 | "ref": "holochain-0.1.4", 249 | "repo": "holochain", 250 | "type": "github" 251 | } 252 | }, 253 | "holochain_3": { 254 | "flake": false, 255 | "locked": { 256 | "lastModified": 1684139928, 257 | "narHash": "sha256-uno5MTiBwf9RiEiX6iKzJsB+3srJFKwV/1ReXzaZVVw=", 258 | "owner": "holochain", 259 | "repo": "holochain", 260 | "rev": "a91b262e87653f5f2e3a50c06eaac2bb81fb88d3", 261 | "type": "github" 262 | }, 263 | "original": { 264 | "owner": "holochain", 265 | "ref": "holochain-0.2.1-beta-dev.0", 266 | "repo": "holochain", 267 | "type": "github" 268 | } 269 | }, 270 | "lair": { 271 | "flake": false, 272 | "locked": { 273 | "lastModified": 1691746070, 274 | "narHash": "sha256-CHsTI4yIlkfnYWx2sNgzAoDBvKTLIChybzyJNbB1sMU=", 275 | "owner": "holochain", 276 | "repo": "lair", 277 | "rev": "6ab41b60744515f1760669db6fc5272298a5f324", 278 | "type": "github" 279 | }, 280 | "original": { 281 | "owner": "holochain", 282 | "ref": "lair_keystore-v0.3.0", 283 | "repo": "lair", 284 | "type": "github" 285 | } 286 | }, 287 | "lair_2": { 288 | "flake": false, 289 | "locked": { 290 | "lastModified": 1670953460, 291 | "narHash": "sha256-cqOr7iWzsNeomYQiiFggzG5Dr4X0ysnTkjtA8iwDLAQ=", 292 | "owner": "holochain", 293 | "repo": "lair", 294 | "rev": "cbfbefefe43073904a914c8181a450209a74167b", 295 | "type": "github" 296 | }, 297 | "original": { 298 | "owner": "holochain", 299 | "ref": "lair_keystore-v0.2.3", 300 | "repo": "lair", 301 | "type": "github" 302 | } 303 | }, 304 | "lair_3": { 305 | "flake": false, 306 | "locked": { 307 | "lastModified": 1682356264, 308 | "narHash": "sha256-5ZYJ1gyyL3hLR8hCjcN5yremg8cSV6w1iKCOrpJvCmc=", 309 | "owner": "holochain", 310 | "repo": "lair", 311 | "rev": "43be404da0fd9d57bf4429c44def405bd6490f61", 312 | "type": "github" 313 | }, 314 | "original": { 315 | "owner": "holochain", 316 | "ref": "lair_keystore-v0.2.4", 317 | "repo": "lair", 318 | "type": "github" 319 | } 320 | }, 321 | "launcher": { 322 | "flake": false, 323 | "locked": { 324 | "lastModified": 1677270906, 325 | "narHash": "sha256-/xT//6nqhjpKLMMv41JE0W3H5sE9jKMr8Dedr88D4N8=", 326 | "owner": "holochain", 327 | "repo": "launcher", 328 | "rev": "1ad188a43900c139e52df10a21e3722f41dfb967", 329 | "type": "github" 330 | }, 331 | "original": { 332 | "owner": "holochain", 333 | "ref": "holochain-0.1", 334 | "repo": "launcher", 335 | "type": "github" 336 | } 337 | }, 338 | "launcher_2": { 339 | "flake": false, 340 | "locked": { 341 | "lastModified": 1683619203, 342 | "narHash": "sha256-Nyfn+Bt5mJh14d2Y3N3RcYelfZwW3mkvvJFtTkCNEX0=", 343 | "owner": "holochain", 344 | "repo": "launcher", 345 | "rev": "b2f3658a4412dc00a8739d3b51f95a518d109432", 346 | "type": "github" 347 | }, 348 | "original": { 349 | "owner": "holochain", 350 | "ref": "holochain-0.2", 351 | "repo": "launcher", 352 | "type": "github" 353 | } 354 | }, 355 | "nix-filter": { 356 | "locked": { 357 | "lastModified": 1675361037, 358 | "narHash": "sha256-CTbDuDxFD3U3g/dWUB+r+B/snIe+qqP1R+1myuFGe2I=", 359 | "owner": "numtide", 360 | "repo": "nix-filter", 361 | "rev": "e1b2f96c2a31415f362268bc48c3fccf47dff6eb", 362 | "type": "github" 363 | }, 364 | "original": { 365 | "owner": "numtide", 366 | "repo": "nix-filter", 367 | "type": "github" 368 | } 369 | }, 370 | "nixpkgs": { 371 | "locked": { 372 | "lastModified": 1679262748, 373 | "narHash": "sha256-DQCrrAFrkxijC6haUzOC5ZoFqpcv/tg2WxnyW3np1Cc=", 374 | "owner": "NixOS", 375 | "repo": "nixpkgs", 376 | "rev": "60c1d71f2ba4c80178ec84523c2ca0801522e0a6", 377 | "type": "github" 378 | }, 379 | "original": { 380 | "id": "nixpkgs", 381 | "ref": "nixos-unstable", 382 | "type": "indirect" 383 | } 384 | }, 385 | "nixpkgs-lib": { 386 | "locked": { 387 | "dir": "lib", 388 | "lastModified": 1675183161, 389 | "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", 390 | "owner": "NixOS", 391 | "repo": "nixpkgs", 392 | "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", 393 | "type": "github" 394 | }, 395 | "original": { 396 | "dir": "lib", 397 | "owner": "NixOS", 398 | "ref": "nixos-unstable", 399 | "repo": "nixpkgs", 400 | "type": "github" 401 | } 402 | }, 403 | "pre-commit-hooks-nix": { 404 | "flake": false, 405 | "locked": { 406 | "lastModified": 1676513100, 407 | "narHash": "sha256-MK39nQV86L2ag4TmcK5/+r1ULpzRLPbbfvWbPvIoYJE=", 408 | "owner": "cachix", 409 | "repo": "pre-commit-hooks.nix", 410 | "rev": "5f0cba88ac4d6dd8cad5c6f6f1540b3d6a21a798", 411 | "type": "github" 412 | }, 413 | "original": { 414 | "owner": "cachix", 415 | "repo": "pre-commit-hooks.nix", 416 | "type": "github" 417 | } 418 | }, 419 | "root": { 420 | "inputs": { 421 | "flake-parts": [ 422 | "holochain-flake", 423 | "flake-parts" 424 | ], 425 | "holochain-flake": "holochain-flake", 426 | "holochain-nix-versions": "holochain-nix-versions", 427 | "nixpkgs": [ 428 | "holochain-flake", 429 | "nixpkgs" 430 | ] 431 | } 432 | }, 433 | "rust-overlay": { 434 | "inputs": { 435 | "flake-utils": [ 436 | "holochain-flake", 437 | "crane", 438 | "flake-utils" 439 | ], 440 | "nixpkgs": [ 441 | "holochain-flake", 442 | "crane", 443 | "nixpkgs" 444 | ] 445 | }, 446 | "locked": { 447 | "lastModified": 1675391458, 448 | "narHash": "sha256-ukDKZw922BnK5ohL9LhwtaDAdCsJL7L6ScNEyF1lO9w=", 449 | "owner": "oxalica", 450 | "repo": "rust-overlay", 451 | "rev": "383a4acfd11d778d5c2efcf28376cbd845eeaedf", 452 | "type": "github" 453 | }, 454 | "original": { 455 | "owner": "oxalica", 456 | "repo": "rust-overlay", 457 | "type": "github" 458 | } 459 | }, 460 | "rust-overlay_2": { 461 | "inputs": { 462 | "flake-utils": "flake-utils_2", 463 | "nixpkgs": [ 464 | "holochain-flake", 465 | "nixpkgs" 466 | ] 467 | }, 468 | "locked": { 469 | "lastModified": 1679451618, 470 | "narHash": "sha256-gWFYRgmeT+8xDYHK4HSuCY9Pi7mSxC+2illHrmDkG7A=", 471 | "owner": "oxalica", 472 | "repo": "rust-overlay", 473 | "rev": "a89d328ca7d106c3fdbbd072b6c7088ab5b798a3", 474 | "type": "github" 475 | }, 476 | "original": { 477 | "owner": "oxalica", 478 | "repo": "rust-overlay", 479 | "type": "github" 480 | } 481 | }, 482 | "scaffolding": { 483 | "flake": false, 484 | "locked": { 485 | "lastModified": 1677514461, 486 | "narHash": "sha256-xflYnH6whXRqXFAqY2MHVXTWWcesn9OzZuyNhdXjsgo=", 487 | "owner": "holochain", 488 | "repo": "scaffolding", 489 | "rev": "c245d306110f3a5408f1dbe15d6a3725884ef3f4", 490 | "type": "github" 491 | }, 492 | "original": { 493 | "owner": "holochain", 494 | "ref": "holochain-0.1", 495 | "repo": "scaffolding", 496 | "type": "github" 497 | } 498 | }, 499 | "scaffolding_2": { 500 | "flake": false, 501 | "locked": { 502 | "lastModified": 1683890859, 503 | "narHash": "sha256-/nG2TGU4Q7zy0KGS/opcW1836LZ7FJhA+/OEh5gNj34=", 504 | "owner": "holochain", 505 | "repo": "scaffolding", 506 | "rev": "1ca1092ad5d147bd23a75444874830cc033aa9cf", 507 | "type": "github" 508 | }, 509 | "original": { 510 | "owner": "holochain", 511 | "ref": "holochain-0.2", 512 | "repo": "scaffolding", 513 | "type": "github" 514 | } 515 | }, 516 | "versions": { 517 | "inputs": { 518 | "holochain": "holochain_2", 519 | "lair": "lair_2", 520 | "launcher": "launcher", 521 | "scaffolding": "scaffolding" 522 | }, 523 | "locked": { 524 | "dir": "versions/0_1", 525 | "lastModified": 1684163217, 526 | "narHash": "sha256-haXBmyqyufhckmsVd9BnquhhlbmJF7vKrN9u7SmPeYA=", 527 | "owner": "holochain", 528 | "repo": "holochain", 529 | "rev": "8d9d5837a46599fb165b08cdeb8ecd0ed023de07", 530 | "type": "github" 531 | }, 532 | "original": { 533 | "dir": "versions/0_1", 534 | "owner": "holochain", 535 | "repo": "holochain", 536 | "type": "github" 537 | } 538 | } 539 | }, 540 | "root": "root", 541 | "version": 7 542 | } 543 | -------------------------------------------------------------------------------- /static/popper-v2/popper-v2.9.2.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @popperjs/core v2.9.2 - MIT License 3 | */ 4 | 5 | "use strict";!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){function t(e){return{width:(e=e.getBoundingClientRect()).width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function n(e){return null==e?window:"[object Window]"!==e.toString()?(e=e.ownerDocument)&&e.defaultView||window:e}function o(e){return{scrollLeft:(e=n(e)).pageXOffset,scrollTop:e.pageYOffset}}function r(e){return e instanceof n(e).Element||e instanceof Element}function i(e){return e instanceof n(e).HTMLElement||e instanceof HTMLElement}function a(e){return"undefined"!=typeof ShadowRoot&&(e instanceof n(e).ShadowRoot||e instanceof ShadowRoot)}function s(e){return e?(e.nodeName||"").toLowerCase():null}function f(e){return((r(e)?e.ownerDocument:e.document)||window.document).documentElement}function p(e){return t(f(e)).left+o(e).scrollLeft}function c(e){return n(e).getComputedStyle(e)}function l(e){return e=c(e),/auto|scroll|overlay|hidden/.test(e.overflow+e.overflowY+e.overflowX)}function u(e,r,a){void 0===a&&(a=!1);var c=f(r);e=t(e);var u=i(r),d={scrollLeft:0,scrollTop:0},m={x:0,y:0};return(u||!u&&!a)&&(("body"!==s(r)||l(c))&&(d=r!==n(r)&&i(r)?{scrollLeft:r.scrollLeft,scrollTop:r.scrollTop}:o(r)),i(r)?((m=t(r)).x+=r.clientLeft,m.y+=r.clientTop):c&&(m.x=p(c))),{x:e.left+d.scrollLeft-m.x,y:e.top+d.scrollTop-m.y,width:e.width,height:e.height}}function d(e){var n=t(e),o=e.offsetWidth,r=e.offsetHeight;return 1>=Math.abs(n.width-o)&&(o=n.width),1>=Math.abs(n.height-r)&&(r=n.height),{x:e.offsetLeft,y:e.offsetTop,width:o,height:r}}function m(e){return"html"===s(e)?e:e.assignedSlot||e.parentNode||(a(e)?e.host:null)||f(e)}function h(e){return 0<=["html","body","#document"].indexOf(s(e))?e.ownerDocument.body:i(e)&&l(e)?e:h(m(e))}function v(e,t){var o;void 0===t&&(t=[]);var r=h(e);return e=r===(null==(o=e.ownerDocument)?void 0:o.body),o=n(r),r=e?[o].concat(o.visualViewport||[],l(r)?r:[]):r,t=t.concat(r),e?t:t.concat(v(m(r)))}function g(e){return i(e)&&"fixed"!==c(e).position?e.offsetParent:null}function y(e){for(var t=n(e),o=g(e);o&&0<=["table","td","th"].indexOf(s(o))&&"static"===c(o).position;)o=g(o);if(o&&("html"===s(o)||"body"===s(o)&&"static"===c(o).position))return t;if(!o)e:{if(o=-1!==navigator.userAgent.toLowerCase().indexOf("firefox"),-1===navigator.userAgent.indexOf("Trident")||!i(e)||"fixed"!==c(e).position)for(e=m(e);i(e)&&0>["html","body"].indexOf(s(e));){var r=c(e);if("none"!==r.transform||"none"!==r.perspective||"paint"===r.contain||-1!==["transform","perspective"].indexOf(r.willChange)||o&&"filter"===r.willChange||o&&r.filter&&"none"!==r.filter){o=e;break e}e=e.parentNode}o=null}return o||t}function b(e){function t(e){o.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){o.has(e)||(e=n.get(e))&&t(e)})),r.push(e)}var n=new Map,o=new Set,r=[];return e.forEach((function(e){n.set(e.name,e)})),e.forEach((function(e){o.has(e.name)||t(e)})),r}function w(e){var t;return function(){return t||(t=new Promise((function(n){Promise.resolve().then((function(){t=void 0,n(e())}))}))),t}}function x(e){return e.split("-")[0]}function O(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&a(n))do{if(t&&e.isSameNode(t))return!0;t=t.parentNode||t.host}while(t);return!1}function j(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function E(e,r){if("viewport"===r){r=n(e);var a=f(e);r=r.visualViewport;var s=a.clientWidth;a=a.clientHeight;var l=0,u=0;r&&(s=r.width,a=r.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(l=r.offsetLeft,u=r.offsetTop)),e=j(e={width:s,height:a,x:l+p(e),y:u})}else i(r)?((e=t(r)).top+=r.clientTop,e.left+=r.clientLeft,e.bottom=e.top+r.clientHeight,e.right=e.left+r.clientWidth,e.width=r.clientWidth,e.height=r.clientHeight,e.x=e.left,e.y=e.top):(u=f(e),e=f(u),s=o(u),r=null==(a=u.ownerDocument)?void 0:a.body,a=_(e.scrollWidth,e.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),l=_(e.scrollHeight,e.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),u=-s.scrollLeft+p(u),s=-s.scrollTop,"rtl"===c(r||e).direction&&(u+=_(e.clientWidth,r?r.clientWidth:0)-a),e=j({width:a,height:l,x:u,y:s}));return e}function D(e,t,n){return t="clippingParents"===t?function(e){var t=v(m(e)),n=0<=["absolute","fixed"].indexOf(c(e).position)&&i(e)?y(e):e;return r(n)?t.filter((function(e){return r(e)&&O(e,n)&&"body"!==s(e)})):[]}(e):[].concat(t),(n=(n=[].concat(t,[n])).reduce((function(t,n){return n=E(e,n),t.top=_(n.top,t.top),t.right=U(n.right,t.right),t.bottom=U(n.bottom,t.bottom),t.left=_(n.left,t.left),t}),E(e,n[0]))).width=n.right-n.left,n.height=n.bottom-n.top,n.x=n.left,n.y=n.top,n}function L(e){return 0<=["top","bottom"].indexOf(e)?"x":"y"}function P(e){var t=e.reference,n=e.element,o=(e=e.placement)?x(e):null;e=e?e.split("-")[1]:null;var r=t.x+t.width/2-n.width/2,i=t.y+t.height/2-n.height/2;switch(o){case"top":r={x:r,y:t.y-n.height};break;case"bottom":r={x:r,y:t.y+t.height};break;case"right":r={x:t.x+t.width,y:i};break;case"left":r={x:t.x-n.width,y:i};break;default:r={x:t.x,y:t.y}}if(null!=(o=o?L(o):null))switch(i="y"===o?"height":"width",e){case"start":r[o]-=t[i]/2-n[i]/2;break;case"end":r[o]+=t[i]/2-n[i]/2}return r}function M(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function k(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function A(e,n){void 0===n&&(n={});var o=n;n=void 0===(n=o.placement)?e.placement:n;var i=o.boundary,a=void 0===i?"clippingParents":i,s=void 0===(i=o.rootBoundary)?"viewport":i;i=void 0===(i=o.elementContext)?"popper":i;var p=o.altBoundary,c=void 0!==p&&p;o=M("number"!=typeof(o=void 0===(o=o.padding)?0:o)?o:k(o,C));var l=e.elements.reference;p=e.rects.popper,a=D(r(c=e.elements[c?"popper"===i?"reference":"popper":i])?c:c.contextElement||f(e.elements.popper),a,s),c=P({reference:s=t(l),element:p,strategy:"absolute",placement:n}),p=j(Object.assign({},p,c)),s="popper"===i?p:s;var u={top:a.top-s.top+o.top,bottom:s.bottom-a.bottom+o.bottom,left:a.left-s.left+o.left,right:s.right-a.right+o.right};if(e=e.modifiersData.offset,"popper"===i&&e){var d=e[n];Object.keys(u).forEach((function(e){var t=0<=["right","bottom"].indexOf(e)?1:-1,n=0<=["top","bottom"].indexOf(e)?"y":"x";u[e]+=d[n]*t}))}return u}function W(){for(var e=arguments.length,t=Array(e),n=0;n(g.devicePixelRatio||1)?"translate("+e+"px, "+u+"px)":"translate3d("+e+"px, "+u+"px, 0)",m)):Object.assign({},o,((t={})[v]=a?u+"px":"",t[h]=d?e+"px":"",t.transform="",t))}function H(e){return e.replace(/left|right|bottom|top/g,(function(e){return $[e]}))}function R(e){return e.replace(/start|end/g,(function(e){return ee[e]}))}function S(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function q(e){return["top","right","bottom","left"].some((function(t){return 0<=e[t]}))}var C=["top","bottom","right","left"],N=C.reduce((function(e,t){return e.concat([t+"-start",t+"-end"])}),[]),V=[].concat(C,["auto"]).reduce((function(e,t){return e.concat([t,t+"-start",t+"-end"])}),[]),I="beforeRead read afterRead beforeMain main afterMain beforeWrite write afterWrite".split(" "),_=Math.max,U=Math.min,z=Math.round,F={placement:"bottom",modifiers:[],strategy:"absolute"},X={passive:!0},Y={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(e){var t=e.state,o=e.instance,r=(e=e.options).scroll,i=void 0===r||r,a=void 0===(e=e.resize)||e,s=n(t.elements.popper),f=[].concat(t.scrollParents.reference,t.scrollParents.popper);return i&&f.forEach((function(e){e.addEventListener("scroll",o.update,X)})),a&&s.addEventListener("resize",o.update,X),function(){i&&f.forEach((function(e){e.removeEventListener("scroll",o.update,X)})),a&&s.removeEventListener("resize",o.update,X)}},data:{}},G={name:"popperOffsets",enabled:!0,phase:"read",fn:function(e){var t=e.state;t.modifiersData[e.name]=P({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})},data:{}},J={top:"auto",right:"auto",bottom:"auto",left:"auto"},K={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(e){var t=e.state,n=e.options;e=void 0===(e=n.gpuAcceleration)||e;var o=n.adaptive;o=void 0===o||o,n=void 0===(n=n.roundOffsets)||n,e={placement:x(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:e},null!=t.modifiersData.popperOffsets&&(t.styles.popper=Object.assign({},t.styles.popper,T(Object.assign({},e,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:o,roundOffsets:n})))),null!=t.modifiersData.arrow&&(t.styles.arrow=Object.assign({},t.styles.arrow,T(Object.assign({},e,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:n})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})},data:{}},Q={name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var t=e.state;Object.keys(t.elements).forEach((function(e){var n=t.styles[e]||{},o=t.attributes[e]||{},r=t.elements[e];i(r)&&s(r)&&(Object.assign(r.style,n),Object.keys(o).forEach((function(e){var t=o[e];!1===t?r.removeAttribute(e):r.setAttribute(e,!0===t?"":t)})))}))},effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach((function(e){var o=t.elements[e],r=t.attributes[e]||{};e=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]).reduce((function(e,t){return e[t]="",e}),{}),i(o)&&s(o)&&(Object.assign(o.style,e),Object.keys(r).forEach((function(e){o.removeAttribute(e)})))}))}},requires:["computeStyles"]},Z={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var t=e.state,n=e.name,o=void 0===(e=e.options.offset)?[0,0]:e,r=(e=V.reduce((function(e,n){var r=t.rects,i=x(n),a=0<=["left","top"].indexOf(i)?-1:1,s="function"==typeof o?o(Object.assign({},r,{placement:n})):o;return r=(r=s[0])||0,s=((s=s[1])||0)*a,i=0<=["left","right"].indexOf(i)?{x:s,y:r}:{x:r,y:s},e[n]=i,e}),{}))[t.placement],i=r.x;r=r.y,null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=i,t.modifiersData.popperOffsets.y+=r),t.modifiersData[n]=e}},$={left:"right",right:"left",bottom:"top",top:"bottom"},ee={start:"end",end:"start"},te={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options;if(e=e.name,!t.modifiersData[e]._skip){var o=n.mainAxis;o=void 0===o||o;var r=n.altAxis;r=void 0===r||r;var i=n.fallbackPlacements,a=n.padding,s=n.boundary,f=n.rootBoundary,p=n.altBoundary,c=n.flipVariations,l=void 0===c||c,u=n.allowedAutoPlacements;c=x(n=t.options.placement),i=i||(c!==n&&l?function(e){if("auto"===x(e))return[];var t=H(e);return[R(e),t,R(t)]}(n):[H(n)]);var d=[n].concat(i).reduce((function(e,n){return e.concat("auto"===x(n)?function(e,t){void 0===t&&(t={});var n=t.boundary,o=t.rootBoundary,r=t.padding,i=t.flipVariations,a=t.allowedAutoPlacements,s=void 0===a?V:a,f=t.placement.split("-")[1];0===(i=(t=f?i?N:N.filter((function(e){return e.split("-")[1]===f})):C).filter((function(e){return 0<=s.indexOf(e)}))).length&&(i=t);var p=i.reduce((function(t,i){return t[i]=A(e,{placement:i,boundary:n,rootBoundary:o,padding:r})[x(i)],t}),{});return Object.keys(p).sort((function(e,t){return p[e]-p[t]}))}(t,{placement:n,boundary:s,rootBoundary:f,padding:a,flipVariations:l,allowedAutoPlacements:u}):n)}),[]);n=t.rects.reference,i=t.rects.popper;var m=new Map;c=!0;for(var h=d[0],v=0;vi[O]&&(b=H(b)),O=H(b),w=[],o&&w.push(0>=j[y]),r&&w.push(0>=j[b],0>=j[O]),w.every((function(e){return e}))){h=g,c=!1;break}m.set(g,w)}if(c)for(o=function(e){var t=d.find((function(t){if(t=m.get(t))return t.slice(0,e).every((function(e){return e}))}));if(t)return h=t,"break"},r=l?3:1;0