From 1273f70c68999fb13d19420deb0eb2cbef137738 Mon Sep 17 00:00:00 2001 From: Tim Berners-Lee Date: Fri, 2 Aug 2019 14:25:24 -0400 Subject: [PATCH] Added basic prefs as (fior now) extra tab in dashboard --- dashboard/basicPreferences.js | 49 ++++++++++++++++ dashboard/basicPreferences.ts | 31 +++++++--- dashboard/homepage.js | 107 ++++++++++++++++++++++++++++++++++ outline/manager.js | 7 +-- profile/editProfilePane.ts | 2 +- profile/profilePane.ts | 5 +- 6 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 dashboard/basicPreferences.js create mode 100644 dashboard/homepage.js diff --git a/dashboard/basicPreferences.js b/dashboard/basicPreferences.js new file mode 100644 index 00000000..7e52f2b0 --- /dev/null +++ b/dashboard/basicPreferences.js @@ -0,0 +1,49 @@ +"use strict"; +exports.__esModule = true; +var solid_ui_1 = require("solid-ui"); +var pane_registry_1 = require("pane-registry"); +var panes; +var UI; +var kb = UI.store; +var nodeMode = (typeof module !== 'undefined'); +if (nodeMode) { + UI = solid_ui_1["default"]; + panes = pane_registry_1["default"]; +} +else { // Add to existing mashlib + panes = window.panes; + UI = panes.UI; +} +exports.basicPreferencesPane = { + icon: UI.icons.iconBase + 'noun_Sliders_341315_000000.svg', + name: 'preferences', + label: function (subject) { + if (subject.uri === subject.site().uri) { + return "Prefs"; + } + return null; + }, + // Render the pane + // The subject should be the logged in user. + render: function (subject, dom) { + var container = dom.createElement('div'); + /* Preferences + ** + ** Things like whether to color text by author webid, to expand image URLs inline, + ** expanded inline image height. ... + ** In general, preferences can be set per user, per user/app combo, per instance, + ** and per instance/user combo. (Seee the long chat pane preferences for an example.) + ** Here in the basic preferences, we are only setting per-user defaults. + */ + var preferencesFormText = "\n\n @prefix rdf: .\n @prefix solid: .\n @prefix ui: .\n @prefix : <#>.\n\n :this\n \"Basic preferences\" ;\n a ui:Form ;\n ui:part :powerUser, :developerUser, :newestFirst, :inlineImageHeightEms;\n ui:parts ( :powerUser :developerUser :newestFirst :inlineImageHeightEms ).\n\n:powerUser a ui:BooleanField; ui:property solid:powerUser;\n ui:label \"Color user input by user\".\n:developerUser a ui:BooleanField; ui:property solid:developerUser;\n ui:label \"Expand image URLs inline\".\n:newestFirst a ui:BooleanField; ui:property solid:newestFirst;\n ui:label \"Newest messages at the top\".\n\n:inlineImageHeightEms a ui:IntegerField; ui:property solid:inlineImageHeightEms;\n ui:label \"Inline image height (lines)\".\n\n"; + var preferencesForm = kb.sym('https://solid.github.io/solid-panes/dashboard/basicPreferencesForm.ttl#this'); + var preferencesFormDoc = preferencesForm.doc(); + if (!kb.holds(undefined, undefined, undefined, preferencesFormDoc)) { // If not loaded already + $rdf.parse(preferencesFormText, kb, preferencesFormDoc.uri, 'text/turtle'); // Load form directly + } + var preferenceProperties = kb.statementsMatching(null, ns.ui.property, null, preferencesFormDoc).map(function (st) { return st.object; }); + var context = { noun: 'chat room', me: me, statusArea: statusArea, div: menuArea, dom: dom, kb: kb }; + container.appendChild(UI.preferences.renderPreferencesForm(chatChannel, mainClass, preferencesForm, context)); + } +}; +// ends diff --git a/dashboard/basicPreferences.ts b/dashboard/basicPreferences.ts index 7f588ee2..d66c3562 100644 --- a/dashboard/basicPreferences.ts +++ b/dashboard/basicPreferences.ts @@ -6,6 +6,9 @@ import { generateHomepage } from "./homepage" let panes: any let UI: SolidUi +const kb = UI.store +const $rdf = UI.rdf +const ns = UI.ns const nodeMode = (typeof module !== 'undefined') @@ -19,7 +22,7 @@ if (nodeMode) { export const basicPreferencesPane: PaneDefinition = { icon: UI.icons.iconBase + 'noun_Sliders_341315_000000.svg', - name: 'preferences', + name: 'basicPreferences', label: (subject) => { if (subject.uri === subject.site().uri) { return "Prefs" @@ -29,9 +32,18 @@ export const basicPreferencesPane: PaneDefinition = { // Render the pane // The subject should be the logged in user. - render: (subject, dom) => { + render: (subject: NamedNode, dom: HTMLDocument, paneOptions: any) => { + function complainIfBad (ok: Boolean, mess: any) { + if (ok) return + container.appendChild(UI.widgets.errorMessageBlock(dom, mess, '#fee')) + } + const container = dom.createElement('div') + const formArea = container.appendChild(dom.createElement('div')) + const statusArea = container.appendChild(dom.createElement('div')) + + /* Preferences ** ** Things like whether to color text by author webid, to expand image URLs inline, @@ -51,8 +63,8 @@ export const basicPreferencesPane: PaneDefinition = { :this "Basic preferences" ; a ui:Form ; - ui:part :powerUser, :developerUser, :newestFirst, :inlineImageHeightEms; - ui:parts ( :powerUser :developerUser :newestFirst :inlineImageHeightEms ). + ui:part :powerUser, :developerUser; + ui:parts ( :powerUser :developerUser ). :powerUser a ui:BooleanField; ui:property solid:powerUser; ui:label "Color user input by user". @@ -70,10 +82,13 @@ export const basicPreferencesPane: PaneDefinition = { if (!kb.holds(undefined, undefined, undefined, preferencesFormDoc)) { // If not loaded already $rdf.parse(preferencesFormText, kb, preferencesFormDoc.uri, 'text/turtle') // Load form directly } - let preferenceProperties = kb.statementsMatching(null, ns.ui.property, null, preferencesFormDoc).map(st => st.object) - - var context = {noun: 'chat room', me: me, statusArea: statusArea, div: menuArea, dom, kb} - container.appendChild(UI.preferences.renderPreferencesForm(chatChannel, mainClass, preferencesForm, context)) + // todo make Statement type for fn nelow + let preferenceProperties = kb.statementsMatching(null, ns.ui.property, null, preferencesFormDoc).map(function (st: any) {return st.object}) + var me = UI.authn.currentUser() + // var context = {noun: 'chat room', me: me, statusArea: statusArea, div: formArea, dom, kb} + // container.appendChild(UI.preferences.renderPreferencesForm(me, mainClass, preferencesForm, context)) + UI.widgets.appendForm(dom, formArea, {}, me, preferencesForm, me.doc(), complainIfBad) + return container } } // ends diff --git a/dashboard/homepage.js b/dashboard/homepage.js new file mode 100644 index 00000000..208ed8ab --- /dev/null +++ b/dashboard/homepage.js @@ -0,0 +1,107 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +exports.__esModule = true; +var rdflib_1 = require("rdflib"); +var solid_ui_1 = require("solid-ui"); +var ns = solid_ui_1["default"].ns; +function generateHomepage(subject, store, fetcher) { + return __awaiter(this, void 0, void 0, function () { + var pod, ownersProfile, name, wrapper; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + pod = subject.site().uri; + return [4 /*yield*/, loadProfile(pod + "/profile/card#me", fetcher)]; + case 1: + ownersProfile = _a.sent(); + name = getName(store, ownersProfile); + wrapper = document.createElement('div'); + wrapper.classList.add('container'); + wrapper.appendChild(createTitle(ownersProfile.uri, name)); + wrapper.appendChild(createDataSection(name)); + return [2 /*return*/, wrapper]; + } + }); + }); +} +exports.generateHomepage = generateHomepage; +function createDataSection(name) { + var dataSection = document.createElement('section'); + var title = document.createElement('h2'); + title.innerText = 'Data'; + dataSection.appendChild(title); + var listGroup = document.createElement('div'); + listGroup.classList.add('list-group'); + dataSection.appendChild(listGroup); + var publicDataLink = document.createElement('a'); + publicDataLink.classList.add('list-group-item'); + publicDataLink.href = '/public/'; + publicDataLink.innerText = "View " + name + "'s files"; + listGroup.appendChild(publicDataLink); + return dataSection; +} +function createTitle(uri, name) { + var profileLink = document.createElement('a'); + profileLink.href = uri; + profileLink.innerText = name; + var profileLinkPost = document.createElement('span'); + profileLinkPost.innerText = "'s Pod"; + var title = document.createElement('h1'); + title.appendChild(profileLink); + title.appendChild(profileLinkPost); + return title; +} +function loadProfile(profileUrl, fetcher) { + return __awaiter(this, void 0, void 0, function () { + var webId; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + webId = rdflib_1["default"].sym(profileUrl); + return [4 /*yield*/, fetcher.load(webId)]; + case 1: + _a.sent(); + return [2 /*return*/, webId]; + } + }); + }); +} +function getName(store, ownersProfile) { + return store.anyValue(ownersProfile, ns.vcard("fn"), null, ownersProfile.doc()) + || store.anyValue(ownersProfile, ns.foaf("name"), null, ownersProfile.doc()) + || new URL(ownersProfile.uri).host.split('.')[0]; +} diff --git a/outline/manager.js b/outline/manager.js index aa3db8d8..7458b09f 100644 --- a/outline/manager.js +++ b/outline/manager.js @@ -280,7 +280,8 @@ module.exports = function (doc) { const me = UI.authn.currentUser() var items = [ // {paneName: 'folder', label: 'Your files', subject: me.site()}, // replaced with storages {paneName: 'home', label: 'Your stuff', icon: UI.icons.iconBase + 'noun_547570.svg'}, - {paneName: 'trustedApplications', label: 'Preferences', icon: UI.icons.iconBase + 'noun_Sliders_341315_00000.svg'}, + {paneName: 'basicPreferences', label: 'Preferences', icon: UI.icons.iconBase + 'noun_Sliders_341315_00000.svg'}, + {paneName: 'trustedApplications', label: 'Trusted Aps', icon: UI.icons.iconBase + 'noun_15177.svg.svg'}, {paneName: 'editProfile', label: 'Edit your profile', icon: UI.icons.iconBase + 'noun_492246.svg'} ] @@ -303,7 +304,7 @@ module.exports = function (doc) { const storages = kb.each(me, ns.space('storage'), null, me.doc()) for (var pod of storages) { - var label = storages.length > 1 ? pod.uri.split('//')[1].slice(0,-1) : 'Your storage' + var label = storages.length > 1 ? pod.uri.split('//')[1].slice(0, -1) : 'Your storage' items.push({paneName: 'folder', label: label, subject: pod, icon: UI.icons.iconBase + 'noun_Cabinet_251723.svg'}) } @@ -317,8 +318,6 @@ module.exports = function (doc) { containerDiv.innerHTML = '' var table = containerDiv.appendChild(dom.createElement('table')) const me = UI.authn.currentUser() - // @@ Using document.location.origin here is a hack, inserted there because Tim wants this before his presentation Thursday: - const subject = (item === 'folder') ? $rdf.sym(document.location.origin) : me thisOutline.GotoSubject(item.subject || me, true, pane, false, undefined, table) } diff --git a/profile/editProfilePane.ts b/profile/editProfilePane.ts index aaed81b5..1f22f9c6 100644 --- a/profile/editProfilePane.ts +++ b/profile/editProfilePane.ts @@ -94,7 +94,7 @@ const thisPane: PaneDefinition = { // 'noun_638141.svg' not editing } // renderProfileForm var div = dom.createElement('div') - var editableProfile + var editableProfile: NamedNode | null div.setAttribute('style', 'border: 0.3em solid ' + highlightColor + '; border-radius: 0.5em; padding: 0.7em; margin-top:0.7em;') var table = div.appendChild(dom.createElement('table')) diff --git a/profile/profilePane.ts b/profile/profilePane.ts index 477dac53..b9a93013 100644 --- a/profile/profilePane.ts +++ b/profile/profilePane.ts @@ -13,7 +13,7 @@ // import UI from 'solid-ui' // import solidUi, { SolidUi } from 'solid-ui' import { NamedNode } from 'rdflib' -import panes from 'pane-registry' +// import panes from 'pane-registry' import { PaneDefinition } from '../types' import { getLabel } from './profilePaneUtils' @@ -62,7 +62,8 @@ const thisPane: PaneDefinition = { // 'noun_638141.svg' not editing return d } - async function doRender(container, subject, dom) { + async function doRender(container: HTMLElement, subject: NamedNode | null, dom: HTMLDocument) { + if (!subject) throw new Error('subject missing') const profile = subject.doc() let otherProfiles = kb.each(subject, ns.rdfs('seeAlso'), null, profile) if (otherProfiles.length > 0) {