diff --git a/generators/app/index.js b/generators/app/index.js index 24597df1..ed218f0c 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -131,6 +131,10 @@ module.exports = class extends Generator { src: 'src/containers/Profile/**', dest: 'src/containers/Profile', }, + { + src: 'src/containers/TextEditor/**', + dest: 'src/containers/TextEditor', + }, { src: 'src/containers/TicTacToe/**', dest: 'src/containers/TicTacToe', diff --git a/generators/app/templates/public/locales/en-US/translation.json b/generators/app/templates/public/locales/en-US/translation.json index de8630a4..d49399d0 100644 --- a/generators/app/templates/public/locales/en-US/translation.json +++ b/generators/app/templates/public/locales/en-US/translation.json @@ -9,7 +9,16 @@ "notifications": { "success": "Success", "error": "Error", - "unknownError": "An unknown error has occurred" + "unknownError": "An unknown error has occurred", + "404": "Editing a new document.", + "errorLoading": "There was an error loading your file, please try again.", + "errorSaving": "There was an error saving your file, please try again.", + "errorFetching": "Error fetching document.", + "accessGranted": "Co-editor now has read/write access.", + "errorGrantingAccess": "There was an error giving access to your file, please try again.", + "saved": "Your file was saved successfully.", + "notEditable": "(not editable)", + "notSharable": "(log in and load a doc on your own pod to give others access)" }, "login": { "title": "Hi! Welcome to Solid.", @@ -24,7 +33,7 @@ "btnTxtProvider": "Log In with Provider", "errors": { "unknown": "Something is wrong, please try again...", - "webIdNotValid": "WeibID is not valid", + "webIdNotValid": "WebID is not valid", "emptyProvider": "Solid Provider is required", "emptyWebId": "Valid WebID is required" } @@ -40,6 +49,7 @@ "welcome": "Welcome", "logOut": "Log out", "profile": "Profile", + "text-editor": "Text Editor", "changeLanguage": "Change Language", "languages": { "en": "English (English)", @@ -186,6 +196,12 @@ "href": "https://solidsdk.inrupt.net/public/general/en/app-inbox-cannot-be-created.html" } }, + "errorFormRender": { + "link": { + "label": "Learn more", + "href": "https://solidsdk.inrupt.net/public/general/en/form-rendering-errors.html" + } + }, "formLanguage": { "shacl": "SHACL", "shaclExtension": "SHACL + RDF Extension", @@ -212,7 +228,19 @@ "viewBtn": "View", "editBtn": "Edit", "viewMode": "View Mode", - "editMode": "Edit Mode" + "editMode": "Edit Mode", + "formSaved": "Form successfully saved", + "formNotLoaded": "Form could not be rendered", + "fieldDeleted": "Form field successfully deleted", + "fieldAdded": "New form field successfully added" } + }, + "editor": { + "explanation": "This simple text editor enables you to create a plaintext file and save it in a Pod. To make this very clear for the demo you provide a URL as an absolute path to the file in the Pod. The Pod can be the one this App has been logged into or another person's Pod. To write, and read, into another person's Pod the owner will have had to provide you with write permission.", + "url": "URL", + "friend": "Co-editor's webid", + "load": "Load", + "save": "Save", + "grantAccess": "Grant Access" } } diff --git a/generators/app/templates/public/locales/en/translation.json b/generators/app/templates/public/locales/en/translation.json index 5a4fc2f3..d49399d0 100644 --- a/generators/app/templates/public/locales/en/translation.json +++ b/generators/app/templates/public/locales/en/translation.json @@ -9,7 +9,16 @@ "notifications": { "success": "Success", "error": "Error", - "unknownError": "An unknown error has occurred" + "unknownError": "An unknown error has occurred", + "404": "Editing a new document.", + "errorLoading": "There was an error loading your file, please try again.", + "errorSaving": "There was an error saving your file, please try again.", + "errorFetching": "Error fetching document.", + "accessGranted": "Co-editor now has read/write access.", + "errorGrantingAccess": "There was an error giving access to your file, please try again.", + "saved": "Your file was saved successfully.", + "notEditable": "(not editable)", + "notSharable": "(log in and load a doc on your own pod to give others access)" }, "login": { "title": "Hi! Welcome to Solid.", @@ -40,6 +49,7 @@ "welcome": "Welcome", "logOut": "Log out", "profile": "Profile", + "text-editor": "Text Editor", "changeLanguage": "Change Language", "languages": { "en": "English (English)", @@ -128,6 +138,7 @@ "idLabel": "Game ID", "opponentWebIDLabel": "Opponent WebID", "nogames": "No game were found", + "invitationAccept": "has invited you to play a game of TicTacToe. Would you like to play?", "invitationTemplate": "<0><0><0>{{- name}}0> has invited you to play a game of TicTacToe.0><1>Would you like to play?1>0>", "invitationAcceptText": "Accept", "invitationDeclineText": "Decline", @@ -223,5 +234,13 @@ "fieldDeleted": "Form field successfully deleted", "fieldAdded": "New form field successfully added" } + }, + "editor": { + "explanation": "This simple text editor enables you to create a plaintext file and save it in a Pod. To make this very clear for the demo you provide a URL as an absolute path to the file in the Pod. The Pod can be the one this App has been logged into or another person's Pod. To write, and read, into another person's Pod the owner will have had to provide you with write permission.", + "url": "URL", + "friend": "Co-editor's webid", + "load": "Load", + "save": "Save", + "grantAccess": "Grant Access" } } diff --git a/generators/app/templates/public/locales/es/translation.json b/generators/app/templates/public/locales/es/translation.json index d9c58844..218e4c4d 100644 --- a/generators/app/templates/public/locales/es/translation.json +++ b/generators/app/templates/public/locales/es/translation.json @@ -9,7 +9,16 @@ "notifications": { "success": "Éxito", "error": "Error", - "unknownError": "Ha occurido un error desconocido" + "unknownError": "Ha occurido un error desconocido", + "404": "Editando un documento nuevo.", + "errorLoading": "Había un error al cargar tu documento. Por favor, intenta de nuevo.", + "errorSaving": "Había un error al guardar tu documento. Por favor, intenta de nuevo.", + "errorFetching": "Error recogiendo fichero.", + "accessGranted": "Collaborador ahora tiene acceso de leer y escribir.", + "errorGrantingAccess": "Había un error al dar acceso a tu documento. Por favor, intenta de nuevo.", + "saved": "Tu documento ha sido guardado al pod.", + "notEditable": "(no editable)", + "notSharable": "(logéate y carga undocumento en tu própio pod para poder dar acceso)" }, "login": { "title": "Hola! Bienvenido a Solid.", @@ -40,6 +49,7 @@ "welcome": "Bienvenida", "logOut": "Cerrar Sesión", "profile": "Perfil", + "text-editor": "Editor de Textos", "changeLanguage": "Cambiar Lenguaje", "languages": { "en": "English (Inglés)", @@ -128,6 +138,7 @@ "idLabel": "ID del Juego", "opponentWebIDLabel": "WebID del Oponente", "nogames": "No se han encontrado juegos", + "invitationAccept": "te ha invitado a jugar una partida de Tres en Linea. Te gustaría jugar?", "invitationTemplate": "<0><0><0>{{- name}}0> te ha invitado a un juego de Tres en Linea.0><1>Quieres aceptar el juego?1>0>", "invitationAcceptText": "Aceptar", "invitationDeclineText": "Rechazar", @@ -223,5 +234,13 @@ "fieldDeleted": "Campo de formulario eliminado correctamente", "fieldAdded": "Campo de formulario agregado con éxito" } + }, + "editor": { + "explanation": "Aqui tiene un simple editor de textos con que puede crear un fichero de texto plano y guardarlo en un Pod. Para mostrarlo claramente en este demo, hay que proveer una URL que directamente identifica el fichero en el Pod. El Pod puede ser el del mismo usuario conectado, o de otra persona. Para poder escribir, y leer, al Pod de otra persona, el proprietario tendrá que dar permiso de escribir, usando el botón de 'Dar Acceso' abajo.", + "url": "URL", + "friend": "Webid de collaborador", + "load": "Cargar", + "save": "Guardar", + "grantAccess": "Dar Acceso" } } diff --git a/generators/app/templates/src/containers/TextEditor/index.js b/generators/app/templates/src/containers/TextEditor/index.js new file mode 100644 index 00000000..e7532f9b --- /dev/null +++ b/generators/app/templates/src/containers/TextEditor/index.js @@ -0,0 +1,3 @@ +import TextEditor from './text-editor.component'; + +export default TextEditor; diff --git a/generators/app/templates/src/containers/TextEditor/text-editor.component.js b/generators/app/templates/src/containers/TextEditor/text-editor.component.js new file mode 100644 index 00000000..92dbec2f --- /dev/null +++ b/generators/app/templates/src/containers/TextEditor/text-editor.component.js @@ -0,0 +1,201 @@ +/* eslint-disable constructor-super */ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TextEditorWrapper, TextEditorContainer, Header, Form, FullGridSize, Button, Label, Input, TextArea, WebId } from './text-editor.style'; +import SolidAuth from 'solid-auth-client'; +import { successToaster, errorToaster } from '@utils'; +import { fetchDocument } from 'tripledoc'; +import { AccessControlList } from '@inrupt/solid-react-components'; +import { useWebId } from '@solid/react'; + +const pim = { storage: 'http://www.w3.org/ns/pim/space#storage' }; + +function extractWacAllow(response) { + // WAC-Allow: user="read write append control",public="read" + let modes = { + user: { + read: false, + append: false, + write: false, + control: false + }, + public: { + read: false, + append: false, + write: false, + control: false + } + }; + const wacAllowHeader = response.headers.get('WAC-Allow'); + if (wacAllowHeader) { + wacAllowHeader // 'user="read write append control",public="read"' + .split(',') // ['user="read write append control"', 'public="read"'] + .map(str => str.trim()) + .forEach(statement => { // 'user="read write append control"' + const parts = statement.split('='); // ['user', '"read write control"'] + if (parts.length >= 2 && ['user', 'public'].indexOf(parts[0]) !== -1 && parts[1].length > 2) { + const modeStr = parts[1].replace(/"/g, ''); // 'read write control' or '' + if (modeStr.length) { + modeStr.split(' ').forEach(mode => { + modes[parts[0]][mode] = true; + }); + } + } + }); + } + return modes; +} + +export const Editor = (props) => { + const { t } = useTranslation(); + const [url, setUrl] = useState(''); + const [friend, setFriend] = useState('https://example-friend.com/profile/card#me'); + const [text, setText] = useState(''); + const [profileDoc, setProfileDoc] = useState(); + const webId = useWebId(); + useEffect(() => { + async function fetchProfileDoc () { + if (webId) { + setProfileDoc(await fetchDocument(webId)); + } + } + fetchProfileDoc(); + }, [webId]); + useEffect(() => { + if (profileDoc && !url) { + const sub = profileDoc.getSubject(webId); + const storageRoot = sub.getNodeRef(pim.storage); + if (storageRoot) { + const exampleUrl = new URL('/share/some-doc.txt', storageRoot); + setUrl(exampleUrl.toString()); + } + } + }, [profileDoc]); + + const [loaded, setLoaded] = useState(false); + const [editable, setEditable] = useState(false); + const [sharable, setSharable] = useState(false); + + function handleUrlChange(event) { + event.preventDefault(); + setUrl(event.target.value); + } + + function handleFriendChange(event) { + event.preventDefault(); + setFriend(event.target.value); + } + + function handleTextChange(event) { + event.preventDefault(); + setText(event.target.value); + } + + function handleLoad(event) { + event.preventDefault(); + const doc = SolidAuth.fetch(url); + doc.then(async (response) => { + const text = await response.text(); + if (response.ok) { + setText(text); + } else if (response.status === 404) { + successToaster(t('notifications.404')); + } else { + errorToaster(t('notifications.errorLoading')); + } + const wacAllowModes = extractWacAllow(response); + setEditable(wacAllowModes.user.write); + setSharable(wacAllowModes.user.control); + setLoaded(true); + }).catch((e) => { + errorToaster(t('notifications.errorFetching')); + }); + } // assuming the logged in user doesn't change without a page refresh + + async function handleShare(event) { + event.preventDefault(); + try { + const permissions = [ + { + agents: [friend], + modes: [AccessControlList.MODES.READ, AccessControlList.MODES.WRITE] + } + ]; + const ACLFile = new AccessControlList(webId, url); + await ACLFile.createACL(permissions); + successToaster(t('notifications.accessGranted')); + } catch (e) { + errorToaster(t('notifications.errorGrantingAccess')); + } + } + + async function handleSave(event) { + event.preventDefault(); + // Not using TripleDoc or LDFlex here, because this is not an RDF document. + const result = await SolidAuth.fetch(url, { + method: 'PUT', + body: text, + headers: { + 'Content-Type': 'text/plain' + } + }); + + if (result.ok) { + successToaster(t('notifications.saved')); + } else if(result.ok === false) { + errorToaster(t('notifications.errorSaving')); + } + } + + return ( +
+ ); +} + +/** + * A React component page that is displayed when there's no valid route. Users can click the button + * to get back to the home/welcome page. + */ +const TextEditor = () => { + const { t } = useTranslation(); + return ( ++ {t('editor.explanation')} +
+