├── .babelrc ├── .eslintignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── Documentation ├── VisualLanguage.html └── conventions.md ├── LICENSE.md ├── README.md ├── dev ├── index.html ├── loader.ts └── pane │ └── index.ts ├── doc └── images │ ├── panes-for-classes.epgz │ └── panes-for-classes.svg ├── eslint.config.mjs ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── src ├── RDFXMLPane.js ├── argument │ ├── argumentPane.js │ ├── argument_icon_v04.jpg │ ├── icon_argument.png │ └── transparentyingyang.png ├── attach │ ├── attachPane.js │ ├── tbl-paperclip-128.png │ ├── tbl-paperclip-22.png │ └── tbl-paperclip-22a.png ├── audio │ └── audioPane.js ├── chatPreferencesForm.ttl ├── classInstancePane.js ├── dashboard │ ├── basicPreferences.ts │ ├── dashboardPane.ts │ ├── homepage.ts │ ├── languages │ │ ├── codes.html │ │ ├── codes.xml │ │ ├── codes2.txt │ │ ├── foo │ │ ├── foo.ttl │ │ └── get-language-names.sh │ ├── ontologyData.ttl │ └── preferencesFormText.ttl ├── dataContentPane.js ├── defaultPane.js ├── dokieli │ ├── Makefile │ ├── dokieliPane.js │ ├── new.html │ └── new.js ├── form │ ├── form-22.png │ ├── form-b-22.png │ ├── form.graffle │ ├── form.png │ ├── pane.js │ └── psuedocode-notes.txt ├── global.d.ts ├── home │ └── homePane.ts ├── humanReadablePane.js ├── imagePane.js ├── index.ts ├── internal │ └── internalPane.ts ├── mainPage │ ├── footer.ts │ ├── header.ts │ └── index.ts ├── meeting │ ├── Makefile │ └── test │ │ └── meeting1 │ │ ├── Actions │ │ ├── actions.ttl │ │ ├── config.ttl │ │ └── state.ttl │ │ ├── Schedule │ │ ├── details.ttl │ │ ├── details.ttl.acl │ │ ├── forms.ttl │ │ ├── forms.ttl.acl │ │ ├── index.html │ │ ├── index.html.acl │ │ ├── results.ttl │ │ └── results.ttl.acl │ │ ├── SharedNotes │ │ └── pad.ttl │ │ ├── chat │ │ └── chat.ttl │ │ ├── details.ttl │ │ └── pad │ │ └── pad.ttl ├── microblogPane │ ├── mbStyle.css │ └── microblogPane.js ├── n3Pane.js ├── outline │ ├── context.ts │ ├── manager.js │ ├── manager.test.ts │ ├── outlineIcons.js │ ├── propertyViews.test.ts │ ├── propertyViews.ts │ ├── queryByExample.js │ ├── userInput.js │ ├── viewAsImage.ts │ └── viewAsMbox.ts ├── pad │ ├── images │ │ ├── ColourOff.ai │ │ ├── ColourOff.png │ │ ├── ColourOn.ai │ │ └── ColourOn.png │ └── padPane.ts ├── playlist │ └── playlistPane.js ├── registerPanes.js ├── schedule │ ├── Makefile │ ├── formsForSchedule.js │ ├── formsForSchedule.ttl │ └── schedulePane.js ├── sharing │ └── sharingPane.ts ├── slideshow │ └── slideshowPane.js ├── socialPane.js ├── style │ └── tabbedtab.css ├── tabbed │ └── tabbedPane.ts ├── tableViewPane.js ├── test-import-export │ ├── common.js │ ├── edit-importer.js │ └── testImportExport.js ├── transaction │ ├── 068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png │ ├── 075988-3d-transparent-glass-icon-business-currency-british-pound-sc35.png │ ├── 22-pixel-068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png │ ├── pane.js │ ├── period.js │ ├── thumbs_075987-3d-transparent-glass-icon-business-creditcard2.png │ └── thumbs_075989-3d-transparent-glass-icon-business-currency-cent-sc35.png ├── trip │ └── tripPane.js ├── trustedApplications │ ├── __snapshots__ │ │ └── trustedApplications.test.ts.snap │ ├── trustedApplications.dom.ts │ ├── trustedApplications.test.ts │ ├── trustedApplications.utils.ts │ └── trustedApplications.view.ts ├── types.ts ├── ui │ ├── 22-builder.png │ ├── builder.graffle │ ├── builder.png │ ├── builder2.png │ └── pane.js └── video │ └── videoPane.js ├── timestamp.sh ├── travis └── bumpversion.js ├── tsconfig.json ├── typings ├── raw-loader.d.ts └── solid-namespace │ └── index.d.ts └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ], 6 | "plugins": [ 7 | [ 8 | "babel-plugin-inline-import", 9 | { 10 | "extensions": [ 11 | ".ttl" 12 | ] 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | typings/rdflib 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - "**" 10 | pull_request: 11 | branches: 12 | - "**" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: 23 | - 18.x 24 | - 20.x 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | - run: npm ci 33 | - run: npm run lint --if-present 34 | - run: npm test 35 | - run: npm run build --if-present 36 | - name: Save build 37 | if: matrix.node-version == '18.x' 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: build 41 | path: | 42 | . 43 | !node_modules 44 | retention-days: 1 45 | 46 | npm-publish-build: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/download-artifact@v4 51 | with: 52 | name: build 53 | - uses: actions/setup-node@v4 54 | with: 55 | node-version: 18.x 56 | - uses: rlespinasse/github-slug-action@v3.x 57 | - name: Append commit hash to package version 58 | run: 'sed -i -E "s/(\"version\": *\"[^\"]+)/\1-${GITHUB_SHA_SHORT}/" package.json' 59 | - name: Disable pre- and post-publish actions 60 | run: 'sed -i -E "s/\"((pre|post)publish)/\"ignore:\1/" package.json' 61 | - uses: JS-DevTools/npm-publish@v1 62 | with: 63 | token: ${{ secrets.NPM_TOKEN }} 64 | tag: ${{ env.GITHUB_REF_SLUG }} 65 | 66 | npm-publish-latest: 67 | needs: build 68 | runs-on: ubuntu-latest 69 | if: github.ref == 'refs/heads/main' 70 | steps: 71 | - uses: actions/download-artifact@v4 72 | with: 73 | name: build 74 | - uses: actions/setup-node@v1 75 | with: 76 | node-version: 18.x 77 | - name: Disable pre- and post-publish actions 78 | run: 'sed -i -E "s/\"((pre|post)publish)/\"ignore:\1/" package.json' 79 | - uses: JS-DevTools/npm-publish@v1 80 | with: 81 | token: ${{ secrets.NPM_TOKEN }} 82 | tag: latest 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built code 2 | dist 3 | lib 4 | src/versionInfo.ts 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | node_modules 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # files used by IDEs such as WebStorm 41 | .idea 42 | 43 | # history files in Visual Code 44 | .history 45 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This file primarily exists to prevent ./dist from being ignored when publishing. 2 | # (Because it's listed in .gitignore). 3 | 4 | coverage -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.19.0 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solid-panes 2 | 3 | A set of core solid-compatible applets based on solid-ui 4 | 5 | These are a set of interlinked applications, or parts of applications, 6 | which called 'panes' -- as in parts of a window. A pane displays a data object of certain class using part of the window. 7 | They don't tile like window panes necessarily, but one pane can involve other panes to display 8 | objects related to the main object, in all kinds of creative ways. You can give the sub-pane a bit of 9 | HTML DOM element to work in, and the data object, and it does the rest. 10 | 11 | You can explicitly invoke a specific sub-pane, or you can just provide a DOM element to contain it, 12 | and ask the pane system to pick the appropriate pane. It does this by calling each potential pane in order 13 | with the object, and asking whether it wants to render that object. Typically the pane chosen is the most specific pane, 14 | so typically a hand-written user interface will be chosen over a generic machine-generated one. 15 | 16 | These panes are used in the Data Browser - see mashlib 17 | [https://github.com/linkeddata/mashlib](https://github.com/linkeddata/mashlib) 18 | 19 | Currently the panes available include: 20 | 21 | - A default pane which lists the properties of any object 22 | - An internals pane which allows the URI and the HTTP fetch history to be checked 23 | - A pane for Address Books, Groups as well as individual Contacts 24 | - A pane for seeing pictures in a slideshow 25 | - A pane for a playlist of YouTube videos 26 | - A pane for a range of issue trackers, to-do-lists, agendas, etc 27 | - A file and directory manager for a Solid/LDP hierarchical file store 28 | - A Sharing pane to control the Access Control Lists for any object or folder 29 | - and so on 30 | 31 | The solid-app-set panes are built using a set of widgets and utilities in 32 | [https://github.com/linkeddata/solid-ui](https://github.com/linkeddata/solid-ui) 33 | 34 | To help onboarding, we're using [roles](https://github.com/solidos/userguide/#role) to limit the number of panes presented 35 | to new users. 36 | 37 | ## Goals 38 | 39 | - Make the system module in terms of NPM modules; one for each pane 40 | 41 | - Allow (signed?) panes to be advertised in turtle data in the web, and loaded automatically 42 | 43 | - Allow a Solid user to save preferences for which panes are used for which data types. 44 | 45 | - Create new panes for playlist and photo management and sharing, fitness, etc 46 | 47 | Volunteers are always welcome! 48 | 49 | ## Documentation 50 | - [Visual Language](https://solidos.github.io/solid-panes/Documentation/VisualLanguage.html) 51 | - [Conventions](./Documentation/conventions.md) 52 | 53 | ## Development 54 | To get started, make sure you have Node.js installed (for instance 55 | through https://github.com/nvm-sh/nvm), and: 56 | 1. run 57 | ```sh 58 | git clone https://github.com/solidos/solid-panes 59 | cd solid-panes 60 | npm install 61 | npm run start 62 | ``` 63 | 2. a browser window should automatically open at http://localhost:9000, if for some reason it doesn't go ahead and manually navigate there. 64 | 3. Once you arrive at the Solid Pane Tester page, the `profile-pane` will be loaded by default. Proceed to edit `solid-panes/dev/loader.ts` and, at line 5, you should see `import Pane from 'profile-pane'`. Simply change `'profile-pane'` to your preferred pane/directory containing the pane of choice; for example, you could choose `'source-pane'` and bam, it will load that pane. For those who are new, you can go to the `solid-panes/src` directory and manually navigate through each individual folder. In most folders, you simply look for any file that has `pane` in the title. Copy and paste the `pane.js` file of your choice into the `solid-panes/dev/pane` folder, or you can import directly from the `src` directory. For example, importing from `'../src/dokieli/dokieliPane'` will work just fine. Each time you save `solid-panes/dev/loader.ts` while importing a different pane, your browser at `http://localhost:9000/` should automatically refresh. It's a good idea to keep the developer console open in your web browser to ensure panes are loading and rendering properly. 65 | 66 | 4. Another tip: to ensure you arrive at the proper destination, look at lines 48–53 in `solid-panes/dev/loader.ts`. You should see an event listener that is ready for a string. `renderPane('https://solidos.solidcommunity.net/Team/SolidOs%20team%20chat/index.ttl#this')` will be the default. Depending on the `pane.js` that you chose in the earlier import statements, the `renderPane` function determines the way you will see DOMContent inside of that particular pane. If you have created an `index.html` in your provider pod storage area, you could use `'https://yoursolidname.solidcommunity.net/profile/index.html'` inside of the `renderPane()` function parameters. You can edit the string manually in `solid-panes/dev/loader.ts`, or you can go to your developer console and type `renderPane('https://yoursolidname.solidcommunity.net/profile/index.html')` — just point to a part of your account that is congruent to the pane that you wish to import! :) 67 | 68 | ## Contributing panes 69 | When you created a pane, you can either add it as an npm dependency 70 | of this repo (for instance meeting-pane, issue-pane, chat-pane are all 71 | imported in src/registerPanes.js), or add it under the src/ tree of this repo. 72 | 73 | 74 | ## Eg. some RDF CLasses 75 | 76 | Here, just to show how it works, are how some RDF Classes map onto panes. Anything to do with 77 | contacts (A VCARD Address Book, Group, Individual, Organization) can be handled by the one contact 78 | pane. Any other pane which wants to deal with contacts can just use the pane within its own user interface. 79 | 80 | ![Mapping many classes on the L to panes on the R](https://solidos.github.io/solid-panes/doc/images/panes-for-classes.svg) 81 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Pane Tester 6 | 17 | 18 | 19 |

Solid Pane Tester

20 |
21 |

22 | A handy tool for pane developers. Put your JS or TS file in dev/pane/. 23 | Run renderPane('https://solidos.solidcommunity.net/profile/card#me') from the console. 24 | Don't forget that the resource owner needs to add http://localhost:9000 as a trusted app. 25 |

26 |
HTML element from pane.render will be inserted here ...
27 | 28 | 29 | -------------------------------------------------------------------------------- /dev/loader.ts: -------------------------------------------------------------------------------- 1 | import * as paneRegistry from 'pane-registry' 2 | import * as $rdf from 'rdflib' 3 | import { solidLogicSingleton, store, authSession } from 'solid-logic' 4 | import { getOutliner } from '../src' 5 | import Pane from 'profile-pane' 6 | 7 | // FIXME: 8 | window.$rdf = $rdf 9 | 10 | async function renderPane (uri: string) { 11 | if (!uri) { 12 | console.log("usage renderPane('http://example.com/#this')", uri) 13 | return 14 | } 15 | const subject = $rdf.sym(uri) 16 | const doc = subject.doc() 17 | 18 | await new Promise((resolve, reject) => { 19 | store.fetcher.load(doc).then(resolve, reject) 20 | }) 21 | const context = { 22 | // see https://github.com/solidos/solid-panes/blob/005f90295d83e499fd626bd84aeb3df10135d5c1/src/index.ts#L30-L34 23 | dom: document, 24 | getOutliner, 25 | session: { 26 | store: store, 27 | paneRegistry, 28 | logic: solidLogicSingleton 29 | } 30 | } 31 | const options = {} 32 | console.log(subject, Pane) 33 | const icon = createIconElement(Pane) 34 | const paneDiv = Pane.render(subject, context, options) 35 | const target = document.getElementById('render') 36 | target.innerHTML = '' 37 | target.appendChild(icon) 38 | target.appendChild(paneDiv) 39 | } 40 | 41 | function createIconElement (Pane) { 42 | const icon = Pane.icon 43 | const img = document.createElement('img') 44 | img.src = icon 45 | img.width = 40 46 | return img 47 | } 48 | 49 | document.addEventListener('DOMContentLoaded', () => { 50 | renderPane( 51 | 'https://solidos.solidcommunity.net/Team/SolidOs%20team%20chat/index.ttl#this' 52 | ) 53 | }) 54 | 55 | window.onload = async () => { 56 | console.log('document ready') 57 | // registerPanes((cjsOrEsModule: any) => paneRegistry.register(cjsOrEsModule.default || cjsOrEsModule)) 58 | paneRegistry.register(require('contacts-pane')) 59 | await authSession.handleIncomingRedirect({ 60 | restorePreviousSession: true 61 | }) 62 | const session = await authSession 63 | if (!session.info.isLoggedIn) { 64 | console.log('The user is not logged in') 65 | document.getElementById('loginBanner').innerHTML = 66 | '' 67 | } else { 68 | console.log(`Logged in as ${session.info.webId}`) 69 | 70 | document.getElementById( 71 | 'loginBanner' 72 | ).innerHTML = `Logged in as ${session.info.webId} ` 73 | } 74 | renderPane() 75 | } 76 | window.logout = () => { 77 | authSession.logout() 78 | window.location = '' 79 | } 80 | window.login = async function () { 81 | const session = await authSession 82 | if (!session.info.isLoggedIn) { 83 | const issuer = prompt('Please enter an issuer URI', 'https://solidcommunity.net') 84 | await authSession.login({ 85 | oidcIssuer: issuer, 86 | redirectUrl: window.location.href, 87 | clientName: 'Solid Panes Dev Loader' 88 | }) 89 | } 90 | }; 91 | (window as any).renderPane = renderPane 92 | -------------------------------------------------------------------------------- /dev/pane/index.ts: -------------------------------------------------------------------------------- 1 | // import { longChatPane as Pane } from 'chat-pane' 2 | // import Pane from '../../src/profile/profile.view' 3 | import Pane from 'profile-pane' 4 | import * as UI from 'solid-ui' 5 | 6 | console.log('Loaded pane into Solid Pane Tester. Check window.Pane and window.UI') 7 | ;(window as any).Pane = Pane 8 | ;(window as any).UI = UI 9 | 10 | export default Pane 11 | -------------------------------------------------------------------------------- /doc/images/panes-for-classes.epgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/doc/images/panes-for-classes.epgz -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | 5 | export default [{ 6 | ignores: ["**/dist", "**/lib", "typings/rdflib"], 7 | }, { 8 | plugins: { 9 | "@typescript-eslint": typescriptEslint, 10 | }, 11 | 12 | languageOptions: { 13 | globals: { 14 | ...globals.browser, 15 | ...globals.node, 16 | Atomics: "readonly", 17 | SharedArrayBuffer: "readonly", 18 | }, 19 | 20 | parser: tsParser, 21 | }, 22 | files: ["src/**/*.js", "src/**/*.ts", "src/**/*.cjs", "src/**/*.mjs"], 23 | rules: { 24 | "no-unused-vars": ["warn", { 25 | argsIgnorePattern: "^_", 26 | varsIgnorePattern: "^_", 27 | }], 28 | 29 | "@typescript-eslint/no-unused-vars": ["warn", { 30 | argsIgnorePattern: "^_", 31 | varsIgnorePattern: "^_", 32 | }], 33 | }, 34 | }]; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-babel', 3 | testEnvironment: 'jsdom', 4 | testEnvironmentOptions: { 5 | customExportConditions: ['node'] 6 | }, 7 | moduleNameMapper: { 8 | '^[./a-zA-Z0-9$_-]+\\.ttl$': '/__mocks__/fileMock.js', // '\\.ttl$' 9 | }, 10 | collectCoverage: true, 11 | // For some reason Jest is not measuring coverage without the below option. 12 | // Unfortunately, despite `!(.test)`, it still measures coverage of test files as well: 13 | forceCoverageMatch: ['./src/**/*!(.test).ts'], 14 | // Since we're only measuring coverage for TypeScript (i.e. added with test infrastructure in place), 15 | // we can be fairly strict. However, if you feel that something is not fit for coverage, 16 | // mention why in a comment and mark it as ignored: 17 | // https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md 18 | coverageThreshold: { 19 | global: { 20 | branches: 10, 21 | functions: 25, 22 | lines: 20, 23 | statements: 20 24 | } 25 | }, 26 | setupFilesAfterEnv: ['./jest.setup.ts'] 27 | } 28 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import fetchMock from 'jest-fetch-mock' 3 | 4 | fetchMock.enableMocks() 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-panes", 3 | "version": "3.6.3", 4 | "description": "Solid-compatible Panes: applets and views for the mashlib and databrowser", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "npm run clean && npm run build-version && npm run build-lib && npm run build-types", 9 | "build-form": "(cd src/schedule/ && make)", 10 | "build-lib": "npm run build-form && babel src -d lib --source-maps --extensions '.ts,.js'", 11 | "build-dev": "webpack --progress --mode=development", 12 | "build-types": "tsc --emitDeclarationOnly", 13 | "build-version": "./timestamp.sh > src/versionInfo.ts && eslint 'src/versionInfo.ts' --fix", 14 | "watch": "npm run build-version && babel src -d lib --source-maps --extensions '.ts,.js' --watch", 15 | "clean": "rm -rf dist lib", 16 | "lint": "eslint 'src/**/*.js' 'src/**/*.ts'", 17 | "lint-fix": "eslint 'src/**/*.js' 'src/**/*.ts' --fix", 18 | "test": "npm run lint && jest src/**/*test*", 19 | "test-watch": "npm run lint && jest --onlyChanged --watch", 20 | "prepublishOnly": "npm test && npm run build", 21 | "postversion": "git push origin main --follow-tags", 22 | "start": "npm install && npm run build-version && npx webpack serve --open" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/solidos/solid-panes" 27 | }, 28 | "keywords": [ 29 | "solid", 30 | "decentralized", 31 | "widgets", 32 | "ui", 33 | "web", 34 | "rdf", 35 | "ldp", 36 | "linked", 37 | "panes", 38 | "app", 39 | "data" 40 | ], 41 | "author": "Tim Berners-Lee ", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/solidos/solid-panes/issues" 45 | }, 46 | "homepage": "https://github.com/solidos/solid-panes", 47 | "dependencies": { 48 | "@solid/better-simple-slideshow": "^0.1.0", 49 | "@types/jest": "^29.5.14", 50 | "activitystreams-pane": "^0.6.14", 51 | "chat-pane": "^2.4.27", 52 | "contacts-pane": "^2.6.13", 53 | "dompurify": "^3.2.4", 54 | "eslint": "^9.20.1", 55 | "folder-pane": "^2.4.28", 56 | "issue-pane": "^2.4.20", 57 | "marked": "^11.2.0", 58 | "meeting-pane": "^2.4.20", 59 | "mime-types": "^2.1.35", 60 | "profile-pane": "^1.1.2", 61 | "rdflib": "^2.2.36", 62 | "solid-namespace": "^0.5.4", 63 | "solid-ui": "^2.5.1", 64 | "source-pane": "^2.2.28" 65 | }, 66 | "devDependencies": { 67 | "@babel/cli": "^7.26.4", 68 | "@babel/core": "^7.26.7", 69 | "@babel/preset-env": "^7.26.7", 70 | "@babel/preset-typescript": "^7.26.0", 71 | "@testing-library/dom": "^9.3.4", 72 | "@testing-library/jest-dom": "^6.6.3", 73 | "@types/webpack-env": "^1.18.8", 74 | "@typescript-eslint/eslint-plugin": "^8.24.0", 75 | "@typescript-eslint/parser": "^8.24.0", 76 | "babel-loader": "^9.2.1", 77 | "babel-plugin-inline-import": "^3.0.0", 78 | "buffer": "^6.0.3", 79 | "globals": "^15.15.0", 80 | "html-webpack-plugin": "^5.6.3", 81 | "husky": "^8.0.3", 82 | "jest": "^29.7.0", 83 | "jest-environment-jsdom": "^29.7.0", 84 | "jest-fetch-mock": "^3.0.3", 85 | "lint-staged": "^15.4.3", 86 | "node-polyfill-webpack-plugin": "^2.0.1", 87 | "path-browserify": "^1.0.1", 88 | "react": "^18.3.1", 89 | "react-dom": "^18.3.1", 90 | "ts-jest": "^29.2.5", 91 | "typescript": "^5.7.3", 92 | "webpack": "^5.97.1", 93 | "webpack-cli": "^5.1.4", 94 | "webpack-dev-server": "^4.15.2" 95 | }, 96 | "husky": { 97 | "hooks": { 98 | "pre-commit": "lint-staged", 99 | "pre-push": "npm test" 100 | } 101 | }, 102 | "lint-staged": { 103 | "src/**/*.(js|ts)": [ 104 | "eslint" 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/RDFXMLPane.js: -------------------------------------------------------------------------------- 1 | /* RDF/XML content Pane 2 | ** 3 | ** This pane shows the content of a particular RDF resource 4 | ** or at least the RDF semantics we attribute to that resource, 5 | ** in generated N3 syntax. 6 | */ 7 | 8 | import * as UI from 'solid-ui' 9 | const ns = UI.ns 10 | 11 | export const RDFXMLPane = { 12 | icon: UI.icons.originalIconBase + '22-text-xml4.png', 13 | 14 | name: 'RDFXML', 15 | 16 | audience: [ns.solid('Developer')], 17 | 18 | label: function (subject, context) { 19 | const store = context.session.store 20 | if ( 21 | 'http://www.w3.org/2007/ont/link#ProtocolEvent' in 22 | store.findTypeURIs(subject) 23 | ) { 24 | return null 25 | } 26 | 27 | const n = store.statementsMatching(undefined, undefined, undefined, subject) 28 | .length 29 | if (n === 0) return null 30 | return 'As RDF/XML (' + n + ')' 31 | }, 32 | 33 | render: function (subject, context) { 34 | const myDocument = context.dom 35 | const kb = context.session.store 36 | const div = myDocument.createElement('div') 37 | div.setAttribute('class', 'RDFXMLPane') 38 | // Because of smushing etc, this will not be a copy of the original source 39 | // We could instead either fetch and re-parse the source, 40 | // or we could keep all the pre-smushed triples. 41 | const sts = kb.statementsMatching(undefined, undefined, undefined, subject) // @@ slow with current store! 42 | /* 43 | var kludge = kb.formula([]) // No features 44 | for (var i=0; i< sts.length; i++) { 45 | s = sts[i] 46 | kludge.add(s.subject, s.predicate, s.object) 47 | } 48 | */ 49 | const sz = UI.rdf.Serializer(kb) 50 | sz.suggestNamespaces(kb.namespaces) 51 | sz.setBase(subject.uri) 52 | const str = sz.statementsToXML(sts) 53 | const pre = myDocument.createElement('PRE') 54 | pre.appendChild(myDocument.createTextNode(str)) 55 | div.appendChild(pre) 56 | return div 57 | } 58 | } 59 | 60 | // ends 61 | -------------------------------------------------------------------------------- /src/argument/argumentPane.js: -------------------------------------------------------------------------------- 1 | /* View argument Pane 2 | ** 3 | ** This pane shows a position and optionally the positions which 4 | ** support or oppose it. 5 | ** @@ Unfinsihed. 6 | ** Should allow editing the data too 7 | 8 | */ 9 | import { store } from 'solid-logic' 10 | import * as UI from 'solid-ui' 11 | import * as panes from 'pane-registry' 12 | 13 | // console.log('@@@ argument pane icon at ' + (module.__dirname || __dirname) + '/icon_argument.png') 14 | export default { 15 | icon: (module.__dirname || __dirname) + '/icon_argument.png', // @@ fix fro mashlib version 16 | 17 | name: 'argument', 18 | 19 | label: function (subject) { 20 | const kb = store 21 | const t = kb.findTypeURIs(subject) 22 | 23 | if (t[UI.ns.arg('Position').uri]) return 'Argument' 24 | 25 | return null 26 | }, 27 | 28 | // View the data in a file in user-friendly way 29 | render: function (subject, dom) { 30 | const outliner = panes.getOutliner(dom) 31 | const kb = store 32 | const arg = UI.ns.arg 33 | 34 | subject = kb.canon(subject) 35 | // var types = kb.findTypeURIs(subject) 36 | 37 | const div = dom.createElement('div') 38 | div.setAttribute('class', 'argumentPane') 39 | 40 | // var title = kb.any(subject, UI.ns.dc('title')) 41 | 42 | const comment = kb.any(subject, UI.ns.rdfs('comment')) 43 | if (comment) { 44 | const para = dom.createElement('p') 45 | para.setAttribute('style', 'margin-left: 2em; font-style: italic;') 46 | div.appendChild(para) 47 | para.textContent = comment.value 48 | } 49 | 50 | div.appendChild(dom.createElement('hr')) 51 | 52 | let plist = kb.statementsMatching(subject, arg('support')) 53 | outliner.appendPropertyTRs(div, plist, false) 54 | 55 | div.appendChild(dom.createElement('hr')) 56 | 57 | plist = kb.statementsMatching(subject, arg('opposition')) 58 | outliner.appendPropertyTRs(div, plist, false) 59 | 60 | div.appendChild(dom.createElement('hr')) 61 | return div 62 | } 63 | } 64 | 65 | // ends 66 | -------------------------------------------------------------------------------- /src/argument/argument_icon_v04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/argument/argument_icon_v04.jpg -------------------------------------------------------------------------------- /src/argument/icon_argument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/argument/icon_argument.png -------------------------------------------------------------------------------- /src/argument/transparentyingyang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/argument/transparentyingyang.png -------------------------------------------------------------------------------- /src/attach/tbl-paperclip-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/attach/tbl-paperclip-128.png -------------------------------------------------------------------------------- /src/attach/tbl-paperclip-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/attach/tbl-paperclip-22.png -------------------------------------------------------------------------------- /src/attach/tbl-paperclip-22a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/attach/tbl-paperclip-22a.png -------------------------------------------------------------------------------- /src/audio/audioPane.js: -------------------------------------------------------------------------------- 1 | /* Single audio play Pane 2 | ** 3 | */ 4 | import * as UI from 'solid-ui' 5 | import * as $rdf from 'rdflib' 6 | const ns = UI.ns 7 | 8 | export default { 9 | icon: UI.icons.iconBase + 'noun_534313.svg', 10 | 11 | name: 'audio', 12 | 13 | // Does the subject deserve an audio play pane? 14 | label: function (subject, context) { 15 | const kb = context.session.store 16 | const typeURIs = kb.findTypeURIs(subject) 17 | 18 | const prefix = $rdf.Util.mediaTypeClass('audio/*').uri.split('*')[0] 19 | for (const t in typeURIs) { 20 | if (t.startsWith(prefix)) return 'Play audio' 21 | } 22 | return null 23 | }, 24 | 25 | render: function (subject, context) { 26 | const kb = context.session.store 27 | const dom = context.dom 28 | const options = { 29 | autoplay: false, 30 | chain: true, 31 | chainAlbums: true, 32 | loop: false 33 | } 34 | 35 | const removeExtension = function (str) { 36 | const dot = str.lastIndexOf('.') 37 | if (dot < 0) return str // if any 38 | const slash = str.lastIndexOf('/') 39 | if (dot < slash) return str 40 | return str.slice(0, dot) 41 | } 42 | 43 | // True if there is another file like song.mp3 when this is "song 1.mp3" 44 | // or this is song.m4a 45 | // 46 | const looksRedundant = function (x) { 47 | const folder = kb.any(undefined, ns.ldp('contains'), x) 48 | if (!folder) return false 49 | const contents = kb.each(folder, ns.ldp('contains')) 50 | if (contents.length < 2) return false 51 | const thisName = x.uri 52 | for (let k = 0; k < contents.length; k++) { 53 | const otherName = contents[k].uri 54 | if ( 55 | thisName.length > otherName.length && 56 | thisName.startsWith(removeExtension(otherName)) 57 | ) { 58 | return true 59 | } 60 | if ( 61 | thisName.endsWith('.m4a') && 62 | otherName.endsWith('.mp3') && 63 | removeExtension(thisName) === removeExtension(otherName) 64 | ) { 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | 71 | // Alternative methods could include: 72 | // Accesing metadata in the audio contol, or paring the audio file 73 | const guessNames = function (x) { 74 | const a = x.uri.split('/').slice(-3) // Hope artist, album, track 75 | const decode = function (str) { 76 | try { 77 | return decodeURIComponent(str) 78 | } catch (e) { 79 | return str 80 | } 81 | } 82 | artistRow.textContent = decode(a[0]) 83 | albumRow.textContent = decode(a[1]) 84 | trackRow.textContent = decode(removeExtension(a[2])) 85 | } 86 | 87 | const moveOn = function (current, level) { 88 | return new Promise(function (resolve) { 89 | level = level || 0 90 | if (!options.chain) return resolve(null) 91 | // Ideally navigate graph else cheat with URI munging: 92 | const folder = 93 | kb.any(undefined, ns.ldp('contains'), current) || current.dir() 94 | if (!folder) return resolve(null) 95 | kb.fetcher.load(folder).then(function (_xhr) { 96 | const contents = kb.each(folder, ns.ldp('contains')) // @@ load if not loaded 97 | // if (contents.length < 2) return resolve(null) NO might move on from 1-track album 98 | let j 99 | contents.sort() // sort by URI which hopefully will get tracks in order 100 | for (let i = 0; i < contents.length; i++) { 101 | if (current.uri === contents[i].uri) { 102 | j = (i + 1) % contents.length 103 | if (j === 0) { 104 | if (!options.chainAlbums) { 105 | if (options.loop) { 106 | return resolve(contents[j]) 107 | } 108 | return resolve(null) // No more music needed 109 | } else { 110 | // chain albums 111 | if (level === 1 || !options.chainAlbums) return resolve(null) // limit of navigating treee 112 | moveOn(folder, level + 1).then(function (folder2) { 113 | if (folder2) { 114 | kb.fetcher.load(folder2).then(function (_xhr) { 115 | const contents = kb.each(folder2, ns.ldp('contains')) 116 | if (contents.length === 0) return resolve(null) 117 | contents.sort() 118 | console.log('New Album: ' + folder2) 119 | return resolve(contents[0]) // Start off new album 120 | }) 121 | } 122 | }) 123 | } 124 | } else { 125 | return resolve(contents[j]) 126 | } 127 | } 128 | } // for 129 | }) 130 | }) 131 | } 132 | const endedListener = function (event) { 133 | const current = kb.sym(event.target.getAttribute('src')) 134 | if (!options.chain) return 135 | const tryNext = function (cur) { 136 | const current = cur 137 | moveOn(current).then(function (next) { 138 | if (!next) { 139 | console.log('No successor to ' + current) 140 | return 141 | } 142 | if (!looksRedundant(next)) { 143 | console.log('Moving on to ' + next) 144 | guessNames(next) 145 | controlRow.appendChild(audioControl(next, true)) // Force autoplay 146 | controlRow.removeChild(event.target) 147 | } else { 148 | console.log('Ignoring redundant ' + next) 149 | tryNext(next) 150 | } 151 | }) 152 | } 153 | tryNext(current) 154 | } 155 | 156 | const audioControl = function (song, autoplay) { 157 | const audio = dom.createElement('audio') 158 | audio.setAttribute('controls', 'yes') 159 | // get audio with authenticated fetch 160 | kb.fetcher._fetch(song.uri) 161 | .then(function(response) { 162 | return response.blob() 163 | }) 164 | .then(function(myBlob) { 165 | const objectURL = URL.createObjectURL(myBlob) 166 | audio.setAttribute('src', objectURL) // w640 h480 // 167 | }) 168 | 169 | if (autoplay) { 170 | audio.setAttribute('autoplay', 'autoplay') // Make this a personal preference 171 | } 172 | audio.addEventListener('ended', endedListener, false) 173 | return audio 174 | } 175 | 176 | const div = dom.createElement('div') 177 | const table = div.appendChild(dom.createElement('table')) 178 | const labelStyle = 'padding: 0.3em; color:white; background-color: black;' 179 | const artistRow = table.appendChild(dom.createElement('tr')) 180 | artistRow.style.cssText = labelStyle 181 | const albumRow = table.appendChild(dom.createElement('tr')) 182 | albumRow.style.cssText = labelStyle 183 | const trackRow = table.appendChild(dom.createElement('tr')) 184 | trackRow.style.cssText = labelStyle 185 | const controlRow = table.appendChild(dom.createElement('tr')) 186 | guessNames(subject) 187 | controlRow.appendChild(audioControl(subject, options.autoplay)) 188 | 189 | if (!kb.holds(undefined, ns.ldp('contains'), subject) && subject.dir()) { 190 | kb.fetcher.load(subject.dir()) // Prefetch enclosing @@ or playlist 191 | } 192 | 193 | return div 194 | } 195 | } 196 | 197 | // ends 198 | -------------------------------------------------------------------------------- /src/chatPreferencesForm.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix ui: . 3 | @prefix : <#>. 4 | 5 | :this 6 | "Chat preferences" ; 7 | a ui:Form ; 8 | ui:part :colorizeField; 9 | ui:parts ( :colorizeField :expandImagesInline ). 10 | 11 | :colorizeField a ui:BooleanField; ui:label "Color user input by user". 12 | :expandImagesInline a ui:BooleanField; ui:label "Expand image URLs inline". 13 | -------------------------------------------------------------------------------- /src/classInstancePane.js: -------------------------------------------------------------------------------- 1 | /* Class member Pane 2 | ** 3 | ** This outline pane lists the members of a class 4 | */ 5 | 6 | import * as UI from 'solid-ui' 7 | import * as $rdf from 'rdflib' 8 | 9 | const ns = UI.ns 10 | 11 | export const classInstancePane = { 12 | icon: UI.icons.originalIconBase + 'tango/22-folder-open.png', 13 | 14 | name: 'classInstance', 15 | 16 | // Create a new folder in a Solid system, 17 | 18 | audience: [ns.solid('PowerUser')], 19 | 20 | label: function (subject, context) { 21 | const kb = context.session.store 22 | const n = kb.each(undefined, ns.rdf('type'), subject).length 23 | if (n > 0) return 'List (' + n + ')' // Show how many in hover text 24 | return null // Suppress pane otherwise 25 | }, 26 | 27 | render: function (subject, context) { 28 | const dom = context.dom 29 | const outliner = context.getOutliner(dom) 30 | const kb = context.session.store 31 | const complain = function complain (message, color) { 32 | const pre = dom.createElement('pre') 33 | pre.setAttribute('style', 'background-color: ' + color || '#eed' + ';') 34 | div.appendChild(pre) 35 | pre.appendChild(dom.createTextNode(message)) 36 | } 37 | const div = dom.createElement('div') 38 | div.setAttribute('class', 'instancePane') 39 | div.setAttribute( 40 | 'style', 41 | ' border-top: solid 1px #777; border-bottom: solid 1px #777; margin-top: 0.5em; margin-bottom: 0.5em ' 42 | ) 43 | 44 | // If this is a class, look for all both explicit and implicit 45 | const sts = kb.statementsMatching(undefined, ns.rdf('type'), subject) 46 | if (sts.length > 0) { 47 | const already = {} 48 | const more = [] 49 | sts.forEach(st => { 50 | already[st.subject.toNT()] = st 51 | }) 52 | for (const nt in kb.findMembersNT(subject)) { 53 | if (!already[nt]) { 54 | more.push($rdf.st(kb.fromNT(nt), ns.rdf('type'), subject)) // @@ no provenance 55 | } 56 | } 57 | if (more.length) { 58 | complain( 59 | 'There are ' + 60 | sts.length + 61 | ' explicit and ' + 62 | more.length + 63 | ' implicit members of ' + 64 | UI.utils.label(subject) 65 | ) 66 | } 67 | if (subject.sameTerm(ns.rdf('Property'))) { 68 | // / Do not find all properties used as properties .. unless look at kb index 69 | } else if (subject.sameTerm(ns.rdfs('Class'))) { 70 | const uses = kb.statementsMatching(undefined, ns.rdf('type'), undefined) 71 | const usedTypes = {} 72 | uses.forEach(function (st) { 73 | usedTypes[st.object] = st 74 | }) // Get unique 75 | const used = [] 76 | for (const i in usedTypes) { 77 | used.push($rdf.st($rdf.sym(i), ns.rdf('type'), ns.rdfs('Class'))) 78 | } 79 | complain( 80 | 'Total of ' + 81 | uses.length + 82 | ' type statements and ' + 83 | used.length + 84 | ' unique types.' 85 | ) 86 | } 87 | 88 | if (sts.length > 10) { 89 | const tr = dom.createElement('TR') 90 | tr.appendChild(dom.createTextNode('' + sts.length)) 91 | // tr.AJAR_statement=sts[i] 92 | div.appendChild(tr) 93 | } 94 | 95 | outliner.appendPropertyTRs(div, sts, true, function (_pred) { 96 | return true 97 | }) 98 | 99 | if (more.length) { 100 | complain('Implicit:') 101 | outliner.appendPropertyTRs(div, more, true, function (_pred) { 102 | return true 103 | }) 104 | } 105 | } 106 | 107 | return div 108 | } 109 | } 110 | // ends 111 | -------------------------------------------------------------------------------- /src/dashboard/basicPreferences.ts: -------------------------------------------------------------------------------- 1 | import { PaneDefinition } from 'pane-registry' 2 | import { IndexedFormula, NamedNode, parse, Store } from 'rdflib' 3 | import { icons, login, ns, widgets } from 'solid-ui' 4 | import ontologyData from './ontologyData.ttl' 5 | import preferencesFormText from './preferencesFormText.ttl' 6 | 7 | export const basicPreferencesPane: PaneDefinition = { 8 | icon: icons.iconBase + 'noun_Sliders_341315_000000.svg', 9 | name: 'basicPreferences', 10 | label: _subject => { 11 | return null 12 | }, 13 | 14 | // Render the pane 15 | // The subject should be the logged in user. 16 | render: (subject, context) => { 17 | const dom = context.dom 18 | const store = context.session.store as Store 19 | 20 | function complainIfBad (ok: Boolean, mess: any) { 21 | if (ok) return 22 | container.appendChild(widgets.errorMessageBlock(dom, mess, '#fee')) 23 | } 24 | 25 | const container = dom.createElement('div') 26 | 27 | const formArea = setupUserTypesSection(container, dom) 28 | 29 | function loadData (doc: NamedNode, turtle: String) { 30 | doc = doc.doc() // remove # from URI if nec 31 | if (!store.holds(undefined, undefined, undefined, doc)) { 32 | // If not loaded already 33 | ;(parse as any)(turtle, store, doc.uri, 'text/turtle', null) // Load form directly 34 | } 35 | } 36 | 37 | const preferencesForm = store.sym( 38 | 'urn:uuid:93774ba1-d3b6-41f2-85b6-4ae27ffd2597#this' 39 | ) 40 | loadData(preferencesForm, preferencesFormText) 41 | 42 | const ontologyExtra = store.sym( 43 | 'urn:uuid:93774ba1-d3b6-41f2-85b6-4ae27ffd2597-ONT' 44 | ) 45 | loadData(ontologyExtra, ontologyData) 46 | 47 | async function doRender () { 48 | const renderContext = await login.ensureLoadedPreferences({ 49 | dom, 50 | div: container 51 | }) 52 | if (!renderContext.preferencesFile) { 53 | // Could be CORS 54 | console.log( 55 | 'Not doing private class preferences as no access to preferences file. ' + 56 | renderContext.preferencesFileError 57 | ) 58 | return 59 | } 60 | const appendedForm = widgets.appendForm( 61 | dom, 62 | formArea, 63 | {}, 64 | renderContext.me, 65 | preferencesForm, 66 | renderContext.preferencesFile, 67 | complainIfBad 68 | ) 69 | appendedForm.style.borderStyle = 'none' 70 | 71 | const trustedApplicationsView = context.session.paneRegistry.byName('trustedApplications') 72 | if (trustedApplicationsView) { 73 | container.appendChild(trustedApplicationsView.render(null, context)) 74 | } 75 | 76 | // @@ TODO Remove need for casting as any and bang (!) syntax 77 | addDeleteSection(container, store, renderContext.me!, dom) 78 | } 79 | 80 | doRender() 81 | 82 | return container 83 | } 84 | } 85 | 86 | function setupUserTypesSection ( 87 | container: Element, 88 | dom: HTMLDocument 89 | ): Element { 90 | const formContainer = createSection(container, dom, 'User types') 91 | 92 | const description = formContainer.appendChild(dom.createElement('p')) 93 | description.innerText = 'Here you can self-assign user types to help the data browser know which views you would like to access.' 94 | 95 | const userTypesLink = formContainer.appendChild(dom.createElement('a')) 96 | userTypesLink.href = 'https://github.com/solidos/userguide/#role' 97 | userTypesLink.innerText = 'Read more' 98 | 99 | const formArea = formContainer.appendChild(dom.createElement('div')) 100 | 101 | return formArea 102 | } 103 | 104 | export default basicPreferencesPane 105 | 106 | // ends 107 | 108 | function addDeleteSection ( 109 | container: HTMLElement, 110 | store: IndexedFormula, 111 | profile: NamedNode, 112 | dom: HTMLDocument 113 | ): void { 114 | const section = createSection(container, dom, 'Delete account') 115 | 116 | const podServerNodes = store.each(profile, ns.space('storage'), null, profile.doc()) 117 | const podServers = podServerNodes.map(node => node.value) 118 | 119 | const list = section.appendChild(dom.createElement('ul')) 120 | 121 | podServers.forEach(async server => { 122 | const deletionLink = await generateDeletionLink(server, dom) 123 | if (deletionLink) { 124 | const listItem = list.appendChild(dom.createElement('li')) 125 | listItem.appendChild(deletionLink) 126 | } 127 | }) 128 | } 129 | 130 | async function generateDeletionLink ( 131 | podServer: string, 132 | dom: HTMLDocument 133 | ): Promise { 134 | const link = dom.createElement('a') 135 | link.textContent = `Delete your account at ${podServer}` 136 | const deletionUrl = await getDeletionUrlForServer(podServer) 137 | if (typeof deletionUrl !== 'string') { 138 | return null 139 | } 140 | link.href = deletionUrl 141 | return link 142 | } 143 | 144 | /** 145 | * Hacky way to get the deletion link to a Pod 146 | * 147 | * This function infers the deletion link by assuming the URL structure of Node Solid server. 148 | * In the future, Solid will hopefully provide a standardised way of discovering the deletion link: 149 | * https://github.com/solidos/data-interoperability-panel/issues/18 150 | * 151 | * If NSS is in multi-user mode (the case on inrupt.net and solid.community), the deletion URL for 152 | * vincent.dev.inrupt.net would be at dev.inrupt.net/account/delete. In single-user mode, the 153 | * deletion URL would be at vincent.dev.inrupt.net/account/delete. 154 | * 155 | * @param server Pod server containing the user's account. 156 | * @returns URL of the page that Node Solid Server would offer to delete the account, or null if 157 | * the URLs we tried give invalid responses. 158 | */ 159 | async function getDeletionUrlForServer ( 160 | server: string 161 | ): Promise { 162 | const singleUserUrl = new URL(server) 163 | const multiUserUrl = new URL(server) 164 | multiUserUrl.pathname = singleUserUrl.pathname = '/account/delete' 165 | 166 | const hostnameParts = multiUserUrl.hostname.split('.') 167 | // Remove `vincent.` from `vincent.dev.inrupt.net`, for example: 168 | multiUserUrl.hostname = hostnameParts.slice(1).join('.') 169 | 170 | const multiUserNssResponse = await fetch(multiUserUrl.href, { 171 | method: 'HEAD' 172 | }) 173 | if (multiUserNssResponse.ok) { 174 | return multiUserUrl.href 175 | } 176 | 177 | const singleUserNssResponse = await fetch(singleUserUrl.href, { 178 | method: 'HEAD' 179 | }) 180 | if (singleUserNssResponse.ok) { 181 | return singleUserUrl.href 182 | } 183 | return null 184 | } 185 | 186 | function createSection ( 187 | container: Element, 188 | dom: HTMLDocument, 189 | title: string 190 | ): Element { 191 | const section = container.appendChild(dom.createElement('div')) 192 | section.style.border = '0.3em solid #418d99' 193 | section.style.borderRadius = '0.5em' 194 | section.style.padding = '0.7em' 195 | section.style.marginTop = '0.7em' 196 | 197 | const titleElement = section.appendChild(dom.createElement('h3')) 198 | titleElement.innerText = title 199 | 200 | return section 201 | } 202 | -------------------------------------------------------------------------------- /src/dashboard/dashboardPane.ts: -------------------------------------------------------------------------------- 1 | import { icons } from 'solid-ui' 2 | import { authn, authSession, store } from 'solid-logic' 3 | import { Fetcher, NamedNode } from 'rdflib' 4 | import { generateHomepage } from './homepage' 5 | import { DataBrowserContext, PaneDefinition } from 'pane-registry' 6 | 7 | export const dashboardPane: PaneDefinition = { 8 | icon: icons.iconBase + 'noun_547570.svg', 9 | name: 'dashboard', 10 | label: subject => { 11 | console.log() 12 | if (subject.uri === subject.site().uri) { 13 | return 'Dashboard' 14 | } 15 | return null 16 | }, 17 | render: (subject, context) => { 18 | console.log('Dashboard Pane Render') 19 | const dom = context.dom 20 | const container = dom.createElement('div') 21 | const runBuildPage = () => { 22 | container.innerHTML = '' 23 | buildPage( 24 | container, 25 | authn.currentUser() || null, 26 | context, 27 | subject 28 | ) 29 | } 30 | 31 | authSession.onLogin(() => { 32 | // console.log('On Login') 33 | runBuildPage() 34 | }) 35 | authSession.onSessionRestore(() => { 36 | // console.log('On Session Restore') 37 | runBuildPage() 38 | }) 39 | // console.log('Initial Load') 40 | runBuildPage() 41 | 42 | return container 43 | } 44 | } 45 | 46 | function buildPage ( 47 | container: HTMLElement, 48 | webId: NamedNode | null, 49 | context: DataBrowserContext, 50 | subject: NamedNode 51 | ) { 52 | // if uri then SolidOS is a browse.html web app 53 | const uri = (new URL(window.location.href)).searchParams.get('uri') 54 | if (webId && (uri || webId.site().uri === subject.site().uri)) { 55 | return buildDashboard(container, context) 56 | } 57 | return buildHomePage(container, subject) 58 | } 59 | 60 | function buildDashboard (container: HTMLElement, context: DataBrowserContext) { 61 | // console.log('build dashboard') 62 | // @@ TODO get a proper type 63 | const outliner: any = context.getOutliner(context.dom) 64 | outliner 65 | .getDashboard() 66 | .then((dashboard: HTMLElement) => container.appendChild(dashboard)) 67 | } 68 | 69 | function buildHomePage (container: HTMLElement, subject: NamedNode) { 70 | // console.log('build home page') 71 | const wrapper = document.createElement('div') 72 | container.appendChild(wrapper) 73 | const shadow = wrapper.attachShadow({ mode: 'open' }) 74 | const link = document.createElement('link') 75 | link.rel = 'stylesheet' 76 | link.href = '/common/css/bootstrap.min.css' 77 | shadow.appendChild(link) 78 | generateHomepage(subject, store, store.fetcher as Fetcher).then(homepage => 79 | shadow.appendChild(homepage) 80 | ) 81 | } 82 | 83 | export default dashboardPane 84 | -------------------------------------------------------------------------------- /src/dashboard/homepage.ts: -------------------------------------------------------------------------------- 1 | import { Fetcher, IndexedFormula, NamedNode, sym } from 'rdflib' 2 | import { ns } from 'solid-ui' 3 | 4 | export async function generateHomepage ( 5 | subject: NamedNode, 6 | store: IndexedFormula, 7 | fetcher: Fetcher 8 | ): Promise { 9 | const ownersProfile = await loadProfile(subject, fetcher) 10 | const name = getName(store, ownersProfile) 11 | 12 | const wrapper = document.createElement('div') 13 | wrapper.classList.add('container') 14 | wrapper.appendChild(createTitle(ownersProfile.uri, name)) 15 | wrapper.appendChild(createDataSection(name)) 16 | 17 | return wrapper 18 | } 19 | 20 | function createDataSection (name: string): HTMLElement { 21 | const dataSection = document.createElement('section') 22 | 23 | const title = document.createElement('h2') 24 | title.innerText = 'Data' 25 | dataSection.appendChild(title) 26 | 27 | const listGroup = document.createElement('div') 28 | listGroup.classList.add('list-group') 29 | dataSection.appendChild(listGroup) 30 | 31 | const publicDataLink = document.createElement('a') 32 | publicDataLink.classList.add('list-group-item') 33 | publicDataLink.href = window.document.location.href + 'public/' 34 | publicDataLink.innerText = `View ${name}'s files` 35 | listGroup.appendChild(publicDataLink) 36 | 37 | return dataSection 38 | } 39 | 40 | function createTitle (uri: string, name: string): HTMLElement { 41 | const profileLink = document.createElement('a') 42 | profileLink.href = uri 43 | profileLink.innerText = name 44 | 45 | const profileLinkPost = document.createElement('span') 46 | profileLinkPost.innerText = '\'s Pod' 47 | 48 | const title = document.createElement('h1') 49 | title.appendChild(profileLink) 50 | title.appendChild(profileLinkPost) 51 | 52 | return title 53 | } 54 | 55 | async function loadProfile ( 56 | subject: NamedNode, 57 | fetcher: Fetcher 58 | ): Promise { 59 | const pod = subject.site().uri 60 | // TODO: This is a hack - we cannot assume that the profile is at this document, but we will live with it for now 61 | const webId = sym(`${pod}profile/card#me`) 62 | await fetcher.load(webId) 63 | return webId 64 | } 65 | 66 | function getName (store: IndexedFormula, ownersProfile: NamedNode): string { 67 | return ( 68 | store.anyValue(ownersProfile, ns.vcard('fn'), null, ownersProfile.doc()) || 69 | store.anyValue(ownersProfile, ns.foaf('name'), null, ownersProfile.doc()) || 70 | new URL(ownersProfile.uri).host.split('.')[0] 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/dashboard/languages/codes2.txt: -------------------------------------------------------------------------------- 1 | аҧсуа бызшәа, аҧсшәа 2 | Afaraf 3 | Afrikaans 4 | Akan 5 | Shqip 6 | አማርኛ 7 |
العربية
8 | aragonés 9 | Հայերեն 10 | অসমীয়া 11 | авар мацӀ, магӀарул мацӀ 12 | avesta 13 | aymar aru 14 | azərbaycan dili 15 | bamanankan 16 | башҡорт теле 17 | euskara, euskera 18 | беларуская мова 19 | বাংলা 20 | भोजपुरी 21 | Bislama 22 | bosanski jezik 23 | brezhoneg 24 | български език 25 | ဗမာစာ 26 | català, valencià 27 | Chamoru 28 | нохчийн мотт 29 | chiCheŵa, chinyanja 30 | чӑваш чӗлхи 31 | Kernewek 32 | corsu, lingua corsa 33 | ᓀᐦᐃᔭᐍᐏᐣ 34 | hrvatski jezik 35 | čeština, český jazyk 36 | dansk 37 |
ދިވެހި
38 | Nederlands, Vlaams 39 | རྫོང་ཁ 40 | English 41 | Esperanto 42 | eesti, eesti keel 43 | Eʋegbe 44 | føroyskt 45 | vosa Vakaviti 46 | suomi, suomen kieli 47 | français, langue française 48 | Fulfulde, Pulaar, Pular 49 | Galego 50 | ქართული 51 | Deutsch 52 | ελληνικά 53 | Avañe'ẽ 54 | ગુજરાતી 55 | Kreyòl ayisyen 56 |
(Hausa) هَوُسَ
57 |
עברית
58 | Otjiherero 59 | हिन्दी, हिंदी 60 | Hiri Motu 61 | magyar 62 | Interlingua 63 | Bahasa Indonesia 64 | Gaeilge 65 | Asụsụ Igbo 66 | Iñupiaq, Iñupiatun 67 | Ido 68 | Íslenska 69 | Italiano 70 | ᐃᓄᒃᑎᑐᑦ 71 | ꦧꦱꦗꦮ, Basa Jawa 72 | kalaallisut, kalaallit oqaasii 73 | ಕನ್ನಡ 74 | Kanuri 75 | қазақ тілі 76 | ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ 77 | Gĩkũyũ 78 | Ikinyarwanda 79 | Кыргызча, Кыргыз тили 80 | коми кыв 81 | Kikongo 82 | Kuanyama 83 | latine, lingua latina 84 | Lëtzebuergesch 85 | Luganda 86 | Limburgs 87 | Lingála 88 | ພາສາລາວ 89 | lietuvių kalba 90 | latviešu valoda 91 | Gaelg, Gailck 92 | македонски јазик 93 | fiteny malagasy 94 | മലയാളം 95 | Malti 96 | te reo Māori 97 | मराठी 98 | Kajin M̧ajeļ 99 | Монгол хэл 100 | Dorerin Naoero 101 | Diné bizaad 102 | isiNdebele 103 | नेपाली 104 | Owambo 105 | Norsk Bokmål 106 | Norsk Nynorsk 107 | Norsk 108 | ꆈꌠ꒿ Nuosuhxop 109 | isiNdebele 110 | occitan, lenga d'òc 111 | ᐊᓂᔑᓈᐯᒧᐎᓐ 112 | ѩзыкъ словѣньскъ 113 | Afaan Oromoo 114 | ଓଡ଼ିଆ 115 | ирон æвзаг 116 | पालि, पाळि 117 |
فارسی
118 | język polski, polszczyzna 119 |
پښتو
120 | Português 121 | Runa Simi, Kichwa 122 | Rumantsch Grischun 123 | Ikirundi 124 | Română 125 | русский 126 | संस्कृतम् 127 | sardu 128 | Davvisámegiella 129 | gagana fa'a Samoa 130 | yângâ tî sängö 131 | српски језик 132 | Gàidhlig 133 | chiShona 134 | සිංහල 135 | Slovenčina, Slovenský Jazyk 136 | Slovenski Jezik, Slovenščina 137 | Soomaaliga, af Soomaali 138 | Sesotho 139 | Español 140 | Basa Sunda 141 | Kiswahili 142 | SiSwati 143 | Svenska 144 | தமிழ் 145 | తెలుగు 146 | ไทย 147 | ትግርኛ 148 | བོད་ཡིག 149 | Türkmen, Түркмен 150 | Wikang Tagalog 151 | Setswana 152 | Faka Tonga 153 | Türkçe 154 | Xitsonga 155 | Twi 156 | Reo Tahiti 157 | Українська 158 |
اردو
159 | Tshivenḓa 160 | Tiếng Việt 161 | Volapük 162 | Walon 163 | Cymraeg 164 | Wollof 165 | Frysk 166 | isiXhosa 167 |
ייִדיש
168 | Yorùbá 169 | Saɯ cueŋƅ, Saw cuengh 170 | isiZulu 171 | -------------------------------------------------------------------------------- /src/dashboard/languages/foo: -------------------------------------------------------------------------------- 1 | Pokestring at -1,-1 2 | 3 | Pokestring at -1,-1 4 | 5 | Pokestring at -1,-1 List of ISO 639-1 codes - Wikipedia 6 | Pokestring at -1,-1 7 | // 8 | document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");RLCONF={"wgCanonicalNamespace":"","wgCanonicalSpecialPageName":!1,"wgNamespaceNumber":0,"wgPageName":"List_of_ISO_639-1_codes","wgTitle":"List of ISO 639-1 codes","wgCurRevisionId":908451105,"wgRevisionId":908451105,"wgArticleId":5611796,"wgIsArticle":!0,"wgIsRedirect":!1,"wgAction":"view","wgUserName":null,"wgUserGroups":["*"],"wgCategories":["Articles with short description","Articles needing additional references from April 2011","All articles needing additional references","Articles containing Chinese-language text","Articles containing Interlingue-language text","Articles containing Japanese-language text","Articles containing Kashmiri-language text","Articles containing Korean-language text","Articles containing Kurdish-language text","Articles containing Malay-language text","Articles containing Punjabi-language text", 9 | "Articles containing Sindhi-language text","Articles containing Tajik-language text","Articles containing Tatar-language text","Articles containing Uighur-language text","Articles containing Uzbek-language text","ISO 639","Identifiers","Unique identifiers"],"wgBreakFrames":!1,"wgPageContentLanguage":"en","wgPageContentModel":"wikitext","wgSeparatorTransformTable":["",""],"wgDigitTransformTable":["",""],"wgDefaultDateFormat":"dmy","wgMonthNames":["","January","February","March","April","May","June","July","August","September","October","November","December"],"wgMonthNamesShort":["","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"wgRelevantPageName":"List_of_ISO_639-1_codes","wgRelevantArticleId":5611796,"wgRequestId":"XT9ULQpAAEIAAE@B4LIAAADW","wgCSPNonce":!1,"wgIsProbablyEditable":!0,"wgRelevantPageIsProbablyEditable":!0,"wgRestrictionEdit":[],"wgRestrictionMove":[],"wgMediaViewerOnClick":!0,"wgMediaViewerEnabledByDefault":!0, 10 | "wgPopupsReferencePreviews":!1,"wgPopupsConflictsWithNavPopupGadget":!1,"wgVisualEditor":{"pageLanguageCode":"en","pageLanguageDir":"ltr","pageVariantFallbacks":"en"},"wgMFDisplayWikibaseDescriptions":{"search":!0,"nearby":!0,"watchlist":!0,"tagline":!1},"wgWMESchemaEditAttemptStepOversample":!1,"wgPoweredByHHVM":!0,"wgULSCurrentAutonym":"English","wgNoticeProject":"wikipedia","wgWikibaseItemId":"Q917906","wgCentralAuthMobileDomain":!1,"wgEditSubmitButtonLabelPublish":!0};RLSTATE={"ext.gadget.charinsert-styles":"ready","ext.globalCssJs.user.styles":"ready","ext.globalCssJs.site.styles":"ready","site.styles":"ready","noscript":"ready","user.styles":"ready","ext.globalCssJs.user":"ready","ext.globalCssJs.site":"ready","user":"ready","user.options":"ready","user.tokens":"loading","ext.cite.styles":"ready","mediawiki.legacy.shared":"ready","mediawiki.legacy.commonPrint":"ready","jquery.tablesorter.styles":"ready","wikibase.client.init":"ready", 11 | "ext.visualEditor.desktopArticleTarget.noscript":"ready","ext.uls.interlanguage":"ready","ext.wikimediaBadges":"ready","ext.3d.styles":"ready","mediawiki.skinning.interface":"ready","skins.vector.styles":"ready"};RLPAGEMODULES=["ext.cite.ux-enhancements","site","mediawiki.page.startup","mediawiki.page.ready","jquery.tablesorter","mediawiki.searchSuggest","ext.gadget.teahouse","ext.gadget.ReferenceTooltips","ext.gadget.watchlist-notice","ext.gadget.DRN-wizard","ext.gadget.charinsert","ext.gadget.refToolbar","ext.gadget.extra-toolbar-buttons","ext.gadget.switcher","ext.centralauth.centralautologin","mmv.head","mmv.bootstrap.autostart","ext.popups","ext.visualEditor.desktopArticleTarget.init","ext.visualEditor.targetLoader","ext.eventLogging","ext.wikimediaEvents","ext.navigationTiming","ext.uls.compactlinks","ext.uls.interface","ext.quicksurveys.init","ext.centralNotice.geoIP","ext.centralNotice.startUp","skins.vector.js"]; 12 | // 13 | 14 | Pokestring at -1,-1 15 | // 16 | (RLQ=window.RLQ||[]).push(function(){mw.loader.implement("user.tokens@0tffind",function($,jQuery,require,module){/*@nomin*/mw.user.tokens.set({"editToken":"+\\","patrolToken":"+\\","watchToken":"+\\","csrfToken":"+\\"}); 17 | });}); 18 | // 19 | 20 | Pokestring at -1,-1 21 | 22 | Pokestring at -1,-1 23 | /**/ 24 | .mw-parser-output cite.citation{font-style:inherit}.mw-parser-output .citation q{quotes:"\"""\"""'""'"}.mw-parser-output .citation .cs1-lock-free a{background:url("//upload.wikimedia.org/wikipedia/commons/thumb/6/65/Lock-green.svg/9px-Lock-green.svg.png")no-repeat;background-position:right .1em center}.mw-parser-output .citation .cs1-lock-limited a,.mw-parser-output .citation .cs1-lock-registration a{background:url("//upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Lock-gray-alt-2.svg/9px-Lock-gray-alt-2.svg.png")no-repeat;background-position:right .1em center}.mw-parser-output .citation .cs1-lock-subscription a{background:url("//upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Lock-red-alt-2.svg/9px-Lock-red-alt-2.svg.png")no-repeat;background-position:right .1em center}.mw-parser-output .cs1-subscription,.mw-parser-output .cs1-registration{color:#555}.mw-parser-output .cs1-subscription span,.mw-parser-output .cs1-registration span{border-bottom:1px dotted;cursor:help}.mw-parser-output .cs1-ws-icon a{background:url("//upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/12px-Wikisource-logo.svg.png")no-repeat;background-position:right .1em center}.mw-parser-output code.cs1-code{color:inherit;background:inherit;border:inherit;padding:inherit}.mw-parser-output .cs1-hidden-error{display:none;font-size:100%}.mw-parser-output .cs1-visible-error{font-size:100%}.mw-parser-output .cs1-maint{display:none;color:#33aa33;margin-left:0.3em}.mw-parser-output .cs1-subscription,.mw-parser-output .cs1-registration,.mw-parser-output .cs1-format{font-size:95%}.mw-parser-output .cs1-kern-left,.mw-parser-output .cs1-kern-wl-left{padding-left:0.2em}.mw-parser-output .cs1-kern-right,.mw-parser-output .cs1-kern-wl-right{padding-right:0.2em} 25 | /**/ 26 | 27 | Pokestring at -1,-1 28 | 29 | 30 | Pokestring at -1,-1 31 | 32 | Pokestring at -1,-1 List of ISO 33 | 639-1 codes 34 | Pokestring at -1,-1 35 | 36 | Pokestring at -1,-1 From Wikipedia, the free 37 | encyclopedia 38 | Pokestring at -1,-1 Jump to navigation 39 | Pokestring at -1,-1 Jump to search 40 | Pokestring at -1,-1 41 | 42 | Pokestring at -1,-1 43 | 44 | Pokestring at -1,-1 Wikimedia list article 45 | Pokestring at -2,-1 46 | 47 | Pokestring at -2,-1 48 | 49 | Pokestring at -1,-1 50 | 51 | column 0 52 | Pokestring at -1,0 53 | 54 | column 1 55 | Pokestring at -1,1 56 | 57 | Pokestring at -1,1 This article 58 | Pokestring at -1,1 needs additional 59 | citations for 60 | Pokestring at -1,1 verification 61 | Pokestring at -1,1 Please help 62 | Pokestring at -1,1 63 | improve this article 64 | Pokestring at -1,1 adding 65 | citations to reliable sources 66 | Pokestring at -1,1 Find sources: 67 | Pokestring at -1,1 "List 68 | of ISO 639-1 codes" 69 | Pokestring at -1,1 news 70 | Pokestring at -1,1 -------------------------------------------------------------------------------- /src/dashboard/languages/foo.ttl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/dashboard/languages/foo.ttl -------------------------------------------------------------------------------- /src/dashboard/languages/get-language-names.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Language names from ... https://www.omniglot.com/language/names.htm ?? 3 | # https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 4 | # https://lov.linkeddata.es/dataset/lov/terms?q=Language%20Code 5 | # http://dbpedia.org/ontology/Language 6 | # https://schema.org/knowsLanguage -> Language 7 | 8 | curl https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes > codes.html 9 | tidy -m -asxml codes.html 10 | # sed -e 's/ //g' < codes.html > codes.xml 11 | # python /devel/github.com/linkeddata/swap/tab2n3.py -xhtml < codes.xml > languageCodes.ttl 12 | grep " codes2.txt 13 | -------------------------------------------------------------------------------- /src/dashboard/ontologyData.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix rdfs: . 3 | @prefix solid: . 4 | @prefix foaf: . 5 | @prefix schema: . 6 | @prefix ui: . 7 | @prefix vcard: . 8 | @prefix : <#>. 9 | 10 | solid:User a rdfs:Class; 11 | rdfs:label "user"@en, "utilisateur"@fr; 12 | rdfs:comment """Any person who might use a Solid-based system"""; 13 | rdfs:subClassOf foaf:Person, schema:Person, vcard:Individual. 14 | 15 | # Since these options are opt-in, it is a bit strange to have new users opt in 16 | # That they are new users - also we do not use this class for anything specific 17 | # yet 18 | # solid:NewUser a rdfs:Class; 19 | # rdfs:label "new user"@en; 20 | # rdfs:comment """A person who might use a Solid-based system who has a low 21 | # level of familiarity with technical details."""; 22 | # rdfs:subClassOf solid:User. 23 | 24 | solid:PowerUser a rdfs:Class; 25 | rdfs:label "power user"@en; 26 | rdfs:comment """A person who might use a Solid-based system 27 | who is prepared to be given a more complex interface in order 28 | to be provided with more pwerful features."""; 29 | rdfs:subClassOf solid:User. 30 | 31 | solid:Developer a rdfs:Class; 32 | rdfs:label "Developer"; 33 | rdfs:comment """Any person who might use a Solid-based system, 34 | who has software development skills."""; 35 | rdfs:subClassOf solid:User. 36 | -------------------------------------------------------------------------------- /src/dashboard/preferencesFormText.ttl: -------------------------------------------------------------------------------- 1 | @prefix rdf: . 2 | @prefix solid: . 3 | @prefix ui: . 4 | @prefix : <#>. 5 | 6 | :this "Basic preferences" ; 7 | a ui:Form ; 8 | ui:part :categorizeUser, :privateComment, :personalInformationHeading; 9 | ui:parts ( :personalInformationHeading :privateComment :categorizeUser ). 10 | 11 | :personalInformationHeading a ui:Heading; 12 | ui:contents "Personal information". 13 | 14 | :privateComment a ui:Comment; 15 | ui:contents "This information is private.". 16 | 17 | :categorizeUser a ui:Classifier; 18 | ui:label "Level of user"; ui:property rdf:type ; ui:category solid:User. 19 | -------------------------------------------------------------------------------- /src/defaultPane.js: -------------------------------------------------------------------------------- 1 | /* Default Pane 2 | ** 3 | ** This outline pane contains the properties which are 4 | ** normally displayed to the user. See also: internalPane 5 | ** This pane hides the ones considered too low-level for the normal user. 6 | */ 7 | 8 | import * as UI from 'solid-ui' 9 | import * as $rdf from 'rdflib' 10 | const ns = UI.ns 11 | 12 | export const defaultPane = { 13 | icon: UI.icons.originalIconBase + 'about.png', 14 | 15 | name: 'default', 16 | 17 | audience: [ns.solid('Developer')], 18 | 19 | label: function (_subject) { 20 | return 'about ' 21 | }, 22 | 23 | render: function (subject, context) { 24 | const dom = context.dom 25 | 26 | const filter = function (pred, inverse) { 27 | if ( 28 | typeof context.session.paneRegistry.byName('internal').predicates[ 29 | pred.uri 30 | ] !== 'undefined' 31 | ) { 32 | return false 33 | } 34 | if ( 35 | inverse && 36 | pred.uri === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' 37 | ) { 38 | return false 39 | } 40 | return true 41 | } 42 | 43 | const outliner = context.getOutliner(dom) 44 | const kb = context.session.store 45 | // var outline = outliner; // @@ 46 | UI.log.info('@defaultPane.render, dom is now ' + dom.location) 47 | subject = kb.canon(subject) 48 | const div = dom.createElement('div') 49 | div.setAttribute('class', 'defaultPane') 50 | // appendRemoveIcon(div, subject, div) 51 | 52 | let plist = kb.statementsMatching(subject) 53 | outliner.appendPropertyTRs(div, plist, false, filter) 54 | plist = kb.statementsMatching(undefined, undefined, subject) 55 | outliner.appendPropertyTRs(div, plist, true, filter) 56 | if ( 57 | subject.termType === 'Literal' && 58 | subject.value.slice(0, 7) === 'http://' 59 | ) { 60 | outliner.appendPropertyTRs( 61 | div, 62 | [$rdf.st(kb.sym(subject.value), UI.ns.link('uri'), subject)], 63 | true, 64 | filter 65 | ) 66 | } 67 | if ( 68 | (subject.termType === 'NamedNode' && 69 | kb.updater.editable(UI.rdf.Util.uri.docpart(subject.uri), kb)) || 70 | (subject.termType === 'BlankNode' && 71 | kb.anyStatementMatching(subject) && 72 | kb.anyStatementMatching(subject).why && 73 | kb.anyStatementMatching(subject).why.uri && 74 | kb.updater.editable(kb.anyStatementMatching(subject).why.uri)) 75 | // check the document containing something about of the bnode @@ what about as object? 76 | /* ! && HCIoptions["bottom insert highlights"].enabled */ 77 | ) { 78 | const holdingTr = dom.createElement('tr') // these are to minimize required changes 79 | const holdingTd = dom.createElement('td') // in userinput.js 80 | holdingTd.setAttribute('colspan', '2') 81 | holdingTd.setAttribute('notSelectable', 'true') 82 | const img = dom.createElement('img') 83 | img.src = UI.icons.originalIconBase + 'tango/22-list-add-new.png' 84 | img.addEventListener('click', function addNewTripleIconMouseDownListener ( 85 | e 86 | ) { 87 | outliner.UserInput.addNewPredicateObject(e) 88 | e.stopPropagation() 89 | e.preventDefault() 90 | }) 91 | img.className = 'bottom-border-active' 92 | // img.addEventListener('click', thisOutline.UserInput.addNewPredicateObject,false) 93 | div 94 | .appendChild(holdingTr) 95 | .appendChild(holdingTd) 96 | .appendChild(img) 97 | } 98 | return div 99 | } 100 | } 101 | 102 | // ends 103 | -------------------------------------------------------------------------------- /src/dokieli/Makefile: -------------------------------------------------------------------------------- 1 | 2 | new.js : new.html 3 | (echo 'module.exports = `'; cat new.html; echo '`') > new.js 4 | 5 | new.html : 6 | curl -HAccept:text/html https://dokie.li/new > new.html 7 | -------------------------------------------------------------------------------- /src/dokieli/dokieliPane.js: -------------------------------------------------------------------------------- 1 | /* Human-readable editable "Dokieli" Pane 2 | ** 3 | ** This outline pane contains the document contents for a Dokieli document 4 | ** The dokeili system allows the user to edit a document including anotations 5 | ** review. It does not use turtle, but RDF/a 6 | */ 7 | 8 | import * as UI from 'solid-ui' 9 | import * as $rdf from 'rdflib' 10 | import * as mime from 'mime-types' 11 | 12 | // const DOKIELI_TEMPLATE_URI = 'https://dokie.li/new' // Copy to make new dok 13 | 14 | import DOKIELI_TEMPLATE from './new.js' // Distributed with this library 15 | 16 | export default { 17 | icon: UI.icons.iconBase + 'dokieli-logo.png', // @@ improve? more like doccument? 18 | 19 | name: 'Dokieli', 20 | 21 | mintClass: UI.ns.solid('DokieliDocument'), // @@ A better class? 22 | 23 | label: function (subject, context) { 24 | const kb = context.session.store 25 | const ns = UI.ns 26 | const allowed = [ 27 | // 'text/plain', 28 | 'text/html', 29 | 'application/xhtml+xml' 30 | // 'image/png', 'image/jpeg', 'application/pdf', 31 | // 'video/mp4' 32 | ] 33 | 34 | const hasContentTypeIn = function (kb, x, displayables) { 35 | const cts = kb.fetcher.getHeader(x, 'content-type') 36 | if (cts) { 37 | for (let j = 0; j < cts.length; j++) { 38 | for (let k = 0; k < displayables.length; k++) { 39 | if (cts[j].indexOf(displayables[k]) >= 0) { 40 | return true 41 | } 42 | } 43 | } 44 | } 45 | return false 46 | } 47 | 48 | // This data coul d come from a fetch OR from ldp comtaimner 49 | const hasContentTypeIn2 = function (kb, x, displayables) { 50 | const t = kb.findTypeURIs(x) 51 | for (let k = 0; k < displayables.length; k++) { 52 | if ($rdf.Util.mediaTypeClass(displayables[k]).uri in t) { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | if (!subject.uri) return null // no bnodes 60 | 61 | const t = kb.findTypeURIs(subject) 62 | if (t[ns.link('WebPage').uri]) return 'view' 63 | 64 | if ( 65 | hasContentTypeIn(kb, subject, allowed) || 66 | hasContentTypeIn2(kb, subject, allowed) 67 | ) { 68 | return 'Dok' 69 | } 70 | 71 | return null 72 | }, 73 | 74 | // Create a new folder in a Solid system, with a dokieli editable document in it 75 | mintNew: function (context, newPaneOptions) { 76 | const kb = context.session.store 77 | let newInstance = newPaneOptions.newInstance 78 | if (!newInstance) { 79 | let uri = newPaneOptions.newBase 80 | if (uri.endsWith('/')) { 81 | uri = uri.slice(0, -1) 82 | newPaneOptions.newBase = uri 83 | } 84 | newInstance = kb.sym(uri) 85 | } 86 | 87 | const contentType = mime.lookup(newInstance.uri) 88 | if (!contentType || !contentType.includes('html')) { 89 | newInstance = $rdf.sym(newInstance.uri + '.html') 90 | } 91 | newPaneOptions.newInstance = newInstance // Save for creation system 92 | 93 | // console.log('New dokieli will make: ' + newInstance) 94 | 95 | let htmlContents = DOKIELI_TEMPLATE 96 | let filename = newInstance.uri.split('/').slice(-1)[0] 97 | filename = decodeURIComponent(filename.split('.')[0]) 98 | const encodedTitle = filename 99 | .replace(/&/g, '&') 100 | .replace(//g, '>') 102 | htmlContents = htmlContents.replace('', '<title>' + encodedTitle) 103 | htmlContents = htmlContents.replace( 104 | '</article>', 105 | '<h1>' + encodedTitle + '</h1></article>' 106 | ) 107 | // console.log('@@ New HTML for Dok:' + htmlContents) 108 | return new Promise(function (resolve) { 109 | kb.fetcher 110 | .webOperation('PUT', newInstance.uri, { 111 | data: htmlContents, 112 | contentType: 'text/html' 113 | }) 114 | .then(function () { 115 | console.log( 116 | 'new Dokieli document created at ' + newPaneOptions.newInstance 117 | ) 118 | resolve(newPaneOptions) 119 | }) 120 | .catch(function (err) { 121 | console.log( 122 | 'Error creating dokieli doc at ' + 123 | newPaneOptions.newInstance + 124 | ': ' + 125 | err 126 | ) 127 | }) 128 | }) 129 | }, 130 | 131 | // Derived from: humanReadablePane .. share code? 132 | render: function (subject, context) { 133 | const myDocument = context.dom 134 | const div = myDocument.createElement('div') 135 | const kb = context.session.store 136 | 137 | // @@ When we can, use CSP to turn off scripts within the iframe 138 | div.setAttribute('class', 'docView') 139 | const iframe = myDocument.createElement('IFRAME') 140 | 141 | // Function to set iframe attributes 142 | const setIframeAttributes = (iframe, blob, lines) => { 143 | const objectURL = URL.createObjectURL(blob) 144 | iframe.setAttribute('src', objectURL) 145 | iframe.setAttribute('type', blob.type) 146 | iframe.setAttribute('class', 'doc') 147 | iframe.setAttribute('style', `border: 1px solid; padding: 1em; height:${lines}em; width:800px; resize: both; overflow: auto;`) 148 | 149 | // Apply sandbox attribute only for HTML files 150 | // @@ NOte beflow - if we set ANY sandbox, then Chrome and Safari won't display it if it is PDF. 151 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe 152 | // You can;'t have any sandbox and allow plugins. 153 | // We could sandbox only HTML files I suppose. 154 | // HTML5 bug: https://lists.w3.org/Archives/Public/public-html/2011Jun/0330.html 155 | if (blob.type === 'text/html' || blob.type === 'application/xhtml+xml') { 156 | iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin') 157 | } 158 | } 159 | 160 | // Fetch and process the blob 161 | kb.fetcher._fetch(subject.uri) 162 | .then(response => response.blob()) 163 | .then(blob => { 164 | const blobTextPromise = blob.type.startsWith('text') ? blob.text() : Promise.resolve('') 165 | return blobTextPromise.then(blobText => ({ blob, blobText })) 166 | }) 167 | .then(({ blob, blobText }) => { 168 | const newLines = blobText.includes('<script src="https://dokie.li/scripts/dokieli.js">') ? -10 : 5 169 | const lines = Math.min(30, blobText.split(/\n/).length + newLines) 170 | setIframeAttributes(iframe, blob, lines) 171 | }) 172 | .catch(err => { 173 | console.log('Error fetching or processing blob:', err) 174 | }) 175 | 176 | const cts = kb.fetcher.getHeader(subject.doc(), 'content-type') 177 | const ct = cts ? cts[0].split(';', 1)[0].trim() : null 178 | if (ct) { 179 | console.log('dokieliPane: c-t:' + ct) 180 | } else { 181 | console.log('dokieliPane: unknown content-type?') 182 | } 183 | 184 | const tr = myDocument.createElement('tr') 185 | tr.appendChild(iframe) 186 | div.appendChild(tr) 187 | return div 188 | } 189 | } 190 | // ends 191 | -------------------------------------------------------------------------------- /src/dokieli/new.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <title> 6 | 7 | 13 | 18 | 19 | 20 | 21 | 26 |
27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/dokieli/new.js: -------------------------------------------------------------------------------- 1 | const DOKIELI_TEMPLATE = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | ` 21 | 22 | export default DOKIELI_TEMPLATE 23 | -------------------------------------------------------------------------------- /src/form/form-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/form/form-22.png -------------------------------------------------------------------------------- /src/form/form-b-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/form/form-b-22.png -------------------------------------------------------------------------------- /src/form/form.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/form/form.graffle -------------------------------------------------------------------------------- /src/form/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/form/form.png -------------------------------------------------------------------------------- /src/form/pane.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Pane for running existing forms for any object 3 | ** 4 | */ 5 | 6 | import * as UI from 'solid-ui' 7 | import { authn } from 'solid-logic' 8 | import * as $rdf from 'rdflib' 9 | const ns = UI.ns 10 | 11 | export const formPane = { 12 | icon: UI.icons.iconBase + 'noun_122196.svg', 13 | 14 | name: 'form', 15 | 16 | audience: [ns.solid('PowerUser')], 17 | 18 | // Does the subject deserve this pane? 19 | label: function (subject) { 20 | const n = UI.widgets.formsFor(subject).length 21 | UI.log.debug('Form pane: forms for ' + subject + ': ' + n) 22 | if (!n) return null 23 | return '' + n + ' forms' 24 | }, 25 | 26 | render: function (subject, context) { 27 | const kb = context.session.store 28 | const dom = context.dom 29 | 30 | const mention = function complain (message, style) { 31 | const pre = dom.createElement('p') 32 | pre.setAttribute('style', style || 'color: grey; background-color: white') 33 | box.appendChild(pre).textContent = message 34 | return pre 35 | } 36 | 37 | const complain = function complain (message, style) { 38 | mention(message, 'style', style || 'color: grey; background-color: #fdd;') 39 | } 40 | 41 | const complainIfBad = function (ok, body) { 42 | if (ok) { 43 | // setModifiedDate(store, kb, store); 44 | // rerender(box); // Deleted forms at the moment 45 | } else complain('Sorry, failed to save your change:\n' + body) 46 | } 47 | 48 | // The question of where to store this data about subject 49 | // This in general needs a whole lot more thought 50 | // and it connects to the discoverbility through links 51 | 52 | // const t = kb.findTypeURIs(subject) 53 | 54 | const me = authn.currentUser() 55 | 56 | const box = dom.createElement('div') 57 | box.setAttribute('class', 'formPane') 58 | 59 | if (!me) { 60 | mention( 61 | 'You are not logged in. If you log in and have ' + 62 | 'workspaces then you would be able to select workspace in which ' + 63 | 'to put this new information' 64 | ) 65 | } else { 66 | const ws = kb.each(me, ns.ui('workspace')) 67 | if (ws.length === 0) { 68 | mention( 69 | "You don't seem to have any workspaces defined. " + 70 | 'A workspace is a place on the web (http://..) or in ' + 71 | 'the file system (file:///) to store application data.\n' 72 | ) 73 | } else { 74 | // @@ 75 | } 76 | } 77 | 78 | // Render forms using a given store 79 | 80 | const renderFormsFor = function (store, subject) { 81 | kb.fetcher.nowOrWhenFetched(store.uri, subject, function (ok, body) { 82 | if (!ok) return complain('Cannot load store ' + store.uri + ': ' + body) 83 | 84 | // Render the forms 85 | 86 | const forms = UI.widgets.formsFor(subject) 87 | 88 | // complain('Form for editing this form:'); 89 | for (let i = 0; i < forms.length; i++) { 90 | const form = forms[i] 91 | const heading = dom.createElement('h4') 92 | box.appendChild(heading) 93 | if (form.uri) { 94 | const formStore = $rdf.Util.uri.document(form.uri) 95 | if (formStore.uri !== form.uri) { 96 | // The form is a hash-type URI 97 | const e = box.appendChild( 98 | UI.widgets.editFormButton( 99 | dom, 100 | box, 101 | form, 102 | formStore, 103 | complainIfBad 104 | ) 105 | ) 106 | e.setAttribute('style', 'float: right;') 107 | } 108 | } 109 | const anchor = dom.createElement('a') 110 | anchor.setAttribute('href', form.uri) 111 | heading.appendChild(anchor) 112 | anchor.textContent = UI.utils.label(form, true) 113 | 114 | /* Keep tis as a reminder to let a New one have its URI given by user 115 | mention("Where will this information be stored?") 116 | const ele = dom.createElement('input'); 117 | box.appendChild(ele); 118 | ele.setAttribute('type', 'text'); 119 | ele.setAttribute('size', '72'); 120 | ele.setAttribute('maxlength', '1024'); 121 | ele.setAttribute('style', 'font-size: 80%; color:#222;'); 122 | ele.value = store.uri 123 | */ 124 | 125 | UI.widgets.appendForm( 126 | dom, 127 | box, 128 | {}, 129 | subject, 130 | form, 131 | store, 132 | complainIfBad 133 | ) 134 | } 135 | }) // end: when store loded 136 | } // renderFormsFor 137 | 138 | // Figure out what store 139 | 140 | // Which places are editable and have stuff about the subject? 141 | 142 | let store = null 143 | 144 | // 1. The document URI of the subject itself 145 | const docuri = $rdf.Util.uri.docpart(subject.uri) 146 | if (subject.uri !== docuri && kb.updater.editable(docuri, kb)) { 147 | store = subject.doc() 148 | } // an editable data file with hash 149 | 150 | store = store || kb.any(kb.sym(docuri), ns.link('annotationStore')) 151 | 152 | // 2. where stuff is already stored 153 | if (!store) { 154 | const docs = {} 155 | const docList = [] 156 | store.statementsMatching(subject).forEach(function (st) { 157 | docs[st.why.uri] = 1 158 | }) 159 | store 160 | .statementsMatching(undefined, undefined, subject) 161 | .forEach(function (st) { 162 | docs[st.why.uri] = 2 163 | }) 164 | for (const d in docs) docList.push(docs[d], d) 165 | docList.sort() 166 | for (let i = 0; i < docList.length; i++) { 167 | const uri = docList[i][1] 168 | if (uri && store.updater.editable(uri)) { 169 | store = store.sym(uri) 170 | break 171 | } 172 | } 173 | } 174 | 175 | // 3. In a workspace store 176 | // @@ TODO: Can probably remove _followeach (not done this time because the commit is a very safe refactor) 177 | const _followeach = function (kb, subject, path) { 178 | if (path.length === 0) return [subject] 179 | const oo = kb.each(subject, path[0]) 180 | let res = [] 181 | for (let i = 0; i < oo.length; i++) { 182 | res = res.concat(_followeach(kb, oo[i], path.slice(1))) 183 | } 184 | return res 185 | } 186 | 187 | const date = '2014' // @@@@@@@@@@@@ pass as parameter 188 | 189 | if (store) { 190 | // mention("@@ Ok, we have a store <" + store.uri + ">."); 191 | renderFormsFor(store, subject) 192 | } else { 193 | complain('No suitable store is known, to edit <' + subject.uri + '>.') 194 | const foobarbaz = UI.login.selectWorkspace(dom, function (ws) { 195 | mention('Workspace selected OK: ' + ws) 196 | 197 | const activities = store.each(undefined, ns.space('workspace'), ws) 198 | for (let j = 0; j < activities.length; j++) { 199 | const act = activities[j] 200 | 201 | const subjectDoc2 = store.any(ws, ns.space('store')) 202 | const start = store.any(ws, ns.cal('dtstart')).value() 203 | const end = store.any(ws, ns.cal('dtend')).value() 204 | if (subjectDoc2 && start && end && start <= date && end > date) { 205 | renderFormsFor(subjectDoc2, subject) 206 | break 207 | } else { 208 | complain('Note no suitable annotation store in activity: ' + act) 209 | } 210 | } 211 | }) 212 | box.appendChild(foobarbaz) 213 | } 214 | 215 | return box 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/form/psuedocode-notes.txt: -------------------------------------------------------------------------------- 1 | These notes are about the user interaction flowchart 2 | for the process of chosing a workspace and if necessray 3 | making one, chosing an account or if necessary signing up for one. 4 | This is yet to be coded up mostly (2013-07). 5 | 6 | Could reflect as to whether to build the flow 7 | is data or RDF or do it with futures or just code it up in AJAX normal. 8 | 9 | ________________________________________ 10 | 11 | Allow cancel back to here at any time directly or indirectly 12 | 13 | If preferences not loaded, load preferences. 14 | on error, explain problem and what trying to do, offer to cancel or try again. 15 | In preferences, find workspaces. 16 | 17 | workspace dialogue: 18 | offer create a new workspace 19 | if so, refresh workspace form 20 | offer select a workspace 21 | if selected, continue returning ws. 22 | 23 | activity dialogue: 24 | if workspace not loaded, load workspace 25 | look for suitable activity 26 | if only one, return that. 27 | If none or more, make form. 28 | offer to create a suitable activity 29 | offer to select an activity. 30 | on activity selected, 31 | return the activity 32 | 33 | create the form in the activity's store. 34 | 35 | 36 | 37 | In normal synchronous form this would look like: 38 | 39 | createForm(getActivity(selectWorkspace(loadPreferences(me)))) 40 | 41 | or p = loadPreferences(me) 42 | w = selectWorkspace(p) 43 | a = getActivity(w) 44 | x = createForm(a) 45 | 46 | World futures really help? You would end up with 47 | 48 | loadPreference(me).then(function(p){ 49 | selectWorkspace(p).then(function(w){})}) 50 | .... etc 51 | 52 | or 53 | loadPreference(me).then(function(p){ 54 | selectWorkspace(p)}) 55 | .then(function(w){ 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.ttl' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /src/home/homePane.ts: -------------------------------------------------------------------------------- 1 | /* Home Pane 2 | ** 3 | ** The home pane is avaiable everywhere and allows a user 4 | ** to 5 | ** - keep track of their stuff 6 | ** - make new things, and possibly 7 | ** - keep track of accounts and workspaces etc 8 | ** 9 | */ 10 | 11 | import { PaneDefinition } from 'pane-registry' 12 | import { NamedNode } from 'rdflib' 13 | import { authn } from 'solid-logic' 14 | import { create, icons, login } from 'solid-ui' 15 | import { CreateContext } from 'solid-ui/lib/create/types' 16 | 17 | const HomePaneSource: PaneDefinition = { 18 | icon: icons.iconBase + 'noun_547570.svg', // noun_25830 19 | 20 | global: true, 21 | 22 | name: 'home', 23 | 24 | // Does the subject deserve an home pane? 25 | // 26 | // yes, always! 27 | // 28 | label: function () { 29 | return 'home' 30 | }, 31 | 32 | render: function (subject, context) { 33 | const dom = context.dom 34 | const showContent = async function () { 35 | const homePaneContext = { div: div, dom: dom, statusArea: div, me: me } 36 | /* 37 | div.appendChild(dom.createElement('h4')).textContent = 'Login status' 38 | var loginStatusDiv = div.appendChild(context.dom.createElement('div')) 39 | // TODO: Find out what the actual type is: 40 | type UriType = unknown; 41 | loginStatusDiv.appendChild(UI.login.loginStatusBox(context.dom, () => { 42 | // Here we know new log in status 43 | })) 44 | */ 45 | div.appendChild(dom.createElement('h4')).textContent = 46 | 'Create new thing somewhere' 47 | const creationDiv = div.appendChild(dom.createElement('div')) 48 | const creationContext: CreateContext = { 49 | div: creationDiv, 50 | dom: dom, 51 | statusArea: div, 52 | me: me 53 | } 54 | const relevantPanes = await login.filterAvailablePanes( 55 | context.session.paneRegistry.list 56 | ) 57 | create.newThingUI(creationContext, context, relevantPanes) // newUI Have to pass panes down 58 | 59 | login.registrationList(homePaneContext, {}).then(function () {}) 60 | } 61 | 62 | const div = dom.createElement('div') 63 | const me: NamedNode = authn.currentUser() as NamedNode // this will be incorrect if not logged in 64 | 65 | showContent() 66 | 67 | return div 68 | } 69 | } // pane object 70 | 71 | // ends 72 | export default HomePaneSource 73 | -------------------------------------------------------------------------------- /src/humanReadablePane.js: -------------------------------------------------------------------------------- 1 | /* Human-readable Pane 2 | ** 3 | ** This outline pane contains the document contents for an HTML document 4 | ** This is for peeking at a page, because the user might not want to leave the data browser. 5 | */ 6 | import { icons, ns } from 'solid-ui' 7 | import { Util } from 'rdflib' 8 | import { marked } from 'marked' 9 | import DOMPurify from 'dompurify'; 10 | 11 | const humanReadablePane = { 12 | icon: icons.originalIconBase + 'tango/22-text-x-generic.png', 13 | 14 | name: 'humanReadable', 15 | 16 | label: function (subject, context) { 17 | const kb = context.session.store 18 | 19 | // See also the source pane, which has lower precedence. 20 | 21 | const allowed = [ 22 | 'text/plain', 23 | 'text/html', 24 | 'text/markdown', 25 | 'application/xhtml+xml', 26 | 'image/png', 27 | 'image/jpeg', 28 | 'application/pdf', 29 | 'video/mp4' 30 | ] 31 | 32 | const hasContentTypeIn = function (kb, x, displayables) { 33 | const cts = kb.fetcher.getHeader(x, 'content-type') 34 | if (cts) { 35 | for (let j = 0; j < cts.length; j++) { 36 | for (let k = 0; k < displayables.length; k++) { 37 | if (cts[j].indexOf(displayables[k]) >= 0) { 38 | return true 39 | } 40 | } 41 | } 42 | } 43 | return false 44 | } 45 | 46 | // This data could come from a fetch OR from ldp container 47 | const hasContentTypeIn2 = function (kb, x, displayables) { 48 | const t = kb.findTypeURIs(x) 49 | for (let k = 0; k < displayables.length; k++) { 50 | if (Util.mediaTypeClass(displayables[k]).uri in t) { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | 57 | if (!subject.uri) return null // no bnodes 58 | 59 | const t = kb.findTypeURIs(subject) 60 | if (t[ns.link('WebPage').uri]) return 'view' 61 | 62 | if ( 63 | hasContentTypeIn(kb, subject, allowed) || 64 | hasContentTypeIn2(kb, subject, allowed) 65 | ) { 66 | return 'View' 67 | } 68 | 69 | return null 70 | }, 71 | 72 | render: function (subject, context) { 73 | const myDocument = context.dom 74 | const div = myDocument.createElement('div') 75 | const kb = context.session.store 76 | 77 | const cts = kb.fetcher.getHeader(subject.doc(), 'content-type') 78 | const ct = cts ? cts[0].split(';', 1)[0].trim() : null // remove content-type parameters 79 | if (ct) { 80 | // console.log('humanReadablePane: c-t:' + ct) 81 | } else { 82 | console.log('humanReadablePane: unknown content-type?') 83 | } 84 | 85 | // @@ When we can, use CSP to turn off scripts within the iframe 86 | div.setAttribute('class', 'docView') 87 | const element = ct === 'text/markdown' ? 'DIV' : 'IFRAME' 88 | const frame = myDocument.createElement(element) 89 | 90 | const setIframeAttributes = (frame, blob, lines) => { 91 | frame.setAttribute('src', URL.createObjectURL(blob)); 92 | frame.setAttribute('type', blob.type); 93 | frame.setAttribute('class', 'doc'); 94 | frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; width: 800px; resize: both; overflow: auto;`); 95 | 96 | // Apply sandbox attribute only for HTML files 97 | // @@ Note below - if we set ANY sandbox, then Chrome and Safari won't display it if it is PDF. 98 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe 99 | // You can't have any sandbox and allow plugins. 100 | // We could sandbox only HTML files I suppose. 101 | if (blob.type === 'text/html' || blob.type === 'application/xhtml+xml') { 102 | frame.setAttribute('sandbox', 'allow-scripts allow-same-origin'); 103 | } 104 | }; 105 | 106 | // render markdown to html 107 | const markdownHtml = function () { 108 | kb.fetcher.webOperation('GET', subject.uri).then(response => { 109 | const markdownText = response.responseText 110 | const lines = Math.min(30, markdownText.split(/\n/).length + 5) 111 | const res = marked.parse(markdownText) 112 | const clean = DOMPurify.sanitize(res) 113 | frame.innerHTML = clean 114 | frame.setAttribute('class', 'doc') 115 | frame.setAttribute('style', `border: 1px solid; padding: 1em; height: ${lines}em; width: 800px; resize: both; overflow: auto;`) 116 | }).catch(error => { 117 | console.error('Error fetching markdown content:', error) 118 | frame.innerHTML = '

Error loading content

' 119 | }) 120 | } 121 | 122 | if (ct === 'text/markdown') { 123 | markdownHtml() 124 | } else { 125 | // Fetch and process the blob 126 | kb.fetcher._fetch(subject.uri) 127 | .then(response => response.blob()) 128 | .then(blob => { 129 | const blobTextPromise = blob.type.startsWith('text') ? blob.text() : Promise.resolve('') 130 | return blobTextPromise.then(blobText => ({ blob, blobText })) 131 | }) 132 | .then(({ blob, blobText }) => { 133 | const newLines = blobText.includes(' 61 | 65 | 66 | 67 |
68 |
69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/Schedule/index.html.acl: -------------------------------------------------------------------------------- 1 | @prefix n0: . 2 | @prefix c: . 3 | @prefix n1: . 4 | 5 | <#a1> 6 | a n0:Authorization; 7 | n0:accessTo 8 | ; 9 | n0:agent 10 | c:i; 11 | n0:mode 12 | n0:Control, n0:Read, n0:Write. 13 | <#a2> 14 | a n0:Authorization; 15 | n0:accessTo 16 | ; 17 | n0:agentClass 18 | n1:Agent; 19 | n0:mode 20 | n0:Read. 21 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/Schedule/results.ttl: -------------------------------------------------------------------------------- 1 | @prefix det: . 2 | @prefix sch: . 3 | @prefix n0: . 4 | @prefix c: . 5 | @prefix ic: . 6 | 7 | det:event sch:response <#id1467291979007_response> . 8 | <#id1467291979007_response> 9 | n0:author 10 | c:i; 11 | sch:cell 12 | <#id1467291979007_0>, <#id1467291979007_1>, <#id1467291979007_2>. 13 | <#id1467291979007_0> ic:dtstart "2016-06-23"; sch:availabilty sch:Maybe . 14 | <#id1467291979007_1> ic:dtstart "2016-06-16"; sch:availabilty sch:No . 15 | <#id1467291979007_2> ic:dtstart "2016-07-22"; sch:availabilty sch:Yes . 16 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/Schedule/results.ttl.acl: -------------------------------------------------------------------------------- 1 | @prefix n0: . 2 | @prefix c: . 3 | @prefix n1: . 4 | 5 | <#a1> 6 | a n0:Authorization; 7 | n0:accessTo 8 | ; 9 | n0:agent 10 | c:i; 11 | n0:mode 12 | n0:Control, n0:Read, n0:Write. 13 | <#a2> 14 | a n0:Authorization; 15 | n0:accessTo 16 | ; 17 | n0:agentClass 18 | n1:Agent; 19 | n0:mode 20 | n0:Read, n0:Write. 21 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/SharedNotes/pad.ttl: -------------------------------------------------------------------------------- 1 | @prefix dc: . 2 | @prefix c: . 3 | @prefix XML: . 4 | @prefix p: . 5 | @prefix n: . 6 | 7 | <#thisPad> 8 | dc:author 9 | c:i; 10 | dc:created 11 | "2016-06-28T23:36:43Z"^^XML:dateTime; 12 | a p:Notepad; 13 | p:next 14 | <#id1467157173832>. 15 | <#id1467157173832> 16 | dc:author c:i; n:content "Notes"; p:indent -2; p:next <#id1467160181795>. 17 | <#id1467160181795> dc:author c:i; n:content "2"; p:next <#id1467160183715> . 18 | <#id1467160183715> dc:author c:i; n:content "3"; p:next <#id1467160184290> . 19 | <#id1467160184290> 20 | dc:author c:i; n:content "More Notes"; p:indent -1; p:next <#id1467214145291>. 21 | <#id1467214145291> dc:author c:i; n:content "5"; p:next <#id1467214147635> . 22 | <#id1467214147635> dc:author c:i; n:content "6"; p:next <#id1467304941460> . 23 | <#id1467304941460> dc:author c:i; n:content "vvgbhh"; p:next <#thisPad> . 24 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/chat/chat.ttl: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/details.ttl: -------------------------------------------------------------------------------- 1 | @prefix dc: . 2 | @prefix meeting: . 3 | @prefix p1: . 4 | @prefix det: . 5 | @prefix n0: . 6 | @prefix www: . 7 | @prefix flow: . 8 | @prefix con: . 9 | 10 | <#this> 11 | dc:title 12 | "Our first test meeting"; 13 | a meeting:Meeting; 14 | flow:attachment 15 | <../../../../solid-ui/lib/icons/noun_25830.svg>; 16 | meeting:actions 17 | con:this; 18 | meeting:chat 19 | ; 20 | meeting:toolList 21 | ( 22 | p1:thisPad 23 | det:event 24 | 25 | www: 26 | n0:8424c698-1d30-4d8e-ac47-dfe610f331ba 27 | <../../../../solid-ui/lib/icons/noun_25830.svg> 28 | con:this ); 29 | meeting:videoCallPage 30 | n0:8424c698-1d30-4d8e-ac47-dfe610f331ba. 31 | p1:thisPad dc:title "Shared notes" . 32 | dc:title "Discusssion" . 33 | det:event dc:title "Schedule" . 34 | n0:8424c698-1d30-4d8e-ac47-dfe610f331ba 35 | dc:title "Video call"; a meeting:VideoCallPage. 36 | -------------------------------------------------------------------------------- /src/meeting/test/meeting1/pad/pad.ttl: -------------------------------------------------------------------------------- 1 | @prefix dc: . 2 | @prefix c: . 3 | @prefix XML: . 4 | @prefix p: . 5 | @prefix n: . 6 | 7 | <#thisPad> 8 | dc:author 9 | c:i; 10 | dc:created 11 | "2016-06-26T15:24:36Z"^^XML:dateTime; 12 | a p:Notepad; 13 | p:next 14 | <#id1466956084891>. 15 | <#id1466956084891> dc:author c:i; n:content "Hmmmmmm"; p:next <#id1466957989616> . 16 | <#id1466957989616> dc:author c:i; n:content "..."; p:next <#thisPad> . 17 | -------------------------------------------------------------------------------- /src/microblogPane/mbStyle.css: -------------------------------------------------------------------------------- 1 | .ppane { 2 | font-family: helvetica, arial, sans-serif; 3 | color: #333; 4 | background-color: #fff; 5 | border: #999 solid 1px; 6 | min-width: 35em !important; 7 | word-wrap: break-word; 8 | } 9 | .ppane a { 10 | color: #357598; 11 | } 12 | .ppane input { 13 | border: solid 1px #333 !important; 14 | } 15 | .ppane input[type='button'], 16 | .ppane input[type='submit'] { 17 | background-color: #fff; 18 | padding: 0.1em 0.5em; 19 | border: solid 1px #333; 20 | -moz-border-radius: 3px; 21 | } 22 | .ppane input[type='button']:hover, 23 | .ppane input[type='submit']:hover { 24 | background-color: #eef; 25 | border: solid 1px #357598 !important; 26 | } 27 | .ppane ul { 28 | padding: 0px; 29 | } 30 | .ppane .update-container { 31 | background-color: #fff; 32 | margin-bottom: 2em; 33 | padding: 5px 1em; 34 | -moz-box-shadow: 0px 5px 10px #fff; 35 | } 36 | .ppane .createNewMB { 37 | padding: 5px 1em; 38 | border: solid 1px #ccc; 39 | background-color: #fff; 40 | } 41 | .ppane ul { 42 | list-style-type: none !important; 43 | } 44 | .ppane .postLink { 45 | display: inline; 46 | margin-top: 0.7em; 47 | font-size: 90%; 48 | color: #888 !important; 49 | } 50 | .ppane .userLink { 51 | display: block; 52 | clear: both; 53 | margin-bottom: 1em; 54 | margin-right: 1em; 55 | } 56 | .ppane .postAvatar { 57 | max-width: 4em; 58 | max-height: 4em; 59 | float: left; 60 | margin-right: 1em; 61 | border: double 3px #888; 62 | -moz-box-shadow: 0px 0px 8px #ccc; 63 | } 64 | 65 | /*reply view*/ 66 | .ppane .replyView { 67 | display: none; 68 | } 69 | .replyView-active .favorit { 70 | display: none; 71 | } 72 | .ppane .replyView-active { 73 | margin: 0 !important; 74 | position: fixed; 75 | left: 0px; 76 | top: 0px; 77 | background-color: #eee; 78 | opacity: 0.97; 79 | z-index: 5; 80 | width: 100%; 81 | height: 100%; 82 | padding-top: 20%; 83 | } 84 | .ppane .replyView-active li { 85 | display: block; 86 | opacity: 1 !important; 87 | margin: 1em auto; 88 | width: 30em; 89 | border: solid 1px #888; 90 | background-color: #fff; 91 | padding: 1em; 92 | -moz-box-shadow: 0px 0px 10px #aaa; 93 | } 94 | .ppane .replyView-active .closeContainer { 95 | -moz-box-shadow: none; 96 | text-align: right; 97 | vertical-align: bottom; 98 | margin-bottom: -1em; 99 | border: none; 100 | padding: 0px; 101 | background: none; 102 | } 103 | .ppane .replyView-active .closeContainer .closeButton { 104 | background-color: #000; 105 | width: 1em; 106 | font-size: 110%; 107 | margin-right: -2em; 108 | color: #fff; 109 | padding: 0.2em 0.4em; 110 | font-weight: bold; 111 | border: solid 3px #fff; 112 | -moz-border-radius: 2em; 113 | -moz-box-shadow: 0px 0px 10px #333; 114 | cursor: pointer; 115 | } 116 | 117 | /*notifications*/ 118 | .ppane .notify-container { 119 | width: 100%; 120 | } 121 | .ppane .notify-container .notify { 122 | background-color: #fffae6; 123 | border: solid 1px #ffe788; 124 | padding: 1em 0.5em; 125 | margin: 1em; 126 | -moz-border-radius: 0.5em; 127 | } 128 | .ppane .notify-container .notify.conf { 129 | background-color: #ffeeee; 130 | border-color: #880000; 131 | text-align: right; 132 | position: fixed; 133 | margin: 0px auto; 134 | left: 3em; 135 | width: 35em !important; 136 | top: 40%; 137 | -moz-box-shadow: 0px 0px 10px #800; 138 | } 139 | .ppane .notify-container .notify.conf p { 140 | text-align: left; 141 | } 142 | .ppane .notify-container .notify.conf .confirm { 143 | margin-left: 1em; 144 | } 145 | /*view*/ 146 | .ppane .view-container { 147 | display: none; 148 | padding: 1em; 149 | } 150 | .ppane .view-container.active { 151 | display: block; 152 | -moz-box-shadow: 0px 3px 8px #ccc; 153 | } 154 | 155 | /*header container*/ 156 | .ppane .header-container { 157 | border-bottom: solid 1px #999; 158 | background-color: #fafaf8; 159 | } 160 | .ppane .header-container img { 161 | float: left; 162 | max-height: 75px; 163 | margin: 1em; 164 | border: solid 3px #fff; 165 | margin-top: 0em; 166 | -moz-box-shadow: 0px 0px 8px #ccc; 167 | } 168 | .ppane .header-container .subheader-container { 169 | min-height: 75px; 170 | margin-bottom: 2em; 171 | padding: 1em; 172 | } 173 | .ppane .header-container textarea { 174 | width: 100%; 175 | margin-bottom: 0.4em; 176 | font-family: helvetica, arial, sans-serif; 177 | font-size: 100%; 178 | padding: 0.5em 0; 179 | line-height: 120%; 180 | } 181 | .ppane .header-container textarea:focus, 182 | .ppane input:focus { 183 | border: solid 1px #357598 !important; 184 | outline: none; 185 | -moz-box-shadow: 0px 0px 2px #357598; 186 | } 187 | .ppane .header-container input[type='submit'] { 188 | float: right; 189 | } 190 | .ppane .header-container .createNewMB input[type='text'] { 191 | min-width: 25em; 192 | } 193 | .ppane .header-container .createNewMB input[type='submit'] { 194 | float: none; 195 | } 196 | 197 | /*post list*/ 198 | .ppane .postList .post { 199 | margin: 0; 200 | display: block; 201 | border: solid 1px #888; 202 | padding: 1em; 203 | margin: 0.2em 0em; 204 | } 205 | .ppane .postList blockquote, 206 | .ppane .replyView-active blockquote { 207 | margin-left: 0; 208 | font-size: 110%; 209 | max-width: 30em; 210 | padding-left: 5em; 211 | } 212 | .ppane .postList .reply, 213 | .ppane .replyView-active .reply { 214 | float: right; 215 | } 216 | .ppane .postList .post div { 217 | margin: 0.5em auto; 218 | } 219 | .ppane .postList .favorit { 220 | color: #aaa; 221 | float: right; 222 | font-size: 150%; 223 | margin-left: 0.5em; 224 | } 225 | .ppane .postList .favorit.ed { 226 | color: #e5cc44; 227 | } 228 | .ppane .postList .favorit.ing { 229 | color: #777; 230 | text-shadow: 0px 0px 3px #000; 231 | } 232 | 233 | /*tabs list*/ 234 | .ppane .tabslist { 235 | padding: 0em; 236 | margin: 0.5em 0em; 237 | } 238 | .ppane .tabslist li { 239 | cursor: default; 240 | display: inline; 241 | background-color: #fff; 242 | padding: 0.5em 2em; 243 | margin: 0 0.5em; 244 | border: solid 1px #999; 245 | -moz-border-radius-topleft: 0.5em; 246 | -moz-border-radius-topright: 0.5em; 247 | } 248 | .ppane .tabslist li.active { 249 | border-bottom: solid 1px #fff; 250 | -moz-box-shadow: 0px 10px 0px #fff, 0px 0px 8px #ccc; 251 | } 252 | .ppane .tabslist li:hover { 253 | background-color: #eee; 254 | } 255 | .ppane .tabslist li.active:hover { 256 | background-color: #fff; 257 | } 258 | 259 | /*follow list*/ 260 | .ppane .followlist-container { 261 | } 262 | .ppane .followlist-container .follow { 263 | display: inline; 264 | font-size: 120%; 265 | padding: 0 2em; 266 | line-height: 150%; 267 | } 268 | -------------------------------------------------------------------------------- /src/n3Pane.js: -------------------------------------------------------------------------------- 1 | /* Notation3 content Pane 2 | ** 3 | ** This pane shows the content of a particular RDF resource 4 | ** or at least the RDF semantics we attribute to that resource, 5 | ** in generated N3 syntax. 6 | */ 7 | import * as UI from 'solid-ui' 8 | const ns = UI.ns 9 | 10 | export const n3Pane = { 11 | icon: UI.icons.originalIconBase + 'w3c/n3_smaller.png', 12 | 13 | name: 'n3', 14 | 15 | audience: [ns.solid('Developer')], 16 | 17 | label: function (subject, context) { 18 | const store = context.session.store 19 | if ( 20 | 'http://www.w3.org/2007/ont/link#ProtocolEvent' in 21 | store.findTypeURIs(subject) 22 | ) { 23 | return null 24 | } 25 | const n = store.statementsMatching(undefined, undefined, undefined, subject) 26 | .length 27 | if (n === 0) return null 28 | return 'Data (' + n + ') as N3' 29 | }, 30 | 31 | render: function (subject, context) { 32 | const myDocument = context.dom 33 | const kb = context.session.store 34 | const div = myDocument.createElement('div') 35 | div.setAttribute('class', 'n3Pane') 36 | // Because of smushing etc, this will not be a copy of the original source 37 | // We could instead either fetch and re-parse the source, 38 | // or we could keep all the pre-smushed triples. 39 | const sts = kb.statementsMatching(undefined, undefined, undefined, subject) // @@ slow with current store! 40 | /* 41 | var kludge = kb.formula([]) // No features 42 | for (var i=0; i< sts.length; i++) { 43 | s = sts[i] 44 | kludge.add(s.subject, s.predicate, s.object) 45 | } 46 | */ 47 | const sz = UI.rdf.Serializer(kb) 48 | sz.suggestNamespaces(kb.namespaces) 49 | sz.setBase(subject.uri) 50 | const str = sz.statementsToN3(sts) 51 | const pre = myDocument.createElement('PRE') 52 | pre.appendChild(myDocument.createTextNode(str)) 53 | div.appendChild(pre) 54 | return div 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/outline/context.ts: -------------------------------------------------------------------------------- 1 | import { DataBrowserContext, PaneRegistry } from 'pane-registry' 2 | import { getOutliner } from '../index' 3 | import { SolidLogic } from 'solid-logic' 4 | import { LiveStore } from 'rdflib' 5 | 6 | export function createContext ( 7 | dom: HTMLDocument, 8 | paneRegistry: PaneRegistry, 9 | store: LiveStore, 10 | logic: SolidLogic 11 | ): DataBrowserContext { 12 | return { 13 | dom, 14 | getOutliner, 15 | session: { 16 | paneRegistry, 17 | store, 18 | logic 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/outline/manager.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { TextEncoder, TextDecoder } from 'util' 4 | global.TextEncoder = TextEncoder as any 5 | global.TextDecoder = TextDecoder as any 6 | 7 | 8 | import OutlineManager from './manager' 9 | 10 | import { lit, NamedNode, sym, blankNode } from 'rdflib' 11 | import { findByText, getByText } from '@testing-library/dom' 12 | 13 | const MockPane = { 14 | render: (subject: NamedNode) => { 15 | const div = document.createElement('div') 16 | div.appendChild(document.createTextNode(`Mock Pane for ${subject.uri}`)) 17 | return div 18 | } 19 | } 20 | 21 | const mockPaneRegistry = { 22 | list: [], 23 | byName: () => MockPane 24 | } 25 | 26 | describe('manager', () => { 27 | describe('outline object td', () => { 28 | describe('for a named node', () => { 29 | let result 30 | beforeAll(() => { 31 | const table = document.createElement('table') 32 | const row = document.createElement('tr') 33 | table.appendChild(row) 34 | const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry } }) 35 | result = manager.outlineObjectTD(sym('https://namednode.example/'), null, null, null) 36 | row.appendChild(result) 37 | }) 38 | it('is a html td element', () => { 39 | expect(result.nodeName).toBe('TD') 40 | }) 41 | it('about attribute refers to node', () => { 42 | expect(result).toHaveAttribute('about', '') 43 | }) 44 | it('has class obj', () => { 45 | expect(result).toHaveClass('obj') 46 | }) 47 | it('is selectable', () => { 48 | expect(result).toHaveAttribute('notselectable', 'false') 49 | }) 50 | it('has style', () => { 51 | expect(result).toHaveStyle('margin: 0.2em; border: none; padding: 0; vertical-align: top;') 52 | }) 53 | it('shows an expand icon', () => { 54 | const img = result.firstChild 55 | expect(img.nodeName).toBe('IMG') 56 | expect(img).toHaveAttribute('src', 'https://solidos.github.io/solid-ui/src/originalIcons/tbl-expand-trans.png') 57 | }) 58 | it('shows the node label', () => { 59 | expect(result).toHaveTextContent('namednode.example') 60 | }) 61 | it('label is draggable', () => { 62 | const label = getByText(result, 'namednode.example') 63 | expect(label).toHaveAttribute('draggable', 'true') 64 | }) 65 | describe('link icon', () => { 66 | let linkIcon 67 | beforeEach(() => { 68 | const label = getByText(result, 'namednode.example') 69 | linkIcon = label.lastChild 70 | }) 71 | it('is linked to named node URI', () => { 72 | expect(linkIcon.nodeName).toBe('A') 73 | expect(linkIcon).toHaveAttribute('href', 'https://namednode.example/') 74 | }) 75 | }) 76 | describe('expanding', () => { 77 | it('renders relevant pane', async () => { 78 | const expand = result.firstChild 79 | expand.click() 80 | const error = await findByText(result.parentNode, /Mock Pane/) 81 | expect(error).toHaveTextContent('Mock Pane for https://namednode.example/') 82 | }) 83 | }) 84 | }) 85 | 86 | describe('for a tel uri', () => { 87 | let result 88 | beforeAll(() => { 89 | const manager = new OutlineManager({ dom: document }) 90 | result = manager.outlineObjectTD(sym('tel:+1-201-555-0123'), null, null, null) 91 | }) 92 | it('is a html td element', () => { 93 | expect(result.nodeName).toBe('TD') 94 | }) 95 | it('about attribute refers to tel uri', () => { 96 | expect(result).toHaveAttribute('about', '') 97 | }) 98 | it('has class obj', () => { 99 | expect(result).toHaveClass('obj') 100 | }) 101 | it('is selectable', () => { 102 | expect(result).toHaveAttribute('notselectable', 'false') 103 | }) 104 | it('has style', () => { 105 | expect(result).toHaveStyle('margin: 0.2em; border: none; padding: 0; vertical-align: top;') 106 | }) 107 | it('shows an expand icon', () => { 108 | const img = result.firstChild 109 | expect(img.nodeName).toBe('IMG') 110 | expect(img).toHaveAttribute('src', 'https://solidos.github.io/solid-ui/src/originalIcons/tbl-expand-trans.png') 111 | }) 112 | it('shows the phone number', () => { 113 | expect(result).toHaveTextContent('+1-201-555-0123') 114 | }) 115 | describe('phone link', () => { 116 | let phoneLink 117 | beforeAll(() => { 118 | const label = getByText(result, '+1-201-555-0123') 119 | phoneLink = label.lastChild 120 | }) 121 | it('is linked to tel uri', () => { 122 | expect(phoneLink.nodeName).toBe('A') 123 | expect(phoneLink).toHaveAttribute('href', 'tel:+1-201-555-0123') 124 | }) 125 | it('is represented by phone icon', () => { 126 | const phoneIcon = phoneLink.lastChild 127 | expect(phoneIcon.nodeName).toBe('IMG') 128 | expect(phoneIcon).toHaveAttribute('src', 'https://solidos.github.io/solid-ui/src/originalIcons/silk/telephone.png') 129 | }) 130 | }) 131 | }) 132 | 133 | describe('for a literal', () => { 134 | let result 135 | beforeAll(() => { 136 | const manager = new OutlineManager({ dom: document }) 137 | result = manager.outlineObjectTD(lit('some text'), null, null, null) 138 | }) 139 | it('is a html td element', () => { 140 | expect(result.nodeName).toBe('TD') 141 | }) 142 | it('has no about attribute', () => { 143 | expect(result).not.toHaveAttribute('about') 144 | }) 145 | it('has class obj', () => { 146 | expect(result).toHaveClass('obj') 147 | }) 148 | it('is selectable', () => { 149 | expect(result).toHaveAttribute('notselectable', 'false') 150 | }) 151 | it('has style', () => { 152 | expect(result).toHaveStyle('margin: 0.2em; border: none; padding: 0; vertical-align: top;') 153 | }) 154 | it('shows the literal text', () => { 155 | expect(result).toHaveTextContent('some text') 156 | }) 157 | it('literal text preserves white space', () => { 158 | const text = getByText(result, 'some text') 159 | expect(text).toHaveStyle('white-space: pre-wrap;') 160 | }) 161 | }) 162 | 163 | describe('for a blank node', () => { 164 | let result 165 | beforeAll(() => { 166 | const manager = new OutlineManager({ dom: document }) 167 | result = manager.outlineObjectTD(blankNode('blank-node'), null, null, null) 168 | }) 169 | it('is a html td element', () => { 170 | expect(result.nodeName).toBe('TD') 171 | }) 172 | it('has about attribute', () => { 173 | expect(result).toHaveAttribute('about', '_:blank-node') 174 | }) 175 | it('has class obj', () => { 176 | expect(result).toHaveClass('obj') 177 | }) 178 | it('is selectable', () => { 179 | expect(result).toHaveAttribute('notselectable', 'false') 180 | }) 181 | it('has style', () => { 182 | expect(result).toHaveStyle('margin: 0.2em; border: none; padding: 0; vertical-align: top;') 183 | }) 184 | it('shows 3 dots', () => { 185 | expect(result).toHaveTextContent('...') 186 | }) 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /src/outline/propertyViews.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import { TextEncoder, TextDecoder } from 'util' 4 | global.TextEncoder = TextEncoder as any 5 | global.TextDecoder = TextDecoder as any 6 | 7 | 8 | import { sym } from 'rdflib' 9 | import { propertyViews } from './propertyViews' 10 | 11 | describe('property views', () => { 12 | it.each([ 13 | 'http://xmlns.com/foaf/0.1/depiction', 14 | 'http://xmlns.com/foaf/0.1/img', 15 | 'http://xmlns.com/foaf/0.1/thumbnail', 16 | 'http://xmlns.com/foaf/0.1/logo', 17 | 'http://schema.org/image' 18 | ])('renders %s as image', (property) => { 19 | const views = propertyViews(document) 20 | const view = views.defaults[property] 21 | const result = view(sym('https://pod.example/img.jpg')) 22 | expect(result).toBeInstanceOf(HTMLImageElement) 23 | expect(result).toHaveAttribute('src', 'https://pod.example/img.jpg') 24 | }) 25 | 26 | it.each([ 27 | 'http://xmlns.com/foaf/0.1/mbox' 28 | ])('renders %s as anchor', (property) => { 29 | const views = propertyViews(document) 30 | const view = views.defaults[property] 31 | const result = view(sym('mailto:alice@mail.example')) 32 | expect(result).toBeInstanceOf(HTMLAnchorElement) 33 | expect(result).toHaveAttribute('href', 'mailto:alice@mail.example'); 34 | expect(result).toHaveTextContent('alice@mail.example'); 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/outline/propertyViews.ts: -------------------------------------------------------------------------------- 1 | import { ns } from 'solid-ui' 2 | import viewAsImage from './viewAsImage' 3 | import viewAsMbox from './viewAsMbox' 4 | 5 | /** some builtin simple views **/ 6 | 7 | export function propertyViews (dom) { 8 | // view that applies to items that are objects of certain properties. 9 | const views = { 10 | properties: [], 11 | defaults: [], 12 | classes: [] 13 | } // views 14 | 15 | const asImage = viewAsImage(dom) 16 | const asMbox = viewAsMbox(dom) 17 | 18 | viewsAddPropertyView(views, ns.foaf('depiction').uri, asImage, true) 19 | viewsAddPropertyView(views, ns.foaf('img').uri, asImage, true) 20 | viewsAddPropertyView(views, ns.foaf('thumbnail').uri, asImage, true) 21 | viewsAddPropertyView(views, ns.foaf('logo').uri, asImage, true) 22 | viewsAddPropertyView(views, ns.schema('image').uri, asImage, true) 23 | viewsAddPropertyView(views, ns.foaf('mbox').uri, asMbox, true) 24 | return views 25 | } 26 | 27 | /** add a property view function **/ 28 | export function viewsAddPropertyView (views, property, pviewfunc, isDefault) { 29 | if (!views.properties[property]) { 30 | views.properties[property] = [] 31 | } 32 | views.properties[property].push(pviewfunc) 33 | if (isDefault) { 34 | // will override an existing default! 35 | views.defaults[property] = pviewfunc 36 | } 37 | } // viewsAddPropertyView 38 | -------------------------------------------------------------------------------- /src/outline/viewAsImage.ts: -------------------------------------------------------------------------------- 1 | import * as UI from 'solid-ui' 2 | 3 | export default (dom) => function viewAsImage (obj) { 4 | const img = UI.utils.AJARImage( 5 | obj.uri, 6 | UI.utils.label(obj), 7 | UI.utils.label(obj), 8 | dom 9 | ) 10 | img.setAttribute('class', 'outlineImage') 11 | return img 12 | } 13 | -------------------------------------------------------------------------------- /src/outline/viewAsMbox.ts: -------------------------------------------------------------------------------- 1 | export default (dom) => function viewAsMbox (obj) { 2 | const anchor = dom.createElement('a') 3 | // previous implementation assumed email address was Literal. fixed. 4 | 5 | // FOAF mboxs must NOT be literals -- must be mailto: URIs. 6 | 7 | let address = obj.termType === 'NamedNode' ? obj.uri : obj.value // this way for now 8 | // if (!address) return viewAsBoringDefault(obj) 9 | const index = address.indexOf('mailto:') 10 | address = index >= 0 ? address.slice(index + 7) : address 11 | anchor.setAttribute('href', 'mailto:' + address) 12 | anchor.appendChild(dom.createTextNode(address)) 13 | return anchor 14 | } 15 | -------------------------------------------------------------------------------- /src/pad/images/ColourOff.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/pad/images/ColourOff.ai -------------------------------------------------------------------------------- /src/pad/images/ColourOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/pad/images/ColourOff.png -------------------------------------------------------------------------------- /src/pad/images/ColourOn.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/pad/images/ColourOn.ai -------------------------------------------------------------------------------- /src/pad/images/ColourOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/pad/images/ColourOn.png -------------------------------------------------------------------------------- /src/playlist/playlistPane.js: -------------------------------------------------------------------------------- 1 | /* Playlist Pane 2 | ** 3 | ** This pane allows playlists and playlists slots to be viewed 4 | ** seeAlso: http://smiy.sourceforge.net/pbo/spec/playbackontology.html 5 | */ 6 | import * as UI from 'solid-ui' 7 | import * as $rdf from 'rdflib' 8 | const ns = UI.ns 9 | 10 | export default { 11 | icon: UI.icons.iconBase + 'noun_1619.svg', 12 | 13 | name: 'playlistSlot', 14 | 15 | audience: [ns.solid('PowerUser')], 16 | 17 | label: function (subject, context) { 18 | const kb = context.session.store 19 | 20 | if ( 21 | !kb.anyStatementMatching( 22 | subject, 23 | UI.ns.rdf('type'), 24 | kb.sym('http://purl.org/ontology/pbo/core#PlaylistSlot') 25 | ) 26 | ) { 27 | return null 28 | } 29 | 30 | return 'playlist slot' 31 | }, 32 | 33 | render: function (subject, context) { 34 | const myDocument = context.dom 35 | function isVideo (src, _index) { 36 | if (!src) { 37 | return { 38 | html5: true 39 | } 40 | } 41 | 42 | const youtube = src.match( 43 | /\/\/(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-_%]+)/i 44 | ) 45 | const vimeo = src.match(/\/\/(?:www\.)?vimeo.com\/([0-9a-z\-_]+)/i) 46 | const dailymotion = src.match(/\/\/(?:www\.)?dai.ly\/([0-9a-z\-_]+)/i) 47 | const vk = src.match( 48 | /\/\/(?:www\.)?(?:vk\.com|vkontakte\.ru)\/(?:video_ext\.php\?)(.*)/i 49 | ) 50 | 51 | if (youtube) { 52 | return { 53 | youtube: youtube 54 | } 55 | } else if (vimeo) { 56 | return { 57 | vimeo: vimeo 58 | } 59 | } else if (dailymotion) { 60 | return { 61 | dailymotion: dailymotion 62 | } 63 | } else if (vk) { 64 | return { 65 | vk: vk 66 | } 67 | } 68 | } 69 | 70 | const link = function (contents, uri) { 71 | if (!uri) return contents 72 | const a = myDocument.createElement('a') 73 | a.setAttribute('href', uri) 74 | a.appendChild(contents) 75 | a.addEventListener('click', UI.widgets.openHrefInOutlineMode, true) 76 | return a 77 | } 78 | 79 | const text = function (str) { 80 | return myDocument.createTextNode(str) 81 | } 82 | 83 | const kb = context.session.store 84 | const obj = kb.any( 85 | subject, 86 | $rdf.sym('http://purl.org/ontology/pbo/core#playlist_item') 87 | ) 88 | let index = kb.any( 89 | subject, 90 | $rdf.sym('http://purl.org/ontology/olo/core#index') 91 | ) 92 | 93 | let uri = obj.uri 94 | const video = isVideo(uri) 95 | 96 | const div = myDocument.createElement('div') 97 | let img 98 | if (video && video.youtube) { 99 | uri = uri.replace('watch?v=', 'embed/') 100 | div.setAttribute('class', 'imageView') 101 | img = myDocument.createElement('IFRAME') 102 | img.setAttribute('src', uri) 103 | img.setAttribute('width', 560) 104 | img.setAttribute('height', 315) 105 | img.setAttribute('frameborder', 0) 106 | img.setAttribute('style', 'max-width: 850px; max-height: 100%;') 107 | img.setAttribute('allowfullscreen', 'true') 108 | } else { 109 | div.setAttribute('class', 'imageView') 110 | img = myDocument.createElement('IMG') 111 | img.setAttribute('src', obj.value) 112 | img.setAttribute('width', 560) 113 | img.setAttribute('height', 315) 114 | img.setAttribute('style', 'max-width: 560; max-height: 315;') 115 | } 116 | 117 | let descDiv 118 | if (index) { 119 | const sl = kb.statementsMatching( 120 | null, 121 | $rdf.sym('http://purl.org/ontology/olo/core#index') 122 | ) 123 | const slots = [] 124 | for (let i = 0; i < sl.length; i++) { 125 | if (sl[i]) { 126 | slots.push(parseInt(sl[i].object.value, 10)) 127 | } 128 | } 129 | 130 | index = parseInt(index.value, 10) 131 | descDiv = myDocument.createElement('div') 132 | 133 | const pIndex = 134 | slots[(slots.indexOf(index) - 1 + slots.length) % slots.length] 135 | const nIndex = 136 | slots[(slots.indexOf(index) + 1 + slots.length) % slots.length] 137 | 138 | const prev = link(text('<<'), subject.uri.split('#')[0] + '#' + pIndex) 139 | 140 | descDiv.appendChild(prev) 141 | 142 | const indexDiv = myDocument.createElement('span') 143 | indexDiv.innerHTML = ' Playlist slot : ' + index + ' ' 144 | 145 | descDiv.appendChild(indexDiv) 146 | 147 | const next = link(text('>>'), subject.uri.split('#')[0] + '#' + nIndex) 148 | descDiv.appendChild(next) 149 | } 150 | 151 | const tr = myDocument.createElement('TR') // why need tr? 152 | tr.appendChild(img) 153 | if (descDiv) { 154 | tr.appendChild(descDiv) 155 | } 156 | div.appendChild(tr) 157 | return div 158 | } 159 | } 160 | 161 | // ends 162 | -------------------------------------------------------------------------------- /src/registerPanes.js: -------------------------------------------------------------------------------- 1 | import profilePane from 'profile-pane' 2 | // import editProfileView from './profile/editProfile.view' 3 | import trustedApplications from './trustedApplications/trustedApplications.view' 4 | import dashboardPane from './dashboard/dashboardPane' 5 | import basicPreferences from './dashboard/basicPreferences' 6 | import issuePane from 'issue-pane' 7 | import contactsPane from 'contacts-pane' 8 | import activityStreamsPane from 'activitystreams-pane' 9 | import padPane from './pad/padPane' 10 | // import argumentPane from './argument/argumentPane.js' 11 | import transactionPane from './transaction/pane.js' 12 | import financialPeriodPane from './transaction/period.js' 13 | import meetingPane from 'meeting-pane' 14 | import tabbedPane from './tabbed/tabbedPane' 15 | import { longChatPane, shortChatPane } from 'chat-pane' 16 | import { schedulePane } from './schedule/schedulePane.js' 17 | // import publicationPane from './publication/publicationPane.js' 18 | import tripPane from './trip/tripPane.js' 19 | import { imagePane } from './imagePane.js' 20 | import playListPane from './playlist/playlistPane.js' 21 | import videoPane from './video/videoPane.js' 22 | import audioPane from './audio/audioPane.js' 23 | import dokieliPane from './dokieli/dokieliPane.js' 24 | import folderPane from 'folder-pane' 25 | import { classInstancePane } from './classInstancePane.js' 26 | import { slideshowPane } from './slideshow/slideshowPane.js' 27 | import { socialPane } from './socialPane.js' 28 | import humanReadablePane from './humanReadablePane.js' 29 | 30 | import { dataContentPane } from './dataContentPane.js' 31 | import sourcePane from 'source-pane' 32 | import { n3Pane } from './n3Pane.js' 33 | import { RDFXMLPane } from './RDFXMLPane.js' 34 | import { formPane } from './form/pane.js' 35 | import { tableViewPane } from './tableViewPane.js' 36 | import { defaultPane } from './defaultPane.js' 37 | import uiPane from './ui/pane.js' 38 | 39 | import sharingPane from './sharing/sharingPane' 40 | import internalPane from './internal/internalPane' 41 | 42 | import homePane from './home/homePane' 43 | 44 | export function registerPanes (register) { 45 | /* Note that the earliest panes have priority. So the most specific ones are first. 46 | ** 47 | */ 48 | // Developer designed: 49 | 50 | register(profilePane) // View someone's public profile - dominates all other panes. 51 | const editProfileView = profilePane.editor ; 52 | if (!editProfileView) { 53 | console.log("@@@ editProfileView", "profilePane is not providing an editor pane") 54 | } 55 | 56 | register(editProfileView) // Edit my profile. 57 | 58 | register(trustedApplications) // must be registered before basicPreferences 59 | register(dashboardPane) 60 | register(basicPreferences) 61 | register(issuePane) 62 | register(contactsPane) 63 | register(activityStreamsPane) 64 | 65 | register(padPane) 66 | // register(argumentPane) // A position in an argument tree 67 | 68 | register(transactionPane) 69 | register(financialPeriodPane) 70 | 71 | register(meetingPane) 72 | register(tabbedPane) 73 | 74 | register(longChatPane) // Long pane must have prio in case short pane tries to do a long pane 75 | register(shortChatPane) // was './chat/chatPane.js' 76 | 77 | // register(publicationPane) // Suppress for now 78 | 79 | register(schedulePane) // doodle poll 80 | 81 | register(tripPane) 82 | // register(require('./airPane.js')) 83 | 84 | // Content views 85 | 86 | register(imagePane) // Basic image view 87 | register(playListPane) // Basic playlist view 88 | 89 | register(videoPane) // Video clip player 90 | register(audioPane) // Audio clip player 91 | 92 | register(dokieliPane) // Should be above dataContentPane 93 | register(folderPane) // Should be above dataContentPane 94 | register(classInstancePane) // Should be above dataContentPane 95 | // register(require('./dynamic/dynamicPanes.js')) // warp etc warp broken 2017/8 96 | register(slideshowPane) 97 | 98 | register(socialPane) 99 | 100 | register(humanReadablePane) // A web page as a web page -- how to escape to tabr? 101 | // register(require('markdown-pane').Pane) // replaced by markdown in humanReadablePane 102 | 103 | register(dataContentPane) // Preferred for a data file 104 | register(sourcePane) // edit source 105 | register(n3Pane) 106 | register(RDFXMLPane) 107 | 108 | // User configured - data driven 109 | register(formPane) 110 | 111 | // Generic: 112 | 113 | register(tableViewPane) 114 | 115 | // Fallback totally generic: 116 | register(defaultPane) 117 | 118 | register(uiPane) 119 | 120 | // register(require("categoryPane.js")) // Not useful enough 121 | // register(require("pubsPane.js")) // not finished 122 | 123 | // @@ jambo commented these things out to pare things down temporarily. 124 | // Note must use // not /* to comment out to make sure expander sees it 125 | // register(require("lawPane.js")) 126 | 127 | // register(require('./microblogPane/microblogPane.js')) 128 | 129 | // register(require("./social/pane.js")) // competitor to other social 130 | // register(require("./airPane.js")) 131 | // register(require("./lawPane.js")) 132 | // register(require("pushbackPane.js")) 133 | // register(require("CVPane.js")) 134 | // register(require("photoPane.js")) 135 | // register(require("tagPane.js")) 136 | // register(require("photoImportPane.js")) 137 | 138 | // The sharing pane is fairly generic and administrative 201 139 | register(sharingPane) 140 | 141 | // The internals pane is always (almost?) the last as it is the least user-friendly 142 | register(internalPane) 143 | 144 | register(homePane) // This is a global pane 145 | 146 | // ENDS 147 | } 148 | -------------------------------------------------------------------------------- /src/schedule/Makefile: -------------------------------------------------------------------------------- 1 | # Wrap TTL files into JS files for bundling with library 2 | 3 | ,all : formsForSchedule.js 4 | 5 | formsForSchedule.js : formsForSchedule.ttl 6 | (echo 'module.exports = `' ; cat formsForSchedule.ttl; echo '`') > formsForSchedule.js 7 | -------------------------------------------------------------------------------- /src/schedule/formsForSchedule.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | @prefix dc: . 3 | @prefix foaf: . 4 | @prefix cal: . 5 | @prefix ui: . 6 | @prefix rdfs: . 7 | @prefix sched: . 8 | 9 | cal:Vevent ui:annotationForm <#form2>, <#form3>; ui:creationForm <#form1> . 10 | 11 | <#bigForm> 12 | dc:title 13 | "Schedule event (single form)."; 14 | a ui:Group; 15 | ui:parts ( <#form1> <#form2> <#form3> ). 16 | 17 | <#form1> 18 | dc:title 19 | "Schedule an event (wizard)"; 20 | a ui:Form; 21 | ui:parts ( 22 | <#form1header> 23 | <#eventTitle> 24 | <#eventLocation> 25 | <#eventType> 26 | <#eventSwitch> 27 | <#eventComment> 28 | <#eventAuthor> ). 29 | 30 | <#form1header> a ui:Heading; ui:contents "Schedule an event" . 31 | 32 | <#eventTitle> 33 | a ui:SingleLineTextField; 34 | ui:maxLength 35 | "128"; 36 | ui:property 37 | cal:summary; 38 | ui:size 39 | "40". 40 | 41 | <#eventLocation> 42 | a ui:SingleLineTextField; 43 | ui:maxLength 44 | "512"; 45 | ui:property 46 | cal:location; 47 | ui:size 48 | "40". 49 | 50 | <#eventType> a ui:BooleanField; 51 | ui:property sched:allDay; 52 | ui:default true . # Used to be the only way 53 | 54 | <#eventSwitch> a ui:Options; 55 | ui:dependingOn sched:allDay; 56 | ui:case [ ui:for true; ui:use <#AllDayForm> ]; 57 | ui:case [ ui:for false; ui:use <#NotAllDayForm> ]. 58 | 59 | <#AllDayForm> a ui:IntegerField ; 60 | ui:property sched:durationInDays; 61 | ui:label "How many days?"; 62 | ui:min 1; 63 | ui:default 1 . 64 | 65 | <#NotAllDayForm> a ui:IntegerField ; 66 | ui:property sched:durationInMinutes; 67 | ui:label "Duration (mins)"; 68 | ui:min 5; 69 | ui:default 55 . 70 | 71 | <#eventComment> 72 | a ui:MultiLineTextField; 73 | # ui:maxLength 74 | # "1048"; 75 | # ui:size 76 | # "40". 77 | ui:property 78 | cal:comment. 79 | 80 | <#eventAuthor> 81 | a ui:Multiple; ui:min 1; ui:part <#eventAuthorGroup>; ui:property dc:author. 82 | 83 | <#eventAuthorGroup> a ui:Group; ui:parts ( <#authorName> <#authorEmail> ) . 84 | <#authorName> a ui:SingleLineTextField; ui:property foaf:name . 85 | <#authorEmail> a ui:EmailField; ui:label "email"; ui:property foaf:mbox . 86 | 87 | 88 | ##################### 89 | 90 | <#form2> dc:title "Select possible days or times"; a ui:Form; 91 | ui:parts ( <#id1118132113134> <#possibleSwitch> ). 92 | 93 | <#id1118132113134> a ui:Heading; ui:contents "Time proposals" . 94 | 95 | <#possibleSwitch> a ui:Options; 96 | ui:dependingOn sched:allDay; 97 | ui:case [ ui:for true; ui:use <#AllDayForm2> ]; 98 | ui:case [ ui:for false; ui:use <#NotAllDayForm2> ]. 99 | 100 | <#AllDayForm2> 101 | a ui:Multiple; ui:min 2; ui:part <#posssibleDate>; ui:property sched:option. 102 | <#posssibleDate> a ui:DateField; ui:property cal:dtstart; ui:label "date". 103 | 104 | <#NotAllDayForm2> 105 | a ui:Multiple; ui:min 2; ui:part <#posssibleTime>; ui:property sched:option. 106 | <#posssibleTime> a ui:DateTimeField; ui:property cal:dtstart; ui:label "date and time". 107 | 108 | ################################## 109 | 110 | <#form3> dc:title "Select invited people"; a ui:Form; ui:parts ( <#id1118132113135> <#id1417314641301b> ) . 111 | <#id1417314641301b> 112 | a ui:Multiple; ui:min 1; ui:part <#id1417314674292b>; ui:property sched:invitee. 113 | <#id1417314674292b> a ui:EmailField; ui:label "email"; ui:property foaf:mbox . 114 | <#id1118132113135> a ui:Heading; ui:contents "Who to invite" . 115 | 116 | # ENDS 117 | ` 118 | -------------------------------------------------------------------------------- /src/schedule/formsForSchedule.ttl: -------------------------------------------------------------------------------- 1 | @prefix dc: . 2 | @prefix foaf: . 3 | @prefix cal: . 4 | @prefix ui: . 5 | @prefix rdfs: . 6 | @prefix sched: . 7 | 8 | cal:Vevent ui:annotationForm <#form2>, <#form3>; ui:creationForm <#form1> . 9 | 10 | <#bigForm> 11 | dc:title 12 | "Schedule event (single form)."; 13 | a ui:Group; 14 | ui:parts ( <#form1> <#form2> <#form3> ). 15 | 16 | <#form1> 17 | dc:title 18 | "Schedule an event (wizard)"; 19 | a ui:Form; 20 | ui:parts ( 21 | <#form1header> 22 | <#eventTitle> 23 | <#eventLocation> 24 | <#eventType> 25 | <#eventSwitch> 26 | <#eventComment> 27 | <#eventAuthor> ). 28 | 29 | <#form1header> a ui:Heading; ui:contents "Schedule an event" . 30 | 31 | <#eventTitle> 32 | a ui:SingleLineTextField; 33 | ui:maxLength 34 | "128"; 35 | ui:property 36 | cal:summary; 37 | ui:size 38 | "40". 39 | 40 | <#eventLocation> 41 | a ui:SingleLineTextField; 42 | ui:maxLength 43 | "512"; 44 | ui:property 45 | cal:location; 46 | ui:size 47 | "40". 48 | 49 | <#eventType> a ui:BooleanField; 50 | ui:property sched:allDay; 51 | ui:default true . # Used to be the only way 52 | 53 | <#eventSwitch> a ui:Options; 54 | ui:dependingOn sched:allDay; 55 | ui:case [ ui:for true; ui:use <#AllDayForm> ]; 56 | ui:case [ ui:for false; ui:use <#NotAllDayForm> ]. 57 | 58 | <#AllDayForm> a ui:IntegerField ; 59 | ui:property sched:durationInDays; 60 | ui:label "How many days?"; 61 | ui:min 1; 62 | ui:default 1 . 63 | 64 | <#NotAllDayForm> a ui:IntegerField ; 65 | ui:property sched:durationInMinutes; 66 | ui:label "Duration (mins)"; 67 | ui:min 5; 68 | ui:default 55 . 69 | 70 | <#eventComment> 71 | a ui:MultiLineTextField; 72 | # ui:maxLength 73 | # "1048"; 74 | # ui:size 75 | # "40". 76 | ui:property 77 | cal:comment. 78 | 79 | <#eventAuthor> 80 | a ui:Multiple; ui:min 1; ui:part <#eventAuthorGroup>; ui:property dc:author. 81 | 82 | <#eventAuthorGroup> a ui:Group; ui:parts ( <#authorName> <#authorEmail> ) . 83 | <#authorName> a ui:SingleLineTextField; ui:property foaf:name . 84 | <#authorEmail> a ui:EmailField; ui:label "email"; ui:property foaf:mbox . 85 | 86 | 87 | ##################### 88 | 89 | <#form2> dc:title "Select possible days or times"; a ui:Form; 90 | ui:parts ( <#id1118132113134> <#possibleSwitch> ). 91 | 92 | <#id1118132113134> a ui:Heading; ui:contents "Time proposals" . 93 | 94 | <#possibleSwitch> a ui:Options; 95 | ui:dependingOn sched:allDay; 96 | ui:case [ ui:for true; ui:use <#AllDayForm2> ]; 97 | ui:case [ ui:for false; ui:use <#NotAllDayForm2> ]. 98 | 99 | <#AllDayForm2> 100 | a ui:Multiple; ui:min 2; ui:part <#posssibleDate>; ui:property sched:option. 101 | <#posssibleDate> a ui:DateField; ui:property cal:dtstart; ui:label "date". 102 | 103 | <#NotAllDayForm2> 104 | a ui:Multiple; ui:min 2; ui:part <#posssibleTime>; ui:property sched:option. 105 | <#posssibleTime> a ui:DateTimeField; ui:property cal:dtstart; ui:label "date and time". 106 | 107 | ################################## 108 | 109 | <#form3> dc:title "Select invited people"; a ui:Form; ui:parts ( <#id1118132113135> <#id1417314641301b> ) . 110 | <#id1417314641301b> 111 | a ui:Multiple; ui:min 1; ui:part <#id1417314674292b>; ui:property sched:invitee. 112 | <#id1417314674292b> a ui:EmailField; ui:label "email"; ui:property foaf:mbox . 113 | <#id1118132113135> a ui:Heading; ui:contents "Who to invite" . 114 | 115 | # ENDS 116 | -------------------------------------------------------------------------------- /src/sharing/sharingPane.ts: -------------------------------------------------------------------------------- 1 | /* Sharing Pane 2 | ** 3 | ** This outline pane allows a user to view and adjust the sharing -- access control lists 4 | ** for anything which has that capability. 5 | ** 6 | ** I am using in places single quotes strings like 'this' 7 | ** where internationalization ("i18n") is not a problem, and double quoted 8 | ** like "this" where the string is seen by the user and so I18n is an issue. 9 | */ 10 | 11 | import { aclControl, icons, ns } from 'solid-ui' 12 | 13 | const sharingPane = { 14 | icon: icons.iconBase + 'padlock-timbl.svg', 15 | 16 | name: 'sharing', 17 | 18 | label: (subject, context) => { 19 | const store = context.session.store 20 | const t = store.findTypeURIs(subject) 21 | if (t[ns.ldp('Resource').uri]) return 'Sharing' // @@ be more sophisticated? 22 | if (t[ns.ldp('Container').uri]) return 'Sharing' // @@ be more sophisticated? 23 | if (t[ns.ldp('BasicContainer').uri]) return 'Sharing' // @@ be more sophisticated? 24 | // check being allowed to see/change sharing? 25 | return null // No under other circumstances 26 | }, 27 | 28 | render: (subject, context) => { 29 | const dom = context.dom 30 | const store = context.session.store 31 | const noun = getNoun() 32 | 33 | const div = dom.createElement('div') 34 | div.classList.add('sharingPane') 35 | aclControl.preventBrowserDropEvents(dom) 36 | div.appendChild(aclControl.ACLControlBox5(subject, context, noun, store)) 37 | return div 38 | 39 | function getNoun () { 40 | const t = store.findTypeURIs(subject) 41 | if (t[ns.ldp('BasicContainer').uri] || t[ns.ldp('Container').uri]) { 42 | return 'folder' 43 | } 44 | return 'file' 45 | } 46 | } 47 | } 48 | 49 | export default sharingPane 50 | -------------------------------------------------------------------------------- /src/slideshow/slideshowPane.js: -------------------------------------------------------------------------------- 1 | /* slideshow Pane 2 | ** 3 | */ 4 | import * as UI from 'solid-ui' 5 | const ns = UI.ns 6 | 7 | import makeBSS from '@solid/better-simple-slideshow' 8 | 9 | export const slideshowPane = { 10 | icon: UI.icons.iconBase + 'noun_138712.svg', 11 | 12 | name: 'slideshow', 13 | 14 | audience: [ns.solid('PowerUser')], 15 | 16 | // Does the subject deserve an slideshow pane? 17 | label: function (subject, context) { 18 | const store = context.session.store 19 | const ns = UI.ns 20 | const t = store.findTypeURIs(subject) 21 | if (t[ns.ldp('Container').uri] || t[ns.ldp('BasicContainer').uri]) { 22 | const contents = store.each(subject, ns.ldp('contains')) 23 | let count = 0 24 | contents.forEach(function (file) { 25 | if (UI.widgets.isImage(file)) count++ 26 | }) 27 | return count > 0 ? 'Slideshow' : null 28 | } 29 | return null 30 | }, 31 | 32 | // See https://github.com/leemark/better-simple-slideshow 33 | // and follow instructions there 34 | render: function (subject, context) { 35 | const dom = context.dom 36 | const styleSheet = 37 | 'https://leemark.github.io/better-simple-slideshow/css/simple-slideshow-styles.css' 38 | UI.widgets.addStyleSheet(dom, styleSheet) 39 | 40 | const store = context.session.store 41 | const ns = UI.ns 42 | const div = dom.createElement('div') 43 | div.setAttribute('class', 'bss-slides') 44 | 45 | const t = store.findTypeURIs(subject) 46 | let predicate 47 | if (t[ns.ldp('BasicContainer').uri] || t[ns.ldp('Container').uri]) { 48 | predicate = ns.ldp('contains') 49 | } 50 | const images = store.each(subject, predicate) // @@ random order? 51 | // @@ Ideally: sort by embedded time of image 52 | images.sort() // Sort for now by URI 53 | for (let i = 0; i < images.length; i++) { 54 | if (!UI.widgets.isImage(images[i])) continue 55 | const figure = div.appendChild(dom.createElement('figure')) 56 | const img = figure.appendChild(dom.createElement('img')) 57 | 58 | // get image with authenticated fetch 59 | store.fetcher._fetch(images[i].uri) 60 | .then(function(response) { 61 | return response.blob() 62 | }) 63 | .then(function(myBlob) { 64 | const objectURL = URL.createObjectURL(myBlob) 65 | img.setAttribute('src', objectURL) // w640 h480 // 66 | }) 67 | img.setAttribute('width', '100%') 68 | figure.appendChild(dom.createElement('figcaption')) 69 | } 70 | const options = { dom: dom } 71 | 72 | setTimeout(function () { 73 | makeBSS('.bss-slides', options) 74 | }, 1000) // Must run after the code which called this 75 | 76 | return div 77 | } 78 | } 79 | 80 | // ends 81 | -------------------------------------------------------------------------------- /src/tabbed/tabbedPane.ts: -------------------------------------------------------------------------------- 1 | /* Tabbed view of anything 2 | ** 3 | ** data-driven 4 | ** 5 | */ 6 | import { Store } from 'rdflib' 7 | import { PaneDefinition } from 'pane-registry' 8 | import { icons, ns, tabs, widgets } from 'solid-ui' 9 | 10 | const TabbedPane: PaneDefinition = { 11 | icon: icons.iconBase + 'noun_688606.svg', 12 | 13 | name: 'tabbed', 14 | 15 | audience: [ns.solid('PowerUser')], 16 | 17 | // Does the subject deserve this pane? 18 | label: (subject, context) => { 19 | const kb = context.session.store as Store 20 | const typeURIs = kb.findTypeURIs(subject) 21 | if (ns.meeting('Cluster').uri in typeURIs) { 22 | return 'Tabbed' 23 | } 24 | return null 25 | }, 26 | 27 | render: (subject, context) => { 28 | const dom = context.dom 29 | const store = context.session.store as Store 30 | const div = dom.createElement('div') 31 | 32 | ;(async () => { 33 | if (!store.fetcher) { 34 | throw new Error('Store has no fetcher') 35 | } 36 | await store.fetcher.load(subject) 37 | 38 | div.appendChild(tabs.tabWidget({ 39 | dom, 40 | subject, 41 | predicate: store.any(subject, ns.meeting('predicate')) || ns.meeting('toolList'), 42 | ordered: true, 43 | orientation: ((store as Store).anyValue(subject, ns.meeting('orientation')) || '0') as ('0' | '1' | '2' | '3'), 44 | renderMain: (containerDiv, item) => { 45 | containerDiv.innerHTML = '' 46 | const table = containerDiv.appendChild(context.dom.createElement('table')) 47 | ;(context.getOutliner(context.dom) as any).GotoSubject(item, true, null, false, undefined, table) 48 | }, 49 | renderTab: (containerDiv, item) => { 50 | const predicate = store.the(subject, ns.meeting('predicate')) 51 | containerDiv.appendChild(widgets.personTR(context.dom, predicate, item, {})) 52 | }, 53 | backgroundColor: store.anyValue(subject, ns.ui('backgroundColor')) || '#ddddcc' 54 | })) 55 | })() 56 | return div 57 | } 58 | } 59 | 60 | export default TabbedPane 61 | -------------------------------------------------------------------------------- /src/tableViewPane.js: -------------------------------------------------------------------------------- 1 | // Format an array of RDF statements as an HTML table. 2 | // 3 | // This can operate in one of three modes: when the class of object is given 4 | // or when the source document from which data is taken is given, 5 | // or if a prepared query object is given. 6 | // (In principle it could operate with neither class nor document 7 | // given but typically 8 | // there would be too much data.) 9 | // When the tableClass is not given, it looks for common classes in the data, 10 | // and gives the user the option. 11 | // 12 | // 2008 Written, Ilaria Liccardi 13 | // 2014 core functionality now in common/table.js -timbl 14 | 15 | // /////////////////////////////////////////////////////////////////// 16 | 17 | // Table view pane -- view of a class as a table of properties of class members 18 | import * as UI from 'solid-ui' 19 | 20 | export const tableViewPane = { 21 | icon: UI.icons.originalIconBase + 'table.png', 22 | 23 | name: 'tableOfClass', 24 | 25 | label: function (subject, context) { 26 | const store = context.session.store 27 | // if (!store.holds(subject, UI.ns.rdf('type'),UI.ns.rdfs('Class'))) return null 28 | if (!store.any(undefined, UI.ns.rdf('type'), subject)) { 29 | return null 30 | } 31 | const n = store.statementsMatching(undefined, UI.ns.rdf('type'), subject) 32 | .length 33 | if (n === 0) { 34 | // None, suppress pane 35 | return null 36 | } 37 | if (n > 15) { 38 | // @@ At the moment this pane can be slow with too many @@ fixme by using limits 39 | return null 40 | } 41 | return UI.utils.label(subject) + ' table' 42 | }, 43 | 44 | render: function (subject, context) { 45 | const myDocument = context.dom 46 | const div = myDocument.createElement('div') 47 | div.setAttribute('class', 'tablePane') 48 | div.appendChild(UI.table(myDocument, { tableClass: subject })) 49 | return div 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test-import-export/common.js: -------------------------------------------------------------------------------- 1 | console.log('This is common.js') 2 | // https://2ality.com/2019/04/nodejs-esm-impl.html#importing-commonjs-from-esm 3 | 4 | export default { 5 | foo: 123 6 | } 7 | -------------------------------------------------------------------------------- /src/test-import-export/edit-importer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Suspect this is unsed test code 3 | 4 | /* Profile Editing Appp Pane 5 | ** 6 | ** Unlike view panes, this is available any place whatever the real subject, 7 | ** and allows the user to edit their own profile. 8 | ** 9 | ** Usage: paneRegistry.register('profile/profilePane') 10 | ** or standalone script adding onto existing mashlib. 11 | */ 12 | 13 | // import UI from 'solid-ui' 14 | // import solidUi, { SolidUi } from 'solid-ui' 15 | // @@ TODO: Write away the need for exception on next line 16 | // eslint-disable-next-line camelcase 17 | 18 | const thisPane = { foo: 'bar' } 19 | export default thisPane 20 | // ENDS 21 | -------------------------------------------------------------------------------- /src/test-import-export/testImportExport.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/test-import-export/testImportExport.js -------------------------------------------------------------------------------- /src/transaction/068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/transaction/068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png -------------------------------------------------------------------------------- /src/transaction/075988-3d-transparent-glass-icon-business-currency-british-pound-sc35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/transaction/075988-3d-transparent-glass-icon-business-currency-british-pound-sc35.png -------------------------------------------------------------------------------- /src/transaction/22-pixel-068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/transaction/22-pixel-068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png -------------------------------------------------------------------------------- /src/transaction/thumbs_075987-3d-transparent-glass-icon-business-creditcard2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/transaction/thumbs_075987-3d-transparent-glass-icon-business-creditcard2.png -------------------------------------------------------------------------------- /src/transaction/thumbs_075989-3d-transparent-glass-icon-business-currency-cent-sc35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/transaction/thumbs_075989-3d-transparent-glass-icon-business-currency-cent-sc35.png -------------------------------------------------------------------------------- /src/trip/tripPane.js: -------------------------------------------------------------------------------- 1 | /* Trip Pane 2 | ** 3 | ** This pane deals with trips themselves and also 4 | ** will look at transactions organized by trip. 5 | ** 6 | ** This outline pane allows a user to interact with a transaction 7 | ** downloaded from a bank statement, annotting it with classes and comments, 8 | ** trips, etc 9 | */ 10 | 11 | import * as UI from 'solid-ui' 12 | import * as $rdf from 'rdflib' 13 | const ns = UI.ns 14 | 15 | export default { 16 | icon: UI.icons.iconBase + 'noun_62007.svg', 17 | 18 | name: 'travel expenses', 19 | 20 | audience: [ns.solid('PowerUser')], 21 | 22 | // Does the subject deserve this pane? 23 | label: function (subject, context) { 24 | const kb = context.session.store 25 | const t = kb.findTypeURIs(subject) 26 | 27 | // if (t['http://www.w3.org/2000/10/swap/pim/qif#Transaction']) return "$$"; 28 | // if(kb.any(subject, UI.ns.qu('amount'))) return "$$$"; // In case schema not picked up 29 | 30 | if (UI.ns.qu('Transaction') in kb.findSuperClassesNT(subject)) { 31 | return 'by Trip' 32 | } 33 | if (t['http://www.w3.org/ns/pim/trip#Trip']) return 'Trip $' 34 | 35 | return null // No under other circumstances (while testing at least!) 36 | }, 37 | 38 | render: function (subject, context) { 39 | const myDocument = context.dom 40 | const kb = context.store 41 | const ns = UI.ns 42 | const CAL = $rdf.Namespace('http://www.w3.org/2002/12/cal/ical#') 43 | const TRIP = $rdf.Namespace('http://www.w3.org/ns/pim/trip#') 44 | 45 | const div = myDocument.createElement('div') 46 | div.setAttribute('class', 'transactionPane') 47 | div.innerHTML = '

Trip transactions

' 48 | 49 | const complain = function complain (message, style) { 50 | if (style === undefined) style = 'color: grey' 51 | const pre = myDocument.createElement('pre') 52 | pre.setAttribute('style', style) 53 | div.appendChild(pre) 54 | pre.appendChild(myDocument.createTextNode(message)) 55 | } 56 | 57 | // ////////////////////////////////////////////////////////////////////////////// 58 | // 59 | // Body of trip pane 60 | 61 | const t = kb.findTypeURIs(subject) 62 | 63 | // var me = authn.currentUser() 64 | 65 | // Function: Render a single trip 66 | 67 | const renderTrip = function renderTrip (subject, thisDiv) { 68 | const query = new $rdf.Query(UI.utils.label(subject)) 69 | const vars = ['date', 'transaction', 'comment', 'type', 'in_USD'] 70 | const v = {} 71 | vars.forEach(function (x) { 72 | query.vars.push((v[x] = $rdf.variable(x))) 73 | }) // Only used by UI 74 | query.pat.add(v.transaction, TRIP('trip'), subject) 75 | 76 | const opt = kb.formula() 77 | opt.add(v.transaction, ns.rdf('type'), v.type) // Issue: this will get stored supertypes too 78 | query.pat.optional.push(opt) 79 | 80 | query.pat.add(v.transaction, UI.ns.qu('date'), v.date) 81 | 82 | opt.add(v.transaction, ns.rdfs('comment'), v.comment) 83 | query.pat.optional.push(opt) 84 | 85 | // opt = kb.formula(); 86 | query.pat.add(v.transaction, UI.ns.qu('in_USD'), v.in_USD) 87 | // query.pat.optional.push(opt); 88 | 89 | const calculations = function () { 90 | const total = {} 91 | const trans = kb.each(undefined, TRIP('trip'), subject) 92 | // complain("@@ Number of transactions in this trip: " + trans.length); 93 | trans.forEach(function (t) { 94 | let ty = kb.the(t, ns.rdf('type')) 95 | // complain(" -- one trans: "+t.uri + ' -> '+kb.any(t, UI.ns.qu('in_USD'))); 96 | if (!ty) ty = UI.ns.qu('ErrorNoType') 97 | if (ty && ty.uri) { 98 | const tyuri = ty.uri 99 | if (!total[tyuri]) total[tyuri] = 0.0 100 | const lit = kb.any(t, UI.ns.qu('in_USD')) 101 | if (!lit) { 102 | complain(' @@ No amount in USD: ' + lit + ' for ' + t) 103 | } 104 | if (lit) { 105 | total[tyuri] = total[tyuri] + parseFloat(lit.value) 106 | // complain(' Trans type ='+ty+'; in_USD "' + lit 107 | // +'; total[tyuri] = '+total[tyuri]+';') 108 | } 109 | } 110 | }) 111 | let str = '' 112 | let types = 0 113 | let grandTotal = 0.0 114 | for (const uri in total) { 115 | str += UI.utils.label(kb.sym(uri)) + ': ' + total[uri] + '; ' 116 | types++ 117 | grandTotal += total[uri] 118 | } 119 | complain('Totals of ' + trans.length + ' transactions: ' + str, '') // @@@@@ yucky -- need 2 col table 120 | if (types > 1) { 121 | complain('Overall net: ' + grandTotal, 'text-treatment: bold;') 122 | } 123 | } 124 | const tableDiv = UI.table(myDocument, { 125 | query: query, 126 | onDone: calculations 127 | }) 128 | thisDiv.appendChild(tableDiv) 129 | } 130 | 131 | // Render the set of trips which have transactions in this class 132 | 133 | if (UI.ns.qu('Transaction') in kb.findSuperClassesNT(subject)) { 134 | const ts = kb.each(undefined, ns.rdf('type'), subject) 135 | const triples = [] 136 | const index = [] 137 | for (let i = 0; i < ts.length; i++) { 138 | const trans = ts[i] 139 | const trip = kb.any(trans, TRIP('trip')) 140 | if (!trip) { 141 | triples.push(trans) 142 | } else { 143 | if (!(trans in index)) index[trip] = { total: 0, transactions: [] } 144 | const usd = kb.any(trans, UI.ns.qu('in_USD')) 145 | if (usd) index[trip].total += usd 146 | const date = kb.any(trans, UI.ns.qu('date')) 147 | index[trip.toNT()].transactions.push([date, trans]) 148 | } 149 | } 150 | /* var byDate = function(a,b) { 151 | return new Date(kb.any(a, CAL('dtstart'))) - 152 | new Date(kb.any(b, CAL('dtstart'))); 153 | } 154 | */ 155 | const list = [] 156 | for (const h1 in index) { 157 | const t1 = kb.fromNT(h1) 158 | list.push([kb.any(t1, CAL('dtstart')), t1]) 159 | } 160 | list.sort() 161 | for (let j = 0; j < list.length; j++) { 162 | const t2 = list[j][1] 163 | renderTrip(t2, div) 164 | } 165 | 166 | // Render a single trip 167 | } else if (t['http://www.w3.org/ns/pim/trip#Trip']) { 168 | renderTrip(subject, div) 169 | } 170 | 171 | // if (!me) complain("You do not have your Web Id set. Set your Web ID to make changes."); 172 | 173 | return div 174 | } 175 | } 176 | // ends 177 | -------------------------------------------------------------------------------- /src/trustedApplications/__snapshots__/trustedApplications.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getStatementsToAdd should return all required statements to add the given permissions for a given origin 1`] = ` 4 | [ 5 | Statement { 6 | "graph": NamedNode { 7 | "classOrder": 5, 8 | "termType": "NamedNode", 9 | "value": "https://profile.example", 10 | }, 11 | "object": BlankNode { 12 | "classOrder": 6, 13 | "isBlank": 1, 14 | "isVar": 1, 15 | "termType": "BlankNode", 16 | "value": "bn_mock_app_id", 17 | }, 18 | "predicate": NamedNode { 19 | "classOrder": 5, 20 | "termType": "NamedNode", 21 | "value": "http://www.w3.org/ns/auth/acl#trustedApp", 22 | }, 23 | "subject": NamedNode { 24 | "classOrder": 5, 25 | "termType": "NamedNode", 26 | "value": "https://profile.example#me", 27 | }, 28 | }, 29 | Statement { 30 | "graph": NamedNode { 31 | "classOrder": 5, 32 | "termType": "NamedNode", 33 | "value": "https://profile.example", 34 | }, 35 | "object": NamedNode { 36 | "classOrder": 5, 37 | "termType": "NamedNode", 38 | "value": "https://origin.example", 39 | }, 40 | "predicate": NamedNode { 41 | "classOrder": 5, 42 | "termType": "NamedNode", 43 | "value": "http://www.w3.org/ns/auth/acl#origin", 44 | }, 45 | "subject": BlankNode { 46 | "classOrder": 6, 47 | "isBlank": 1, 48 | "isVar": 1, 49 | "termType": "BlankNode", 50 | "value": "bn_mock_app_id", 51 | }, 52 | }, 53 | Statement { 54 | "graph": NamedNode { 55 | "classOrder": 5, 56 | "termType": "NamedNode", 57 | "value": "https://profile.example", 58 | }, 59 | "object": NamedNode { 60 | "classOrder": 5, 61 | "termType": "NamedNode", 62 | "value": "http://www.w3.org/ns/auth/acl#Read", 63 | }, 64 | "predicate": NamedNode { 65 | "classOrder": 5, 66 | "termType": "NamedNode", 67 | "value": "http://www.w3.org/ns/auth/acl#mode", 68 | }, 69 | "subject": BlankNode { 70 | "classOrder": 6, 71 | "isBlank": 1, 72 | "isVar": 1, 73 | "termType": "BlankNode", 74 | "value": "bn_mock_app_id", 75 | }, 76 | }, 77 | Statement { 78 | "graph": NamedNode { 79 | "classOrder": 5, 80 | "termType": "NamedNode", 81 | "value": "https://profile.example", 82 | }, 83 | "object": NamedNode { 84 | "classOrder": 5, 85 | "termType": "NamedNode", 86 | "value": "http://www.w3.org/ns/auth/acl#Write", 87 | }, 88 | "predicate": NamedNode { 89 | "classOrder": 5, 90 | "termType": "NamedNode", 91 | "value": "http://www.w3.org/ns/auth/acl#mode", 92 | }, 93 | "subject": BlankNode { 94 | "classOrder": 6, 95 | "isBlank": 1, 96 | "isVar": 1, 97 | "termType": "BlankNode", 98 | "value": "bn_mock_app_id", 99 | }, 100 | }, 101 | ] 102 | `; 103 | 104 | exports[`getStatementsToDelete should return all statements for the given origin 1`] = ` 105 | [ 106 | Statement { 107 | "graph": DefaultGraph { 108 | "classOrder": undefined, 109 | "termType": "DefaultGraph", 110 | "uri": "chrome:theSession", 111 | "value": "", 112 | }, 113 | "object": NamedNode { 114 | "classOrder": 5, 115 | "termType": "NamedNode", 116 | "value": "https://app.example", 117 | }, 118 | "predicate": NamedNode { 119 | "classOrder": 5, 120 | "termType": "NamedNode", 121 | "value": "http://www.w3.org/ns/auth/acl#trustedApp", 122 | }, 123 | "subject": NamedNode { 124 | "classOrder": 5, 125 | "termType": "NamedNode", 126 | "value": "https://profile.example#me", 127 | }, 128 | }, 129 | Statement { 130 | "graph": DefaultGraph { 131 | "classOrder": undefined, 132 | "termType": "DefaultGraph", 133 | "uri": "chrome:theSession", 134 | "value": "", 135 | }, 136 | "object": NamedNode { 137 | "classOrder": 5, 138 | "termType": "NamedNode", 139 | "value": "https://origin.example", 140 | }, 141 | "predicate": NamedNode { 142 | "classOrder": 5, 143 | "termType": "NamedNode", 144 | "value": "http://www.w3.org/ns/auth/acl#origin", 145 | }, 146 | "subject": NamedNode { 147 | "classOrder": 5, 148 | "termType": "NamedNode", 149 | "value": "https://app.example", 150 | }, 151 | }, 152 | Statement { 153 | "graph": DefaultGraph { 154 | "classOrder": undefined, 155 | "termType": "DefaultGraph", 156 | "uri": "chrome:theSession", 157 | "value": "", 158 | }, 159 | "object": NamedNode { 160 | "classOrder": 5, 161 | "termType": "NamedNode", 162 | "value": "http://www.w3.org/ns/auth/acl#Read", 163 | }, 164 | "predicate": NamedNode { 165 | "classOrder": 5, 166 | "termType": "NamedNode", 167 | "value": "http://www.w3.org/ns/auth/acl#mode", 168 | }, 169 | "subject": NamedNode { 170 | "classOrder": 5, 171 | "termType": "NamedNode", 172 | "value": "https://app.example", 173 | }, 174 | }, 175 | ] 176 | `; 177 | -------------------------------------------------------------------------------- /src/trustedApplications/trustedApplications.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { TextEncoder, TextDecoder } from 'util' 3 | global.TextEncoder = TextEncoder as any 4 | global.TextDecoder = TextDecoder as any 5 | 6 | import * as $rdf from 'rdflib' 7 | import solidNamespace from 'solid-namespace' 8 | import { generateRandomString, getStatementsToDelete, getStatementsToAdd } from './trustedApplications.utils' 9 | 10 | const ns = solidNamespace($rdf) 11 | 12 | describe('generateRandomString', () => { 13 | it('generates a random string five characters long', () => { 14 | expect(generateRandomString().length).toBe(5) 15 | }) 16 | }) 17 | 18 | describe('getStatementsToDelete', () => { 19 | it('should return an empty array when there are no statements', () => { 20 | const mockStore = $rdf.graph() 21 | const mockOrigin = $rdf.sym('https://origin.example') 22 | const mockProfile = $rdf.sym('https://profile.example#me') 23 | expect( 24 | getStatementsToDelete(mockOrigin, mockProfile, mockStore, ns) 25 | ).toEqual([]) 26 | }) 27 | 28 | it('should return all statements for the given origin', () => { 29 | const mockStore = $rdf.graph() 30 | const mockApplication = $rdf.sym('https://app.example') 31 | const mockOrigin = $rdf.sym('https://origin.example') 32 | const mockProfile = $rdf.sym('https://profile.example#me') 33 | mockStore.add(mockApplication, ns.acl('origin'), mockOrigin) 34 | mockStore.add(mockApplication, ns.acl('mode'), ns.acl('Read')) 35 | mockStore.add(mockProfile, ns.acl('trustedApp'), mockApplication) 36 | const statementsToDelete = getStatementsToDelete( 37 | mockOrigin, 38 | mockProfile, 39 | mockStore, 40 | ns 41 | ) 42 | expect(statementsToDelete.length).toBe(3) 43 | expect(statementsToDelete).toMatchSnapshot() 44 | }) 45 | 46 | it('should not return statements for a different origin', () => { 47 | const mockStore = $rdf.graph() 48 | const mockApplication = $rdf.sym('https://app.example') 49 | const mockOrigin = $rdf.sym('https://origin.example') 50 | const mockProfile = $rdf.sym('https://profile.example#me') 51 | mockStore.add(mockApplication, ns.acl('origin'), mockOrigin) 52 | mockStore.add(mockApplication, ns.acl('mode'), ns.acl('Read')) 53 | mockStore.add(mockProfile, ns.acl('trustedApp'), mockApplication) 54 | 55 | const statementsToDelete = getStatementsToDelete( 56 | $rdf.lit('A different origin'), // @@ TODO Remove casting 57 | mockProfile, 58 | mockStore, 59 | ns 60 | ) 61 | expect(statementsToDelete.length).toBe(0) 62 | expect(statementsToDelete).toEqual([]) 63 | }) 64 | }) 65 | 66 | describe('getStatementsToAdd', () => { 67 | it('should return all required statements to add the given permissions for a given origin', () => { 68 | const mockOrigin = $rdf.sym('https://origin.example') 69 | const mockProfile = $rdf.sym('https://profile.example#me') 70 | const modes = [ns.acl('Read').value, ns.acl('Write').value] 71 | 72 | const statementsToAdd = getStatementsToAdd( 73 | mockOrigin, 74 | 'mock_app_id', 75 | modes, 76 | mockProfile, 77 | ns 78 | ) 79 | expect(statementsToAdd.length).toBe(4) 80 | expect(statementsToAdd).toMatchSnapshot() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/trustedApplications/trustedApplications.utils.ts: -------------------------------------------------------------------------------- 1 | import { BlankNode, IndexedFormula, NamedNode, st, Literal, sym, Statement } from 'rdflib' 2 | import { Namespaces } from 'solid-namespace' 3 | 4 | export function getStatementsToDelete ( 5 | origin: NamedNode | Literal, 6 | person: NamedNode, 7 | kb: IndexedFormula, 8 | ns: Namespaces 9 | ): any { 10 | const applicationStatements = kb.statementsMatching( 11 | null, 12 | ns.acl('origin'), 13 | origin 14 | ) 15 | const statementsToDelete = applicationStatements.reduce( 16 | (memo, st) => { 17 | return memo 18 | .concat( 19 | kb.statementsMatching( 20 | person, 21 | ns.acl('trustedApp'), 22 | st.subject as NamedNode 23 | ) 24 | ) 25 | .concat(kb.statementsMatching(st.subject)) 26 | }, 27 | [] as Array 28 | ) 29 | return statementsToDelete 30 | } 31 | 32 | export function getStatementsToAdd ( 33 | origin: NamedNode, 34 | nodeName: string, 35 | modes: string[], 36 | person: NamedNode, 37 | ns: Namespaces 38 | ): any { 39 | const application = new BlankNode(`bn_${nodeName}`) // NamedNode(`${person.doc().uri}#${nodeName}`) 40 | return [ 41 | st(person, ns.acl('trustedApp'), application, person.doc()), 42 | st(application, ns.acl('origin'), origin, person.doc()), 43 | ...modes 44 | .map(mode => sym(mode)) 45 | .map(mode => st(application, ns.acl('mode'), mode, person.doc())) 46 | ] 47 | } 48 | 49 | export function generateRandomString () { 50 | return Math.random().toString(36).substr(2, 5) 51 | } 52 | -------------------------------------------------------------------------------- /src/trustedApplications/trustedApplications.view.ts: -------------------------------------------------------------------------------- 1 | import { PaneDefinition } from 'pane-registry' 2 | import { NamedNode } from 'rdflib' 3 | import { icons, login, widgets } from 'solid-ui' 4 | import { store } from 'solid-logic' 5 | import { createApplicationTable, createContainer, createText } from './trustedApplications.dom' 6 | 7 | const thisColor = '#418d99' 8 | 9 | const trustedApplicationView: PaneDefinition = { 10 | global: true, 11 | icon: `${icons.iconBase}noun_15177.svg`, 12 | name: 'trustedApplications', 13 | label: () => null, 14 | render: (subject, context) => { 15 | const dom = context.dom 16 | const div = dom.createElement('div') 17 | div.classList.add('trusted-applications-pane') 18 | div.setAttribute( 19 | 'style', 20 | 'border: 0.3em solid ' + 21 | thisColor + 22 | '; border-radius: 0.5em; padding: 0.7em; margin-top:0.7em;' 23 | ) 24 | const table = div.appendChild(dom.createElement('table')) 25 | const main = table.appendChild(dom.createElement('tr')) 26 | const bottom = table.appendChild(dom.createElement('tr')) 27 | const statusArea = bottom.appendChild(dom.createElement('div')) 28 | statusArea.setAttribute('style', 'padding: 0.7em;') 29 | render(dom, main, statusArea).catch(err => statusArea.appendChild(widgets.errorMessageBlock(dom, err))) 30 | return div 31 | } 32 | } 33 | 34 | async function render (dom, main, statusArea): Promise { 35 | const authContext = await login.ensureLoadedProfile({ dom: dom, div: main, statusArea: statusArea, me: null }) 36 | const subject = authContext.me as NamedNode 37 | 38 | const profile = subject.doc() 39 | if (!store.updater) { 40 | throw new Error('Store has no updater') 41 | } 42 | const editable = store.updater.editable(profile.uri, store) 43 | 44 | main.appendChild(createText('h3', 'Manage your trusted applications')) 45 | 46 | if (!editable) { 47 | main.appendChild( 48 | widgets.errorMessageBlock(dom, `Your profile ${subject.doc().uri} is not editable, so we cannot do much here.`) 49 | ) 50 | return 51 | } 52 | 53 | main.appendChild(createText('p', 'Here you can manage the applications you trust.')) 54 | 55 | const applicationsTable = createApplicationTable(subject) 56 | main.appendChild(applicationsTable) 57 | 58 | main.appendChild(createText('h4', 'Notes')) 59 | main.appendChild( 60 | createContainer('ol', [ 61 | main.appendChild( 62 | createText( 63 | 'li', 64 | 'Trusted applications will get access to all resources that you have access to.' 65 | ) 66 | ), 67 | main.appendChild( 68 | createText('li', 'You can limit which modes they have by default.') 69 | ), 70 | main.appendChild( 71 | createText('li', 'They will not gain more access than you have.') 72 | ) 73 | ]) 74 | ) 75 | main.appendChild( 76 | createText('p', 'Application URLs must be valid URL. Examples are http://localhost:3000, https://trusted.app, and https://sub.trusted.app.') 77 | ) 78 | } 79 | 80 | export default trustedApplicationView 81 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | interface SolidAuthorization { 2 | // eslint-disable-next-line camelcase 3 | access_token: string 4 | // eslint-disable-next-line camelcase 5 | client_id: string 6 | // eslint-disable-next-line camelcase 7 | id_token: string 8 | } 9 | 10 | interface SolidClaim { 11 | // eslint-disable-next-line camelcase 12 | at_hash: string 13 | aud: string 14 | azp: string 15 | cnf: { 16 | jwk: string 17 | } 18 | exp: number 19 | iat: number 20 | iss: string 21 | jti: string 22 | nonce: string 23 | sub: string 24 | } 25 | 26 | export interface SolidSession { 27 | authorization: SolidAuthorization 28 | credentialType: string 29 | idClaims: SolidClaim 30 | idp: string 31 | issuer: string 32 | sessionKey: string 33 | webId: string 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/22-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/ui/22-builder.png -------------------------------------------------------------------------------- /src/ui/builder.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/ui/builder.graffle -------------------------------------------------------------------------------- /src/ui/builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/ui/builder.png -------------------------------------------------------------------------------- /src/ui/builder2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidOS/solid-panes/c7f6db53b895a4500f6b448ca9f4cb9545b5336e/src/ui/builder2.png -------------------------------------------------------------------------------- /src/ui/pane.js: -------------------------------------------------------------------------------- 1 | /* Form building/editing Pane 2 | ** 3 | */ 4 | 5 | import * as UI from 'solid-ui' 6 | import * as $rdf from 'rdflib' 7 | const ns = UI.ns 8 | 9 | export default { 10 | // noun_170702.svg' builder noun_122196.svg form 11 | icon: UI.icons.iconBase + 'noun_170702.svg', 12 | 13 | name: 'ui', 14 | 15 | audience: [ns.solid('PowerUser')], 16 | 17 | // Does the subject deserve this pane? 18 | label: function (subject, context) { 19 | const ns = UI.ns 20 | const kb = context.session.store 21 | const t = kb.findTypeURIs(subject) 22 | if (t[ns.rdfs('Class').uri]) return 'creation forms' 23 | // if (t[ns.rdf('Property').uri]) return "user interface"; 24 | if (t[ns.ui('Form').uri]) return 'edit form' 25 | 26 | return null // No under other circumstances (while testing at least!) 27 | }, 28 | 29 | render: function (subject, context) { 30 | const dom = context.dom 31 | const kb = context.session.store 32 | const ns = UI.ns 33 | 34 | const box = dom.createElement('div') 35 | box.setAttribute('class', 'formPane') // Share styles 36 | const label = UI.utils.label(subject) 37 | 38 | const mention = function complain (message, style) { 39 | const pre = dom.createElement('p') 40 | pre.setAttribute('style', style || 'color: grey; background-color: white') 41 | box.appendChild(pre).textContent = message 42 | return pre 43 | } 44 | 45 | const complain = function complain (message, style) { 46 | mention(message, 'style', style || 'color: grey; background-color: #fdd') 47 | } 48 | 49 | const complainIfBad = function (ok, body) { 50 | if (ok) { 51 | // setModifiedDate(store, kb, store); 52 | // rerender(box); // Deleted forms at the moment 53 | } else complain('Sorry, failed to save your change:\n' + body) 54 | } 55 | 56 | // ////////////////////////////////////////////////////////////////////////////// 57 | 58 | const t = kb.findTypeURIs(subject) 59 | 60 | let store = null 61 | let docuri 62 | if (subject.uri) { 63 | docuri = $rdf.Util.uri.docpart(subject.uri) 64 | if (subject.uri !== docuri && kb.updater.editable(docuri)) { 65 | store = kb.sym($rdf.Util.uri.docpart(subject.uri)) 66 | } // an editable ontology with hash 67 | } 68 | if (!store) store = kb.any(kb.sym(docuri), ns.link('annotationStore')) 69 | if (!store) store = UI.widgets.defaultAnnotationStore(subject) 70 | 71 | if (!store) { 72 | store = kb.sym( 73 | 'https://formsregistry.solid.community/public/formRegistry/' 74 | ) // fallback 75 | } 76 | // if (!store) store = kb.sym('http://tabulator.org/wiki/ontologyAnnotation/common') // fallback 77 | 78 | // A fallback which gives a different store page for each ontology would be good @@ 79 | 80 | let pred 81 | if (t[ns.rdfs('Class').uri]) { 82 | // Stuff we can do before we load the store 83 | } 84 | 85 | const wait = mention('(Loading data from: ' + store + ')') 86 | 87 | kb.fetcher.nowOrWhenFetched(store.uri, subject, function (ok, body) { 88 | if (!ok) { 89 | complain('Cannot load store ' + store + ' :' + body) 90 | return 91 | } 92 | box.removeChild(wait) 93 | 94 | // Render a Class -- the forms associated with it 95 | 96 | if (t[ns.rdfs('Class').uri]) { 97 | // For each creation form, allow one to create a new object with it, and also to edit the form. 98 | const displayFormsForRelation = function displayFormsForRelation ( 99 | pred, 100 | allowCreation 101 | ) { 102 | const sts = kb.statementsMatching(subject, pred) 103 | const outliner = context.getOutliner(dom) 104 | if (sts.length) { 105 | for (let i = 0; i < sts.length; i++) { 106 | outliner.appendPropertyTRs(box, [sts[i]]) 107 | const form = sts[i].object 108 | const cell = dom.createElement('td') 109 | box.lastChild.appendChild(cell) 110 | if (allowCreation) { 111 | cell.appendChild( 112 | UI.widgets.newButton( 113 | dom, 114 | kb, 115 | null, 116 | null, 117 | subject, 118 | form, 119 | store, 120 | function (ok, body) { 121 | if (ok) { 122 | // dom.outlineManager.GotoSubject(newThing@@, true, undefined, true, undefined); 123 | // rerender(box); // Deleted forms at the moment 124 | } else { 125 | complain('Sorry, failed to save your change:\n' + body) 126 | } 127 | } 128 | ) 129 | ) 130 | } 131 | let formdef = kb.statementsMatching(form, ns.rdf('type')) 132 | if (!formdef.length) formdef = kb.statementsMatching(form) 133 | if (!formdef.length) complain('No data about form') 134 | else { 135 | UI.widgets.editFormButton( 136 | dom, 137 | box, 138 | form, 139 | formdef[0].why, 140 | complainIfBad 141 | ) 142 | } 143 | } 144 | box.appendChild(dom.createElement('hr')) 145 | } else { 146 | mention( 147 | 'There are currently no known forms to make a ' + label + '.' 148 | ) 149 | } 150 | } 151 | 152 | pred = ns.ui('creationForm') 153 | box.appendChild(dom.createElement('h2')).textContent = UI.utils.label( 154 | pred 155 | ) 156 | mention( 157 | 'Creation forms allow you to add information about a new thing,' + 158 | ' in this case a new ' + 159 | label + 160 | '.' 161 | ) 162 | displayFormsForRelation(pred, true) 163 | box.appendChild(dom.createElement('hr')) 164 | mention('You can make a new creation form:') 165 | box.appendChild( 166 | UI.widgets.newButton( 167 | dom, 168 | kb, 169 | subject, 170 | pred, 171 | ns.ui('Form'), 172 | null, 173 | store, 174 | complainIfBad 175 | ) 176 | ) 177 | 178 | box.appendChild(dom.createElement('hr')) 179 | 180 | pred = ns.ui('annotationForm') 181 | box.appendChild(dom.createElement('h2')).textContent = UI.utils.label( 182 | pred 183 | ) 184 | mention( 185 | 'Annotaion forms allow you to add extra information about a ,' + 186 | label + 187 | ' we already know about.' 188 | ) 189 | displayFormsForRelation(pred, false) 190 | box.appendChild(dom.createElement('hr')) 191 | mention('You can make a new annotation form:') 192 | box.appendChild( 193 | UI.widgets.newButton( 194 | dom, 195 | kb, 196 | subject, 197 | pred, 198 | ns.ui('Form'), 199 | null, 200 | store, 201 | complainIfBad 202 | ) 203 | ) 204 | 205 | mention('(Storing new forms in: ' + store + ')') 206 | 207 | // Render a Form 208 | } else if (t[ns.ui('Form').uri]) { 209 | UI.widgets.appendForm( 210 | dom, 211 | box, 212 | kb, 213 | subject, 214 | ns.ui('FormForm'), 215 | store, 216 | complainIfBad 217 | ) 218 | } else { 219 | complain('ui/pane internal error -- Eh?') 220 | } 221 | }) // end: when store loded 222 | 223 | return box 224 | } 225 | } 226 | // ends 227 | -------------------------------------------------------------------------------- /src/video/videoPane.js: -------------------------------------------------------------------------------- 1 | /* Single video play Pane 2 | ** 3 | */ 4 | import * as UI from 'solid-ui' 5 | import * as $rdf from 'rdflib' 6 | 7 | export default { 8 | icon: UI.icons.iconBase + 'noun_1619.svg', 9 | 10 | name: 'video', 11 | 12 | // Does the subject deserve an slideshow pane? 13 | label: function (subject, context) { 14 | const kb = context.session.store 15 | const typeURIs = kb.findTypeURIs(subject) 16 | const prefix = $rdf.Util.mediaTypeClass('video/*').uri.split('*')[0] 17 | for (const t in typeURIs) { 18 | if (t.startsWith(prefix)) return 'Play video' 19 | } 20 | 21 | return null 22 | }, 23 | 24 | render: function (subject, context) { 25 | const kb = context.session.store 26 | const dom = context.dom 27 | const div = dom.createElement('div') 28 | const video = div.appendChild(dom.createElement('video')) 29 | video.setAttribute('controls', 'yes') 30 | // get video with authenticated fetch 31 | kb.fetcher._fetch(subject.uri) 32 | .then(function(response) { 33 | return response.blob() 34 | }) 35 | .then(function(myBlob) { 36 | const objectURL = URL.createObjectURL(myBlob) 37 | video.setAttribute('src', objectURL) // w640 h480 // 38 | }) 39 | 40 | video.setAttribute('width', '100%') 41 | return div 42 | } 43 | } 44 | // ends 45 | -------------------------------------------------------------------------------- /timestamp.sh: -------------------------------------------------------------------------------- 1 | # Timestamp a git/npm project in node JS 2 | echo "export default {" 3 | date -u '+buildTime: "%Y-%m-%dT%H:%M:%SZ",' 4 | git log | grep commit | head -1 | sed -e 's/ /: "/' | sed -e 's/$/",/' 5 | echo npmInfo: 6 | npm version 7 | echo "};" 8 | -------------------------------------------------------------------------------- /travis/bumpversion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * npm does not allow you to publish a package with the same version multiple times. 3 | * Thus, to publish prerelease versions tagged to the branch they're built from, 4 | * we need to generate unique version numbers that are also higher than versions already published. 5 | * To achieve this, we append `build` to the version number. 6 | * The actual release version can eventually be published without the suffix. 7 | */ 8 | 9 | if ( 10 | !process.env.TRAVIS_BUILD_NUMBER || 11 | process.env.TRAVIS_BUILD_NUMBER.length === 0 12 | ) { 13 | console.error( 14 | 'Could not read the build number to bump the package version - aborting publish.' 15 | ) 16 | process.exit(1) 17 | } 18 | 19 | const fs = require('fs') 20 | const path = require('path') 21 | 22 | const packageJson = require('../package.json') 23 | packageJson.version = `${packageJson.version}build${ 24 | process.env.TRAVIS_BUILD_NUMBER 25 | }` 26 | fs.writeFileSync( 27 | path.resolve(__dirname, '../package.json'), 28 | JSON.stringify(packageJson) 29 | ) 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://stackoverflow.com/questions/54139158/cannot-find-name-describe-do-you-need-to-install-type-definitions-for-a-test 3 | 4 | "compilerOptions": { 5 | /* Basic Options */ 6 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "dom", 10 | "es2015", 11 | "es2019" 12 | ] /* Specify library files to be included in the compilation. */, 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "skipLibCheck": true, 18 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 19 | "sourceMap": true /* Generates corresponding '.map' file. */, 20 | // "outFile": "./", /* Concatenate and emit output to single file. */ 21 | "outDir": "lib" /* Redirect output structure to the directory. */, 22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "composite": true, /* Enable project compilation */ 24 | // "incremental": true, /* Enable incremental compilation */ 25 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 26 | // "removeComments": true, /* Do not emit comments to output. */ 27 | // "noEmit": true, /* Do not emit outputs. */ 28 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 29 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 30 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 31 | 32 | /* Strict Type-Checking Options */ 33 | "strict": false /* Enable all strict type-checking options. */, 34 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 35 | "strictNullChecks": true /* Enable strict null checks. */, 36 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 37 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 38 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 39 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 40 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 41 | 42 | /* Additional Checks */ 43 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 44 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 45 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 46 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 47 | 48 | /* Module Resolution Options */ 49 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 50 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 51 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 53 | "typeRoots": [ 54 | "node_modules/@types", 55 | "node_modules/@testing-library", 56 | "./typings" 57 | ] /* List of folders to include type definitions from. */, 58 | // "types": [], /* Type declaration files to be included in compilation. */ 59 | 60 | "types": ["node", "jsdom", "jest", "jest-dom"], /* https://medium.com/heybooster/debugging-typescript-jest-dom-matchers-in-vue-js-6962dab4d4cc */ 61 | 62 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 63 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 64 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 65 | 66 | /* Source Map Options */ 67 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 68 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 69 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 70 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 71 | 72 | /* Experimental Options */ 73 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 74 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 75 | }, 76 | "include": ["src/**/*"], 77 | "exclude": ["node_modules"] // was , "**/*.test.ts" 78 | } 79 | -------------------------------------------------------------------------------- /typings/raw-loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.ttl' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /typings/solid-namespace/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'solid-namespace' { 2 | import { NamedNode } from 'rdflib' 3 | 4 | type RDFLibSubset = { namedNode: (value: string) => NamedNode } 5 | type toNamedNode = (label: string) => NamedNode 6 | export type Namespaces = { [alias: string]: toNamedNode } 7 | 8 | export default function vocab (rdf: RDFLibSubset): Namespaces 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin") 3 | 4 | module.exports = [{ 5 | mode: 'development', 6 | entry: './dev/loader.ts', 7 | plugins: [ 8 | new HtmlWebpackPlugin({ template: './dev/index.html' }), 9 | new NodePolyfillPlugin() 10 | ], 11 | resolve: { 12 | extensions: ['.mjs', '.js', '.ts'], 13 | fallback: { "path": false } 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(mjs|js|ts)$/, 19 | exclude: /(node_modules|bower_components)/, 20 | use: { 21 | loader: 'babel-loader' 22 | } 23 | } 24 | ] 25 | }, 26 | externals: { 27 | fs: 'null', 28 | 'node-fetch': 'fetch', 29 | 'isomorphic-fetch': 'fetch', 30 | xmldom: 'window', 31 | 'text-encoding': 'TextEncoder', 32 | 'whatwg-url': 'window', 33 | '@trust/webcrypto': 'crypto' 34 | }, 35 | devServer: { 36 | static: './dist', 37 | compress: true, 38 | port: 9000 39 | }, 40 | devtool: 'source-map' 41 | }] 42 | --------------------------------------------------------------------------------