├── .env
├── .gitignore
├── LICENSE.md
├── README.md
├── etc
├── logo.ai
└── logo.png
├── package-lock.json
├── package.json
├── public
├── favicon.png
├── index.html
├── locales
│ └── en
│ │ ├── referenced-by-list.json
│ │ └── translation.json
├── logo192.png
├── logo512.png
├── manifest.json
├── popup.html
└── robots.txt
├── src
├── @solid
│ ├── query-ldflex
│ │ └── index.d.ts
│ └── solid__react
│ │ └── index.d.ts
├── App.css
├── App.js
├── App.test.js
├── AppErrorBoundary.tsx
├── base32
│ └── index.d.ts
├── components
│ ├── AppTitle.js
│ ├── Backups.tsx
│ ├── BackupsDialog.js
│ ├── ChecklistItemElement.js
│ ├── ConceptCreator.tsx
│ ├── Console.js
│ ├── CurrentConcept.tsx
│ ├── CurrentPage.tsx
│ ├── Document.tsx
│ ├── DocumentTextEditor.tsx
│ ├── Editable.tsx
│ ├── EditorToolbar.js
│ ├── EmbedElement.js
│ ├── EmbedPicker.js
│ ├── FAQ.js
│ ├── Home.js
│ ├── IconButton.tsx
│ ├── ImageUploader.js
│ ├── LandingPage.tsx
│ ├── Link.tsx
│ ├── Loader.tsx
│ ├── LogInLogOutButton.js
│ ├── ProfileLink.tsx
│ ├── PublicProfile.tsx
│ ├── ReferencedByList.tsx
│ ├── ReportErrorButton.tsx
│ ├── SharingModal.js
│ ├── SignupPage.js
│ ├── SocialIcons.js
│ ├── UnrecoverableErrorPage.tsx
│ ├── WhatPage.js
│ ├── Workspace.js
│ ├── WorkspaceDrawer.tsx
│ └── edit
│ │ ├── Block.tsx
│ │ ├── ConceptElement.tsx
│ │ ├── ImageElement.tsx
│ │ ├── InsertMenu.tsx
│ │ ├── LinkElement.tsx
│ │ ├── Table.tsx
│ │ ├── TurnIntoMenu.tsx
│ │ └── index.ts
├── constants.js
├── context
│ ├── auth.tsx
│ ├── document.ts
│ ├── preferences.tsx
│ └── workspace.tsx
├── hooks
│ ├── acls.ts
│ ├── backup.ts
│ ├── concepts.ts
│ ├── data.ts
│ └── pages.ts
├── i18n.js
├── index.css
├── index.js
├── logo.svg
├── ontology.ts
├── react-app-env.d.ts
├── serviceWorker.js
├── setupTests.js
├── solid-namespace
│ └── index.d.ts
├── theme.js
├── utils
│ ├── acl.ts
│ ├── backups.ts
│ ├── data.ts
│ ├── editor.js
│ ├── fonts.js
│ ├── ldflex-helper.js
│ ├── model.ts
│ ├── slate.ts
│ └── urls.ts
└── wac-allow
│ └── index.d.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020 Travis Vachon
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | * No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/).
8 |
9 | * Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above.
10 |
11 | * Enforceability: If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14 |
15 | This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Thanks for your interest in Concept! Unfortunately it is currently
2 | unsupported and I have no plans to continue developing it. Happily, it
3 | has been repurposed and reconfigured and reborn as
4 | [itme.online](https://itme.online), a Grant for the Web Flagship
5 | Project. Follow that repository or subscribe to [our
6 | blog](http://itme.press/) or [newsletter](https://tinyletter.com/itme)
7 | to learn more about cutting edge Solid web development.
8 |
9 | ~ tfv 1/25/21
10 |
11 | # Concept
12 |
13 | All (well, some)-in-one workspace built on [Solid](https://solidproject.org/).
14 |
15 | Use Concept:
16 |
17 | [useconcept.art](http://useconcept.art)
18 |
19 | # Development
20 |
21 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
22 |
23 | ## Available Scripts
24 |
25 | In the project directory, you can run:
26 |
27 | ### `npm start`
28 |
29 | Runs the app in the development mode.
30 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
31 |
32 | The page will reload if you make edits.
33 | You will also see any lint errors in the console.
34 |
35 | ### `npm test`
36 |
37 | Launches the test runner in the interactive watch mode.
38 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
39 |
40 | ### `npm run build`
41 |
42 | Builds the app for production to the `build` folder.
43 | It correctly bundles React in production mode and optimizes the build for the best performance.
44 |
45 | The build is minified and the filenames include the hashes.
46 | Your app is ready to be deployed!
47 |
48 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
49 |
50 | ### `npm run eject`
51 |
52 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
53 |
54 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
55 |
56 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
57 |
58 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
59 |
60 | ## Learn More
61 |
62 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
63 |
64 | To learn React, check out the [React documentation](https://reactjs.org/).
65 |
66 | ### Code Splitting
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
69 |
70 | ### Analyzing the Bundle Size
71 |
72 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
73 |
74 | ### Making a Progressive Web App
75 |
76 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
77 |
78 | ### Advanced Configuration
79 |
80 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
81 |
82 | ### Deployment
83 |
84 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
85 |
86 | ### `npm run build` fails to minify
87 |
88 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
89 |
--------------------------------------------------------------------------------
/etc/logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/travis/concept/c7a27fd8a441c98657fecab96f1cf037cb8a2309/etc/logo.ai
--------------------------------------------------------------------------------
/etc/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/travis/concept/c7a27fd8a441c98657fecab96f1cf037cb8a2309/etc/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "concept",
3 | "version": "0.1.0-dev.0",
4 | "private": true,
5 | "dependencies": {
6 | "@inrupt/solid-react-components": "^0.5.1-dev.19",
7 | "@material-ui/core": "^4.9.5",
8 | "@material-ui/icons": "^4.9.1",
9 | "@material-ui/lab": "^4.0.0-alpha.45",
10 | "@rdfjs/data-model": "^1.1.2",
11 | "@sentry/browser": "^5.15.5",
12 | "@solid/query-ldflex": "^2.9.0",
13 | "@solid/react": "^1.8.0",
14 | "@testing-library/jest-dom": "^4.2.4",
15 | "@testing-library/react": "^9.4.0",
16 | "@testing-library/user-event": "^7.2.1",
17 | "@types/react-loader-spinner": "^3.1.0",
18 | "@types/react-router-dom": "^5.1.3",
19 | "@types/solid-auth-client": "^2.4.0",
20 | "@types/uuid": "^7.0.2",
21 | "base32": "0.0.6",
22 | "copy-to-clipboard": "^3.3.1",
23 | "i18next": "^19.4.0",
24 | "i18next-browser-languagedetector": "^4.0.2",
25 | "i18next-xhr-backend": "^3.2.2",
26 | "image-extensions": "^1.1.0",
27 | "is-hotkey": "^0.1.6",
28 | "is-url": "^1.2.4",
29 | "notistack": "^0.9.11",
30 | "parse-link-header": "^1.0.1",
31 | "rdf-namespaces": "^1.8.0",
32 | "react": "^16.12.0",
33 | "react-cropper": "^1.3.0",
34 | "react-dnd": "^10.0.2",
35 | "react-dnd-html5-backend": "^10.0.2",
36 | "react-dom": "^16.12.0",
37 | "react-i18next": "^11.3.4",
38 | "react-loader-spinner": "^3.1.5",
39 | "react-router-dom": "^5.1.2",
40 | "react-scripts": "3.3.0",
41 | "react-slate": "^0.5.1",
42 | "slate": "^0.57.1",
43 | "slate-history": "^0.57.1",
44 | "slate-react": "^0.57.1",
45 | "solid-auth-client": "^2.4.1",
46 | "solid-namespace": "^0.3.0",
47 | "styled-components": "^5.0.1",
48 | "typescript": "^3.8.3",
49 | "use-debounce": "^3.3.0",
50 | "uuid": "^3.4.0",
51 | "wac-allow": "^1.0.0"
52 | },
53 | "scripts": {
54 | "start": "react-scripts start",
55 | "build": "react-scripts build",
56 | "test": "react-scripts test",
57 | "eject": "react-scripts eject"
58 | },
59 | "eslintConfig": {
60 | "extends": "react-app"
61 | },
62 | "browserslist": {
63 | "production": [
64 | ">0.2%",
65 | "not dead",
66 | "not op_mini all"
67 | ],
68 | "development": [
69 | "last 1 chrome version",
70 | "last 1 firefox version",
71 | "last 1 safari version"
72 | ]
73 | },
74 | "devDependencies": {
75 | "@sentry/webpack-plugin": "^1.11.1"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/travis/concept/c7a27fd8a441c98657fecab96f1cf037cb8a2309/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
15 |
16 |
17 |
26 |
27 |
28 | Concept
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/locales/en/referenced-by-list.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "{{count}} reference to {{name}}",
3 | "title_plural": "{{count}} references to {{name}}"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": "pages",
3 | "concepts": "concepts",
4 | "referencedByList": {
5 | "title": "{{count}} reference to {{name}}",
6 | "title_plural": "{{count}} references to {{name}}"
7 | },
8 | "editable": {
9 | "placeholder": "Click here to edit..."
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/travis/concept/c7a27fd8a441c98657fecab96f1cf037cb8a2309/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/travis/concept/c7a27fd8a441c98657fecab96f1cf037cb8a2309/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/@solid/query-ldflex/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@solid/query-ldflex';
2 |
--------------------------------------------------------------------------------
/src/@solid/solid__react/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@solid/react';
2 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | html, body, #root, .App {
2 | height: 100%
3 | }
4 |
5 | .App {
6 | text-align: center;
7 | }
8 |
9 | .App-logo {
10 | height: 40vmin;
11 | pointer-events: none;
12 | }
13 |
14 | @media (prefers-reduced-motion: no-preference) {
15 | .App-logo {
16 | animation: App-logo-spin infinite 20s linear;
17 | }
18 | }
19 |
20 | .App-header {
21 | background-color: #282c34;
22 | min-height: 100vh;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | justify-content: center;
27 | font-size: calc(10px + 2vmin);
28 | color: white;
29 | }
30 |
31 | .App-link {
32 | color: #61dafb;
33 | }
34 |
35 | @keyframes App-logo-spin {
36 | from {
37 | transform: rotate(0deg);
38 | }
39 | to {
40 | transform: rotate(360deg);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
3 | import CssBaseline from '@material-ui/core/CssBaseline';
4 | import { DndProvider } from 'react-dnd'
5 | import DndBackend from 'react-dnd-html5-backend'
6 | import {useLoggedIn} from '@solid/react';
7 | import {SnackbarProvider} from 'notistack';
8 | import { ThemeProvider } from '@material-ui/core/styles';
9 |
10 | import './App.css';
11 | import {AuthProvider} from './context/auth'
12 | import {PreferencesProvider} from './context/preferences'
13 |
14 | import Workspace from './components/Workspace';
15 | import Console from './components/Console';
16 | import LandingPage from './components/LandingPage';
17 | import SignupPage from './components/SignupPage';
18 | import WhatPage from './components/WhatPage';
19 | import CurrentPage from "./components/CurrentPage"
20 | import CurrentConcept from "./components/CurrentConcept"
21 | import Loader from "./components/Loader"
22 | import PublicProfile, {EncodedWebIdPublicProfile} from './components/PublicProfile';
23 | import AppErrorBoundary from "./AppErrorBoundary";
24 | import theme from './theme'
25 |
26 | function App() {
27 | const loggedIn = useLoggedIn()
28 | return (
29 |
30 |
31 |
32 | {(loggedIn === undefined) ? (
33 |
34 | ) : (
35 | loggedIn ? (
36 |
37 |
38 |
39 |
40 | ) : (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ))}
50 |
51 |
52 | )
53 | }
54 |
55 | function AppContainer() {
56 | return (
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default AppContainer;
77 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/AppErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import * as Sentry from '@sentry/browser';
3 | import Button from '@material-ui/core/Button';
4 | import { withSnackbar, ProviderContext } from 'notistack';
5 |
6 | import UnrecoverableErrorPage from './components/UnrecoverableErrorPage';
7 |
8 | type AppErrorBoundaryProps = { children: ReactNode } & ProviderContext
9 | type AppErrorBoundaryState = {
10 | hasError: boolean, eventId: string | undefined, error: any,
11 | snackbarOpen: boolean, lastErrorTime: number | null,
12 | possibleInfiniteLoop: boolean
13 | }
14 |
15 | class AppErrorBoundary extends React.Component {
16 | state = {
17 | eventId: undefined, error: null, hasError: false,
18 | snackbarOpen: false, lastErrorTime: null,
19 | possibleInfiniteLoop: false
20 | }
21 |
22 | static getDerivedStateFromError(error: any) {
23 | // Update state so the next render will show the fallback UI.
24 | return {
25 | hasError: true, error
26 | }
27 | };
28 |
29 | componentDidCatch(error: any, errorInfo: any) {
30 | Sentry.withScope((scope) => {
31 | scope.setExtras(errorInfo);
32 | const eventId = Sentry.captureException(error);
33 | const now = Date.now()
34 | const lastErrorTime = this.state.lastErrorTime
35 | const possibleInfiniteLoop = (!!lastErrorTime && ((now - lastErrorTime) < 500))
36 | this.setState({
37 | eventId, lastErrorTime: now,
38 | possibleInfiniteLoop
39 | });
40 | this.props.enqueueSnackbar("Oh no - we caught an error!", {
41 | variant: "error",
42 | action: Sentry.showReportDialog({ eventId })}>Tell Us About It
43 | })
44 | });
45 | }
46 |
47 | handleClose() {
48 | this.setState({ hasError: false, snackbarOpen: false })
49 | }
50 |
51 | render() {
52 | if (this.state.possibleInfiniteLoop) {
53 | return
54 | } else {
55 | return this.props.children
56 | }
57 | }
58 | }
59 |
60 | export default withSnackbar(AppErrorBoundary)
61 |
--------------------------------------------------------------------------------
/src/base32/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'base32';
2 |
--------------------------------------------------------------------------------
/src/components/AppTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {Link} from 'react-router-dom';
4 |
5 | import Grid from '@material-ui/core/Grid';
6 | import Typography from '@material-ui/core/Typography';
7 | import { makeStyles } from '@material-ui/core/styles';
8 |
9 | import { titleFont } from '../utils/fonts'
10 |
11 | const useStyles = makeStyles(theme => ({
12 | name: {
13 | marginTop: theme.spacing(3),
14 | marginBottom: theme.spacing(-3),
15 | fontFamily: titleFont
16 | },
17 | nameLink: {
18 | textDecoration: "none",
19 | color: "inherit"
20 | },
21 | version: {
22 | position: "relative",
23 | left: theme.spacing(12),
24 | height: 0
25 | },
26 | motto: {
27 | marginBottom: theme.spacing(1),
28 | }
29 | }))
30 |
31 | export function AppTitleGridRow(){
32 | const classes = useStyles({})
33 | return (
34 |
35 |
36 |
37 |
38 | Concept
39 |
40 |
41 |
42 | alpha
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export function MottoGridRow(){
50 | const classes = useStyles({})
51 | return (
52 |
53 |
54 |
55 | Concept is a collaborative workspace for organizing the world.
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Backups.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, PropsWithChildren } from 'react';
2 |
3 | import data from '@solid/query-ldflex';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Button from '@material-ui/core/Button';
7 | import Paper from '@material-ui/core/Paper';
8 | import Dialog from '@material-ui/core/Dialog';
9 | import DialogActions from '@material-ui/core/DialogActions';
10 | import DialogContent from '@material-ui/core/DialogContent';
11 | import DialogContentText from '@material-ui/core/DialogContentText';
12 | import DialogTitle from '@material-ui/core/DialogTitle';
13 | import Table from '@material-ui/core/Table';
14 | import TableBody from '@material-ui/core/TableBody';
15 | import TableCell from '@material-ui/core/TableCell';
16 | import TableContainer from '@material-ui/core/TableContainer';
17 | import TableHead from '@material-ui/core/TableHead';
18 | import TableRow from '@material-ui/core/TableRow';
19 | import IconButton from '@material-ui/core/IconButton';
20 | import CloseIcon from '@material-ui/icons/Close';
21 |
22 | import { ldp, schema, dct } from 'rdf-namespaces';
23 |
24 | import { Slate } from 'slate-react';
25 |
26 | import { useListValuesQuery, useValueQuery, useDateQuery } from '../hooks/data';
27 | import { backupFolderForPage } from '../utils/backups'
28 | import { Document } from '../utils/model'
29 | import { createBackup } from '../hooks/backup'
30 | import concept from '../ontology'
31 | import Editable, { useNewEditor } from "./Editable";
32 | import Loader from "./Loader";
33 |
34 | const useStyles = makeStyles(theme => ({
35 | editor: {
36 | textAlign: "left",
37 | padding: theme.spacing(2),
38 | background: "white",
39 | position: "relative",
40 | height: "600em",
41 | minWidth: theme.spacing(100)
42 | },
43 | table: {
44 | mindWidth: 650
45 | },
46 | restoreLoader: {
47 | textAlign: "right",
48 | paddingRight: theme.spacing(4)
49 | },
50 | previewCloseButton: {
51 | position: 'absolute',
52 | right: theme.spacing(1),
53 | top: theme.spacing(1),
54 | color: theme.palette.grey[500],
55 | },
56 | }));
57 |
58 | type CloseOpts = { closeAll: boolean }
59 | type HandleClose = (opts?: CloseOpts) => void
60 | type RestoreDialogProps = { original: string, date: Date, restore: () => void, handleClose: HandleClose, open: boolean }
61 |
62 | function RestoreDialog({ original, date, restore, handleClose, open }: RestoreDialogProps) {
63 | const [originalName] = useValueQuery(original, schema.name)
64 | const [restoring, setRestoring] = useState(false)
65 | const handleConfirm = useCallback(async () => {
66 | setRestoring(true)
67 | await restore()
68 | handleClose({ closeAll: true })
69 | }, [restore, handleClose])
70 | const classes = useStyles()
71 | return (
72 | handleClose()}
75 | aria-labelledby="confirm-restore-dialog-title"
76 | aria-describedby="confirm-restore-dialog-description"
77 | >
78 |
79 | Restore {date.toLocaleString()} version of {originalName}?
80 |
81 |
82 |
83 | We'll create a backup of the current version first.
84 |
85 |
86 | {restoring ? (
87 |
88 | ) : (
89 |
90 | handleConfirm()} color="primary">
91 | Yes!
92 |
93 | handleClose()} color="primary" autoFocus>
94 | No nevermind
95 |
96 |
97 | )}
98 |
99 | )
100 | }
101 |
102 | function PreviewName({ original }: { original: string }) {
103 | const [name] = useValueQuery(original, schema.name)
104 | return <>{name}>
105 | }
106 |
107 | type PreviewDialogProps = { backup: string, date: Date, open?: boolean, handleClose: HandleClose }
108 |
109 | function PreviewDialog({ backup, date, open = true, handleClose }: PreviewDialogProps) {
110 | const [showRestore, setShowRestore] = useState(false)
111 | const [original] = useValueQuery(backup, concept.backupOf)
112 | const [backupText] = useValueQuery(backup, schema.text)
113 | const editor = useNewEditor()
114 | const classes = useStyles();
115 | const restore = useCallback(async () => {
116 | const currentText = data[original][schema.text]
117 | await createBackup(original, "beforeLastRestore.ttl", (await currentText).value)
118 | await currentText.set(backupText)
119 | setShowRestore(false)
120 | handleClose({ closeAll: true })
121 | }, [original, backupText, handleClose])
122 | return (
123 | handleClose()}>
124 |
125 | {original && }
126 | handleClose()}>
127 |
128 |
129 |
130 |
131 | {backupText && (
132 | { }}>
133 |
134 |
135 |
136 |
137 | )}
138 |
139 |
140 | setShowRestore(true)}>
141 | Restore
142 |
143 |
144 | {
145 | showRestore && {
147 | setShowRestore(false)
148 | handleClose(opts)
149 | }}
150 | open={showRestore} />
151 | }
152 |
153 | )
154 | }
155 |
156 | type BackupProps = {
157 | backupFolder: string,
158 | backup: string,
159 | handleClose: HandleClose
160 | }
161 |
162 | function Backup({ backupFolder, backup, handleClose }: PropsWithChildren) {
163 | const [showPreview, setShowPreview] = useState(false)
164 | const meta = `${backupFolder}.meta`
165 | const [backupDate] = useDateQuery(backup, dct.modified, { source: meta })
166 | const handleClosePreview = useCallback(({ closeAll } = { closeAll: false }) => {
167 | setShowPreview(false)
168 | if (closeAll) {
169 | handleClose({ closeAll })
170 | }
171 | }, [handleClose])
172 | return (
173 |
174 | {backupDate && backupDate.toLocaleString()}
175 | {backup && backup.split("/").slice(-1)[0]}
176 |
177 | setShowPreview(true)}>Show Preview
178 | {showPreview && handleClosePreview(opts)} />}
180 |
181 |
182 | )
183 | }
184 |
185 | type BackupsProps = {
186 | document: Document,
187 | handleClose: HandleClose
188 | }
189 |
190 | export default function Backups({ document, handleClose }: BackupsProps) {
191 | const backupFolder = backupFolderForPage(document.uri)
192 | const [backups] = useListValuesQuery(backupFolder, ldp.contains)
193 | const classes = useStyles()
194 | return (
195 |
196 |
197 |
198 |
199 | Date
200 | Name
201 |
202 |
203 |
204 |
205 | {
206 | backups && backups.map((backup: string) => (
207 |
208 | {backup}
209 |
210 | ))
211 | }
212 |
213 |
214 |
215 | )
216 | }
217 |
--------------------------------------------------------------------------------
/src/components/BackupsDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Dialog from '@material-ui/core/Dialog';
5 | import DialogContent from '@material-ui/core/DialogContent';
6 | import DialogContentText from '@material-ui/core/DialogContentText';
7 | import DialogTitle from '@material-ui/core/DialogTitle';
8 | import IconButton from '@material-ui/core/IconButton';
9 | import CloseIcon from '@material-ui/icons/Close';
10 |
11 | import { schema } from 'rdf-namespaces';
12 |
13 | import { useValueQuery } from '../hooks/data';
14 | import Backups from './Backups';
15 |
16 | const useStyles = makeStyles(theme => ({
17 | closeButton: {
18 | position: 'absolute',
19 | right: theme.spacing(1),
20 | top: theme.spacing(1),
21 | color: theme.palette.grey[500],
22 | }
23 | }));
24 |
25 | export default function BackupsDialog({document, open, handleClose}) {
26 | const [name] = useValueQuery(document.uri, schema.name);
27 | const classes = useStyles();
28 | return (
29 |
30 |
31 | Backups
32 |
33 |
34 |
35 |
36 |
37 |
38 | Backups of {name}
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ChecklistItemElement.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | useEditor, useReadOnly, ReactEditor
5 | } from 'slate-react';
6 | import { Transforms } from 'slate';
7 | import Checkbox from '@material-ui/core/Checkbox';
8 | import { makeStyles } from '@material-ui/core/styles';
9 |
10 | const useStyles = makeStyles(theme => ({
11 | container: {
12 | display: "flex",
13 | flexDirection: "row",
14 | alignItems: "center",
15 | "& + &": {
16 | marginTop: 0
17 | }
18 | },
19 | checkbox: {
20 | padding: theme.spacing(0.5),
21 | marginRight: theme.spacing(0.5)
22 | },
23 | text: {
24 | flex: 1,
25 | opacity: ({checked}) => checked ? 0.666 : 1,
26 | textDecoration: ({checked}) => checked ? 'line-through' : 'none',
27 | "&:focus": {
28 | outline: "none"
29 | }
30 | }
31 | }))
32 |
33 | export default function CheckListItemElement({ attributes, children, element }) {
34 | const editor = useEditor()
35 | const readOnly = useReadOnly()
36 | const { checked } = element
37 | const classes = useStyles({checked})
38 | return (
39 |
43 | {
50 | const path = ReactEditor.findPath(editor, element)
51 | Transforms.setNodes(
52 | editor,
53 | { checked: event.target.checked },
54 | { at: path }
55 | )
56 | }}
57 | />
58 |
63 | {children}
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/ConceptCreator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext, useState, FunctionComponent } from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Button from '@material-ui/core/Button';
5 | import Dialog, { DialogProps } from '@material-ui/core/Dialog';
6 | import DialogContent from '@material-ui/core/DialogContent';
7 | import DialogContentText from '@material-ui/core/DialogContentText';
8 | import DialogActions from '@material-ui/core/DialogActions';
9 | import TextField from '@material-ui/core/TextField';
10 |
11 | import { useDebounce } from 'use-debounce';
12 |
13 | import Loader from './Loader';
14 | import WorkspaceContext from "../context/workspace";
15 | import { resourceExists } from '../utils/ldflex-helper'
16 | import { conceptUri } from '../utils/urls'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | nameTextField: {
20 | width: "100%",
21 | marginTop: theme.spacing(2),
22 | }
23 | }))
24 |
25 | type ConceptCreatorProps = DialogProps & {
26 | close: () => void
27 | }
28 |
29 | const ConceptCreator: FunctionComponent = ({ close, ...props }) => {
30 | const classes = useStyles()
31 | const { workspace, addConcept } = useContext(WorkspaceContext);
32 | const [saving, setSaving] = useState(false)
33 | const [nameCheckNeeded, setNameCheckNeeded] = useState(false)
34 | const [conceptExists, setConceptExists] = useState()
35 | const [error, setError] = useState(null)
36 | const [name, setName] = useState("")
37 | const [debouncedName] = useDebounce(name, 500);
38 |
39 | const save = async () => {
40 | setError(null)
41 | setSaving(true)
42 | addConcept && await addConcept({ name: debouncedName })
43 | setSaving(false)
44 | close()
45 | }
46 | useEffect(() => {
47 | const checkName = async () => {
48 | if (workspace && (debouncedName !== "")) {
49 | setConceptExists(await resourceExists(conceptUri(workspace.conceptContainerUri, debouncedName)))
50 | }
51 | }
52 | if (nameCheckNeeded) {
53 | setNameCheckNeeded(false)
54 | checkName()
55 | }
56 | }, [debouncedName, nameCheckNeeded, workspace])
57 | useEffect(() => setNameCheckNeeded(true), [debouncedName])
58 | return (
59 | e.stopPropagation()}>
60 |
61 | {error && (
62 |
63 | {error}
64 |
65 | )}
66 | {(conceptExists === true) && (
67 |
68 | {debouncedName} already exists
69 |
70 | )}
71 | setName(e.target.value)} />
75 |
76 |
77 | {saving ? (
78 |
79 | ) : (
80 | <>
81 |
82 | save
83 |
84 | close && close()}>
85 | cancel
86 |
87 | >
88 | )}
89 |
90 |
91 | )
92 | }
93 |
94 | export default ConceptCreator
95 |
--------------------------------------------------------------------------------
/src/components/Console.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { LiveUpdate } from "@solid/react";
3 |
4 | import Link from '@material-ui/core/Link';
5 | import Button from '@material-ui/core/Button';
6 |
7 | import { addPage } from "../utils/model"
8 | import { useWorkspace, usePageListItems } from "../hooks/data"
9 |
10 | function Workspace({workspace}){
11 | const [pageListItems] = usePageListItems(workspace)
12 | return (
13 |
14 | {workspace &&
{workspace.uri}}
15 |
addPage(workspace)}>add child
16 | {pageListItems && pageListItems.map(item =>
{item.pageUri}
)}
17 |
18 | )
19 | }
20 |
21 | export default function Console(){
22 | const workspace = useWorkspace()
23 | return (
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/CurrentConcept.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Box from '@material-ui/core/Box';
5 |
6 | import { LiveUpdate } from "@solid/react";
7 | import { useCurrentConceptUri, useCurrentConcept } from '../hooks/concepts';
8 | import Page from './Document'
9 | import Loader from './Loader'
10 |
11 | const useStyles = makeStyles(theme => ({
12 | content: {
13 | flexGrow: 1,
14 | position: "relative",
15 | height: "100%"
16 | },
17 | }));
18 |
19 | function ConceptInsideLiveUpdate() {
20 | const [currentConcept] = useCurrentConcept()
21 | return currentConcept ? ( ) : ( )
22 | }
23 |
24 | export default function CurrentConcept() {
25 | const classes = useStyles()
26 | const currentConceptUri = useCurrentConceptUri()
27 | return (
28 |
29 | {
30 | currentConceptUri ? (
31 |
32 |
33 |
34 | ) : (
35 |
36 | )
37 | }
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/CurrentPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Box from '@material-ui/core/Box';
5 |
6 | import { LiveUpdate } from "@solid/react";
7 | import { useCurrentPageUri, useCurrentPage } from '../hooks/pages';
8 | import Page from './Document'
9 | import Loader from './Loader'
10 |
11 | const usePagesStyles = makeStyles(theme => ({
12 | content: {
13 | flexGrow: 1,
14 | position: "relative",
15 | height: "100%"
16 | },
17 | }));
18 |
19 | function PageInsideLiveUpdate() {
20 | const [currentPage] = useCurrentPage()
21 | return currentPage ? ( ) : ( )
22 | }
23 |
24 | export default function CurrentPage() {
25 | const classes = usePagesStyles()
26 | const currentPageUri = useCurrentPageUri()
27 | return (
28 |
29 | {
30 | currentPageUri ? (
31 |
32 |
33 |
34 | ) : (
35 |
36 | )
37 | }
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Document.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect, useRef, ReactNode, FunctionComponent } from 'react'
2 |
3 | import { LiveUpdate } from "@solid/react";
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import IconButton from './IconButton';
7 | import TextField from '@material-ui/core/TextField';
8 | import Typography from '@material-ui/core/Typography';
9 | import AppBar from '@material-ui/core/AppBar';
10 | import Toolbar from '@material-ui/core/Toolbar';
11 | import Menu, { MenuProps } from '@material-ui/core/Menu';
12 | import MenuItem from '@material-ui/core/MenuItem';
13 | import Dialog from '@material-ui/core/Dialog';
14 | import DialogTitle from '@material-ui/core/DialogTitle';
15 | import DialogActions from '@material-ui/core/DialogActions';
16 | import Button from '@material-ui/core/Button';
17 | import Box from '@material-ui/core/Box';
18 | import Paper from '@material-ui/core/Paper';
19 |
20 | import ShareIcon from '@material-ui/icons/Share'
21 | import BackupIcon from '@material-ui/icons/Backup'
22 | import MenuIcon from '@material-ui/icons/Menu'
23 | import DeveloperModeIcon from '@material-ui/icons/DeveloperMode'
24 |
25 | import { useHistory } from "react-router-dom";
26 |
27 | import Link from './Link';
28 | import SharingModal from "./SharingModal";
29 | import BackupsDialog from "./BackupsDialog";
30 | import Loader from "./Loader";
31 |
32 | import WorkspaceContext from "../context/workspace";
33 | import DocumentContext from '../context/document'
34 |
35 | import DocumentTextEditor from './DocumentTextEditor'
36 | import ReferencedByList from './ReferencedByList'
37 | import { useAccessInfo } from '../hooks/acls';
38 | import { drawerWidth } from '../constants'
39 | import { Page, Document, isConcept, isPage } from '../utils/model'
40 | import { usePreferences } from '../context/preferences'
41 |
42 | const useStyles = makeStyles(theme => ({
43 | appBar: {
44 | width: ({ hasWorkspace }: any) => hasWorkspace ? `calc(100% - ${drawerWidth}px)` : "100%",
45 | marginLeft: ({ hasWorkspace }: any) => hasWorkspace ? drawerWidth : 0,
46 | background: "white"
47 | },
48 | shareButton: {
49 | float: "right"
50 | },
51 | grow: {
52 | flexGrow: 1,
53 | },
54 | sectionDesktop: {
55 | display: 'flex',
56 | },
57 | referencedBy: {
58 | flexGrow: 2
59 | },
60 | error: {
61 | marginTop: theme.spacing(6)
62 | }
63 | }));
64 |
65 | type PageNameProps = {
66 | page: Page
67 | }
68 |
69 | function PageName({ page }: PageNameProps) {
70 | const { updateName } = useContext(WorkspaceContext);
71 | const [editing, setEditing] = useState(false);
72 | const [name, setName] = useState(page.name);
73 | useEffect(() => {
74 | page.name && setName(page.name);
75 | }, [page.name, page.uri])
76 |
77 | const saveAndStopEditing = async () => {
78 | setEditing(false)
79 | if (updateName) {
80 | await updateName(page, name)
81 | }
82 | }
83 |
84 | return editing ? (
85 | (e.key === 'Enter') && saveAndStopEditing()}
88 | onBlur={() => saveAndStopEditing()}
89 | onChange={(e) => setName(e.target.value)} />
90 | ) : (
91 | setEditing(true)} noWrap>{name}
92 | );
93 | }
94 |
95 | type DocumentNameProps = {
96 | document: Document
97 | }
98 |
99 | function DocumentName({ document }: DocumentNameProps) {
100 | return (
101 | {document.name}
102 | );
103 | }
104 |
105 | type PE = FunctionComponent<{
106 | error: any
107 | }>
108 |
109 | const PageError: PE = ({ error }) => {
110 | const classes = useStyles()
111 | console.log("showing error ui for ", error)
112 | return (Sorry, something went wrong.
)
113 |
114 | }
115 |
116 | type EditorErrorBoundaryProps = { children: ReactNode }
117 | type EditorErrorBoundaryState = { hasError: boolean, error: any }
118 |
119 | class EditorErrorBoundary extends React.Component {
120 | constructor(props: EditorErrorBoundaryProps) {
121 | super(props);
122 | this.state = { hasError: false, error: null };
123 | }
124 |
125 | static getDerivedStateFromError(error: any) {
126 | // Update state so the next render will show the fallback UI.
127 | return { hasError: true, error };
128 | }
129 |
130 | componentDidCatch(error: any, errorInfo: any) {
131 | // You can also log the error to an error reporting service
132 | console.log("error rendering editable", error, errorInfo);
133 | }
134 |
135 | render() {
136 | if (this.state.hasError) {
137 | return ( )
138 | } else {
139 | return this.props.children;
140 | }
141 | }
142 | }
143 |
144 | interface AppBarMenuProps extends MenuProps {
145 | open: boolean,
146 | document: Document,
147 | onClose: () => void
148 | }
149 |
150 | function AppBarMenu({ document, onClose, ...props }: AppBarMenuProps) {
151 | const history = useHistory();
152 | const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false)
153 | const { deleteDocument } = useContext(WorkspaceContext);
154 | const close = () => {
155 | setDeleteConfirmationOpen(false)
156 | onClose()
157 | }
158 | return (
159 | <>
160 |
161 |
162 |
163 | Source
164 |
165 |
166 | setDeleteConfirmationOpen(true)}>Delete
167 |
168 |
169 | Are you sure you want to delete this page?
170 |
171 | {
172 | if (deleteDocument) {
173 | deleteDocument(document)
174 | }
175 | close()
176 | history.replace("/")
177 | }}>
178 | yes
179 |
180 |
181 | no
182 |
183 |
184 |
185 | >
186 | )
187 | }
188 |
189 | type DocumentProps = {
190 | document: Document
191 | }
192 |
193 | const DocumentComponent: FunctionComponent = ({ document }) => {
194 | const { workspace } = useContext(WorkspaceContext)
195 | const menuButton = useRef(null);
196 | const classes = useStyles({ hasWorkspace: !!workspace });
197 | const [sharingModalOpen, setSharingModalOpen] = useState(false);
198 | const [backupsDialogOpen, setBackupsDialogOpen] = useState(false);
199 | const [menuOpen, setMenuOpen] = useState(false)
200 | const { aclUri, allowed } = useAccessInfo(document.uri)
201 | const readOnly = !(allowed && allowed.user.has("write"))
202 | const { devMode, setDevMode } = usePreferences()
203 | return (document === undefined) ? ( ) : (
204 |
205 |
206 |
207 | {isPage(document) ? (
208 |
209 | ) : (
210 |
211 | )}
212 |
213 |
214 | {
215 | allowed && allowed.user.has("control") && (
216 | <>
217 | setDevMode(!devMode)}>
219 |
220 |
221 | setSharingModalOpen(!sharingModalOpen)}>
223 |
224 |
225 | setBackupsDialogOpen(!backupsDialogOpen)}>
227 |
228 |
229 | setMenuOpen(!menuOpen)} >
231 |
232 |
233 | >
234 | )
235 | }
236 |
237 |
238 |
239 | setMenuOpen(false)}
242 | keepMounted
243 | anchorOrigin={{
244 | vertical: 'top',
245 | horizontal: 'right',
246 | }}
247 | transformOrigin={{
248 | vertical: 'top',
249 | horizontal: 'right',
250 | }} />
251 | {workspace && aclUri && (
252 |
253 | {document && ( setSharingModalOpen(false)} />)}
254 |
255 | )}
256 | {backupsDialogOpen && setBackupsDialogOpen(false)} />}
257 | {allowed && allowed.user.has("read") && (
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | {isConcept(document) && (
267 |
268 |
269 |
270 |
271 |
272 | )}
273 |
274 | )}
275 |
276 | )
277 | }
278 |
279 | export default DocumentComponent;
280 |
--------------------------------------------------------------------------------
/src/components/DocumentTextEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useRef, useCallback, useEffect } from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Paper from '@material-ui/core/Paper';
5 | import Box from '@material-ui/core/Box';
6 | import SaveIcon from '@material-ui/icons/Save'
7 | import { useDebounce } from 'use-debounce';
8 |
9 | import { Slate, useSlate } from 'slate-react';
10 | import { Node } from 'slate';
11 |
12 | import { HoveringToolbar } from "./EditorToolbar";
13 | import Editable, { useNewEditor } from "./Editable";
14 | import { Document } from "../utils/model"
15 | import { getConceptNodes } from "../utils/slate"
16 | import WorkspaceContext from "../context/workspace";
17 | import { useBackups } from '../hooks/backup';
18 | import { usePreferences } from '../context/preferences'
19 |
20 | const useStyles = makeStyles(theme => ({
21 | saving: {
22 | position: "fixed",
23 | right: theme.spacing(0),
24 | top: "78px",
25 | zIndex: 1000,
26 | color: theme.palette.primary.light
27 | },
28 | editor: {
29 | height: "100%",
30 | overflow: "scroll"
31 | },
32 | editable: {
33 | marginTop: theme.spacing(6),
34 | textAlign: "left",
35 | padding: theme.spacing(1),
36 | paddingLeft: theme.spacing(8),
37 | paddingRight: theme.spacing(8),
38 | paddingTop: 0,
39 | background: "white",
40 | height: "100%",
41 | flexGrow: 2
42 | },
43 | devConsole: {
44 | flexGrow: 1,
45 | fontSize: "0.66em",
46 | textAlign: "left"
47 | }
48 | }));
49 |
50 | interface DocumentTextEditorProps {
51 | document: Document,
52 | readOnly: boolean
53 | }
54 |
55 | export default function DocumentTextEditor({ document, readOnly }: DocumentTextEditorProps) {
56 | const documentUri = document.uri
57 | const { updateText } = useContext(WorkspaceContext);
58 | const classes = useStyles();
59 | const [saving, setSaving] = useState(false);
60 | const documentText = document.text;
61 | const [editorValue, setEditorValue] = useState(undefined);
62 | const [saveNeeded, setSaveNeeded] = useState(false);
63 | const [debouncedValue] = useDebounce(editorValue, 1500);
64 | const savedVersionsRef = useRef>([])
65 | const setSavedVersions = useCallback<(mutate: (current: Array) => Array) => void>(
66 | (mutate) => {
67 | savedVersionsRef.current = mutate(savedVersionsRef.current)
68 | },
69 | [savedVersionsRef]
70 | )
71 | const editor = useNewEditor()
72 |
73 | useEffect(() => {
74 | // set editor text to null when the document changes so we won't save document text from another document to the current document
75 | editor.children = undefined
76 | setEditorValue(undefined);
77 | savedVersionsRef.current = []
78 | }, [editor, documentUri])
79 |
80 | useBackups(document, editorValue)
81 |
82 | const previouslySaved = useCallback(
83 | (text) => savedVersionsRef.current.some(previousVersion => previousVersion === text),
84 | [savedVersionsRef]
85 | )
86 |
87 | useEffect(() => {
88 | // once documentText loads, set editorValue
89 | if ((documentText !== undefined) && (documentText !== null)) {
90 | setEditorValue(currentValue => {
91 | if ((JSON.stringify(currentValue) === documentText) ||
92 | previouslySaved(documentText)) {
93 | return currentValue
94 | } else {
95 | return JSON.parse(documentText)
96 | }
97 | })
98 | }
99 | // include documentUri here so that we run this after setting editorValue to undefined above
100 | }, [documentUri, documentText, previouslySaved, savedVersionsRef]);
101 |
102 | useEffect(() => {
103 | const maybeSave = async () => {
104 | const saveableText = JSON.stringify(debouncedValue);
105 | if (saveableText !== documentText) {
106 | setSaving(true);
107 | if (updateText) {
108 | const conceptUris = getConceptNodes(editor).map(([concept]) => concept.uri)
109 | await updateText(document, saveableText, conceptUris);
110 | }
111 | setSavedVersions(currentSavedVersions => [saveableText, ...currentSavedVersions].slice(0, 100))
112 | setSaving(false);
113 | }
114 | }
115 | if (saveNeeded) {
116 | setSaveNeeded(false);
117 | maybeSave();
118 | }
119 | }, [saveNeeded, document, documentText, debouncedValue, updateText, setSavedVersions, editor])
120 |
121 | useEffect(() => {
122 | if (debouncedValue !== undefined) {
123 | setSaveNeeded(true);
124 | }
125 | }, [debouncedValue])
126 | const { devMode } = usePreferences()
127 | return (
128 |
129 | {saving && }
130 | {editorValue === undefined ? (
131 | Loading...
132 | ) : (
133 | setEditorValue(newValue)}>
136 | {!readOnly && (
137 | <>
138 |
139 | >
140 | )}
141 |
142 |
144 | {devMode && ( )}
145 |
146 |
147 | )}
148 |
149 | );
150 | }
151 |
152 | const DevConsole = () => {
153 | const editor = useSlate()
154 | const classes = useStyles()
155 | return (
156 |
157 |
158 |
159 | {JSON.stringify(editor.children, null, 2)}
160 |
161 |
162 | {JSON.stringify(editor.selection, null, 2)}
163 |
164 |
165 |
166 | )
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/Editable.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, ReactNode, FunctionComponent } from 'react';
2 | import { createEditor, Text, Editor } from 'slate';
3 | import { Editable as SlateEditable, withReact } from 'slate-react';
4 | import isHotkey from 'is-hotkey';
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import { useTranslation } from 'react-i18next';
7 |
8 | import { withHistory } from 'slate-history';
9 |
10 | import {
11 | withImages, withLinks, withChecklists, withLists, toggleMark,
12 | withTables, withEmbeds, withConcepts
13 | } from '../utils/editor';
14 |
15 | import ChecklistItemElement from './ChecklistItemElement'
16 | import EmbedElement from './EmbedElement'
17 | import LinkElement from './edit/LinkElement'
18 | import ConceptElement from './edit/ConceptElement'
19 | import ImageElement from './edit/ImageElement'
20 | import Block from './edit/Block'
21 | import Table from "./edit/Table"
22 | import { ElementProps } from "./edit/"
23 |
24 | const useStyles = makeStyles(theme => ({
25 | blockquote: {
26 | backgroundColor: theme.palette.grey[100],
27 | marginLeft: theme.spacing(1),
28 | marginTop: 0,
29 | marginRight: theme.spacing(1),
30 | marginBottom: 0,
31 | paddingLeft: theme.spacing(1),
32 | paddingTop: theme.spacing(1),
33 | paddingRight: theme.spacing(1),
34 | paddingBottom: theme.spacing(1)
35 | },
36 | orderedList: {
37 | paddingLeft: theme.spacing(3)
38 | },
39 | unorderedList: {
40 | paddingLeft: theme.spacing(3)
41 | },
42 | paragraph: {
43 | marginTop: theme.spacing(1),
44 | marginBottom: theme.spacing(1),
45 | whiteSpace: "pre-wrap",
46 | wordBreak: "break-word"
47 | },
48 | table: {
49 | border: "1px solid black",
50 | borderCollapse: "collapse"
51 | },
52 | tr: {
53 | },
54 | td: {
55 | border: `2px solid ${theme.palette.grey[300]}`,
56 | padding: theme.spacing(1)
57 | },
58 | columnButtons: {
59 | display: "flex",
60 | flexDirection: "column",
61 | verticalAlign: "top"
62 | },
63 | rowButtons: {
64 | display: "flex"
65 | },
66 | }))
67 |
68 | const HOTKEYS: { [key: string]: string } = {
69 | 'mod+b': 'bold',
70 | 'mod+i': 'italic',
71 | 'mod+u': 'underline',
72 | 'mod+`': 'code',
73 | }
74 |
75 | export type LeafProps = {
76 | attributes: { [key: string]: any },
77 | children: ReactNode,
78 | leaf: Text
79 | }
80 |
81 | const Leaf: FunctionComponent = ({ attributes, children, leaf }) => {
82 | if (leaf.bold) {
83 | children = {children}
84 | }
85 |
86 | if (leaf.code) {
87 | children = {children}
88 | }
89 |
90 | if (leaf.italic) {
91 | children = {children}
92 | }
93 |
94 | if (leaf.underline) {
95 | children = {children}
96 | }
97 |
98 | return {children}
99 | }
100 |
101 |
102 |
103 | const Element: FunctionComponent = (props) => {
104 | const { attributes, children, element } = props;
105 | const classes = useStyles()
106 | switch (element.type) {
107 | case 'embed':
108 | return
109 | case 'block-quote':
110 | return {children}
111 | case 'heading-one':
112 | return {children}
113 | case 'heading-two':
114 | return {children}
115 | case 'heading-three':
116 | return {children}
117 | case 'bulleted-list':
118 | return
119 | case 'numbered-list':
120 | return {children}
121 | case 'list-item':
122 | return {children}
123 | case 'image':
124 | return
125 | case 'link':
126 | return
127 | case 'concept':
128 | return
129 | case 'check-list-item':
130 | return
131 | case 'table':
132 | return (
133 |
134 |
135 |
136 | )
137 | case 'table-row':
138 | return {children}
139 | case 'table-cell':
140 | return {children}
141 | default:
142 | return {children}
143 | }
144 | }
145 |
146 | export const useNewEditor = () => useMemo(() => withConcepts(withEmbeds(withTables(withLists(withChecklists(withLinks(withImages(withReact(withHistory(createEditor()))))))))), [])
147 |
148 | type EditableProps = {
149 | editor: Editor,
150 | readOnly?: boolean
151 | } & React.TextareaHTMLAttributes
152 |
153 | const Editable: FunctionComponent = ({ editor, ...props }) => {
154 | const renderLeaf = useCallback(props => , [])
155 | const renderElement = useCallback(props => , [])
156 | const { t } = useTranslation()
157 | return {
163 | for (const hotkey in HOTKEYS) {
164 | if (isHotkey(hotkey, event.nativeEvent)) {
165 | event.preventDefault()
166 | const mark = HOTKEYS[hotkey]
167 | toggleMark(editor, mark)
168 | }
169 | }
170 | }}
171 | {...props} />
172 | }
173 |
174 | export default Editable
175 |
--------------------------------------------------------------------------------
/src/components/EditorToolbar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, useContext } from 'react';
2 | import { ReactEditor, useSlate, useEditor } from 'slate-react';
3 | import { Editor, Range, Transforms } from 'slate'
4 |
5 | import { makeStyles, useTheme } from '@material-ui/core/styles';
6 |
7 | import Toolbar from '@material-ui/core/Toolbar';
8 | import TextField from '@material-ui/core/TextField';
9 | import Popover from '@material-ui/core/Popover';
10 | import Button from '@material-ui/core/Button';
11 | import FormatBold from '@material-ui/icons/FormatBold';
12 | import FormatItalic from '@material-ui/icons/FormatItalic';
13 | import FormatUnderlined from '@material-ui/icons/FormatUnderlined';
14 | import Code from '@material-ui/icons/Code';
15 | import FormatQuote from '@material-ui/icons/FormatQuote';
16 | import FormatListBulleted from '@material-ui/icons/FormatListBulleted';
17 | import FormatListNumbered from '@material-ui/icons/FormatListNumbered';
18 | import ImageIcon from '@material-ui/icons/ImageOutlined';
19 | import LinkIcon from '@material-ui/icons/Link';
20 | import CheckBox from '@material-ui/icons/CheckBoxOutlined';
21 | import ConceptIcon from '@material-ui/icons/Toll';
22 |
23 | import IconButton from './IconButton';
24 |
25 | import {
26 | isMarkActive, toggleMark, isBlockActive, toggleBlock, insertImage,
27 | isLinkActive, insertLink, isConceptActive, insertConcept
28 | } from '../utils/editor'
29 | import { conceptUri } from '../utils/urls'
30 | import WorkspaceContext from '../context/workspace'
31 |
32 | const useStyles = makeStyles(theme => ({
33 | toolbarRoot: {
34 | pointerEvents: "none"
35 | },
36 | toolbar: {
37 | pointerEvents: "auto"
38 | },
39 | insertLinkMenuRoot: {
40 | pointerEvents: "none"
41 | },
42 | insertLinkMenu: {
43 | pointerEvents: "auto"
44 | }
45 | }))
46 |
47 | const InsertImageButton = () => {
48 | const editor = useEditor()
49 | return (
50 | {
54 | event.preventDefault()
55 | const url = window.prompt('Enter the URL of the image:')
56 | if (!url) return
57 | insertImage(editor, url)
58 | }}
59 | >
60 |
61 |
62 | )
63 | }
64 |
65 | const LinkButton = ({onOpen, onClose}) => {
66 | const editor = useSlate()
67 | const [linkButtonOpen, setLinkButtonOpen] = useState(false)
68 | const [url, setUrl] = useState(null)
69 | const [selection, setSelection] = useState(undefined)
70 | const ref = useRef()
71 | const onClosePopover = () => {
72 | setLinkButtonOpen(false)
73 | onClose()
74 | }
75 | const insertAndClose = () => {
76 | Transforms.select(editor, selection)
77 | insertLink(editor, url)
78 | onClosePopover()
79 | }
80 | return (
81 | <>
82 | {
88 | onOpen()
89 | setSelection(editor.selection)
90 | setLinkButtonOpen(!linkButtonOpen)
91 | }}
92 | >
93 |
94 |
95 |
108 | {
112 | if (e.keyCode === 13) {
113 | e.preventDefault()
114 | insertAndClose()
115 | }
116 | }}
117 | onChange={e => {
118 | setUrl(e.target.value)
119 | }}/>
120 | {
121 | insertAndClose()
122 | }}>
123 | Link
124 |
125 |
126 | >
127 | )
128 | }
129 |
130 | const MarkButton = ({ format, icon, ...props }) => {
131 | const editor = useSlate()
132 | return (
133 | {
137 | event.preventDefault()
138 | toggleMark(editor, format)
139 | }}
140 | {...props}
141 | >
142 | {icon}
143 |
144 | )
145 | }
146 |
147 | const selectedText = (editor) => {
148 | if (editor && editor.selection) {
149 | if (Range.isCollapsed(editor.selection)){
150 | return ""
151 | } else {
152 | return Editor.string(editor, editor.selection)
153 | }
154 | } else {
155 | return null
156 | }
157 | }
158 |
159 | const ConceptButton = ({onOpen, onClose}) => {
160 | const {workspace} = useContext(WorkspaceContext)
161 | const editor = useSlate()
162 | const [conceptButtonOpen, setConceptButtonOpen] = useState(false)
163 | const [name, setName] = useState(null)
164 | const [selection, setSelection] = useState(undefined)
165 | const ref = useRef()
166 | const onClosePopover = () => {
167 | setConceptButtonOpen(false)
168 | onClose()
169 | }
170 | const insertAndClose = () => {
171 | Transforms.select(editor, selection)
172 | insertConcept(editor, name, conceptUri(workspace.conceptContainerUri, name))
173 | onClosePopover()
174 | }
175 | return (
176 | <>
177 | {
183 | onOpen()
184 | setSelection(editor.selection)
185 | setName(selectedText(editor))
186 | setConceptButtonOpen(!conceptButtonOpen)
187 | }}
188 | >
189 |
190 |
191 |
204 | {
208 | if (e.keyCode === 13) {
209 | e.preventDefault()
210 | insertAndClose()
211 | }
212 | }}
213 | onChange={e => setName(e.target.value)}/>
214 | {
215 | insertAndClose()
216 | }}>
217 | insert
218 |
219 |
220 | >
221 | )
222 | }
223 |
224 | const BlockButton = ({ format, icon, ...props }) => {
225 | const editor = useSlate()
226 | return (
227 | {
231 | event.preventDefault()
232 | toggleBlock(editor, format)
233 | }}
234 | {...props}
235 | >
236 | {icon}
237 |
238 | )
239 | }
240 |
241 | export default function EditorToolbar(props){
242 | return (
243 |
244 |
245 |
246 | } />
247 | } />
248 | } />
249 |
250 | } />
251 |
252 | )
253 | }
254 |
255 | export function HoveringToolbar() {
256 | const [submenuOpen, setSubmenuOpen] = useState(false)
257 | const editor = useSlate()
258 | const open = submenuOpen || !!(editor.selection && !Range.isCollapsed(editor.selection))
259 | const theme = useTheme()
260 | const classes = useStyles()
261 |
262 | const [anchorPosition, setAnchorPosition] = useState({top: 0, left: 0})
263 | const onOpenSubmenu = () => {
264 | setSubmenuOpen(true)
265 | }
266 | const onCloseSubmenu = () => {
267 | setSubmenuOpen(false)
268 | setTimeout(() => {
269 | ReactEditor.focus(editor)
270 | const selection = editor.selection
271 | // manual focus seems to trigger a process that resets the selection, so reset it
272 | setTimeout(() => {
273 | Transforms.select(editor, selection)
274 | // this 200 second timeout here is wonky but seems to work
275 | // TODO: test on other platforms, figure out a better way to do this
276 | // this will probably be fixed by https://github.com/ianstormtaylor/slate/issues/3412
277 | }, 150)
278 | }, 0)
279 | }
280 | useEffect(() => {
281 | if (editor.selection){
282 | const domSelection = window.getSelection()
283 | const domRange = domSelection.getRangeAt(0)
284 | const rect = domRange.getBoundingClientRect()
285 | setAnchorPosition({top: rect.top - theme.spacing(1), left: rect.left})
286 | }
287 | }, [editor.selection, theme])
288 | return (
289 |
307 | } />
308 | } />
309 | } />
310 | } />
311 |
312 |
313 |
314 | )
315 | }
316 |
--------------------------------------------------------------------------------
/src/components/EmbedElement.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 |
3 | import auth from 'solid-auth-client'
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 |
7 | import {BlockLoader} from './Loader';
8 |
9 | const useStyles = makeStyles(theme => ({
10 | table: {
11 | border: "1px solid black",
12 | borderCollapse: "collapse"
13 | },
14 | tr: {
15 | },
16 | td: {
17 | border: `2px solid ${theme.palette.grey[300]}`,
18 | padding: theme.spacing(1)
19 | }
20 | }))
21 |
22 | const CSVEmbed = ({url}) => {
23 | const classes = useStyles()
24 | const [data, setData] = useState()
25 | const headerData = data && (data.length > 0) && data[0]
26 | const bodyData = data && (data.length > 0) && data.slice(1)
27 | useEffect(() => {
28 | async function loadData(){
29 | const response = await auth.fetch(url)
30 | if (response.ok){
31 | const text = await response.text()
32 | const rows = text.split("\n")
33 | setData(rows.map(row => row.split(",")))
34 | } else {
35 | console.log("data failed to load: ", response)
36 | }
37 | }
38 | loadData()
39 | }, [url])
40 | return data ? (
41 |
42 |
43 |
44 | {headerData.map((cell, i) => (
45 | {cell}
46 | ))}
47 |
48 |
49 |
50 | {bodyData.map((row, i) => (
51 |
52 | {row.map((cell, i) => (
53 |
54 | {cell}
55 |
56 | ))}
57 |
58 | ))}
59 |
60 |
61 | ) : (
62 |
63 | )
64 | }
65 |
66 | const Embed = ({type, url}) => {
67 | if (type === "text/csv") {
68 | return
69 | } else {
70 | return Don't know how to display {url}
71 | }
72 | }
73 |
74 | const EmbedElement = ({element, attributes, children, ...props}) => {
75 | return (
76 |
82 | )
83 | }
84 |
85 | export default EmbedElement
86 |
--------------------------------------------------------------------------------
/src/components/EmbedPicker.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 |
3 | import auth from 'solid-auth-client'
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Button from '@material-ui/core/Button';
7 | import Dialog from '@material-ui/core/Dialog';
8 | import DialogContent from '@material-ui/core/DialogContent';
9 | import DialogContentText from '@material-ui/core/DialogContentText';
10 | import DialogActions from '@material-ui/core/DialogActions';
11 | import TextField from '@material-ui/core/TextField';
12 |
13 | import Loader from './Loader';
14 |
15 | const useStyles = makeStyles(theme => ({
16 | urlTextField: {
17 | width: "100%",
18 | marginTop: theme.spacing(2),
19 | }
20 | }))
21 |
22 | export default function EmbedPicker({onSave, onClose, ...props}){
23 | const classes = useStyles()
24 | const [saving, setSaving] = useState()
25 | const [error, setError] = useState()
26 | const [url, setUrl] = useState()
27 | const save = async () => {
28 | setError(null)
29 | setSaving(true)
30 | try {
31 | const response = await auth.fetch(url, {method: 'HEAD'})
32 | if (response.ok){
33 | const type = response.headers.get("content-type").split(";")[0]
34 | await onSave(url, type)
35 | } else {
36 | setError(`Error loading ${url}`)
37 | }
38 | } catch (e){
39 | if (e.name === "TypeError"){
40 | setError(`Could not load ${url}: the resource probably doesn't exist or does not have the correct CORS configuration.`)
41 | } else {
42 | setError(`Error loading ${url}: ${e.message}`)
43 | }
44 | }
45 | setSaving(false)
46 | }
47 | return (
48 | e.stopPropagation()}>
49 |
50 | {error && (
51 |
52 | {error}
53 |
54 | )}
55 | setUrl(e.target.value)}/>
59 |
60 |
61 | {saving ? (
62 |
63 | ) : (
64 | <>
65 |
66 | save
67 |
68 |
69 | cancel
70 |
71 | >
72 | )}
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/FAQ.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Typography from '@material-ui/core/Typography';
5 | import Grid from '@material-ui/core/Grid';
6 | import Link from '@material-ui/core/Link';
7 |
8 | import { titleFont } from '../utils/fonts'
9 |
10 | const useStyles = makeStyles(theme => ({
11 | question: {
12 | fontFamily: titleFont,
13 | marginTop: theme.spacing(3),
14 | marginBottom: theme.spacing(2)
15 | },
16 | answer: {
17 | ...theme.typography.body2,
18 | "& p": {
19 | marginBottom: theme.spacing(2)
20 | }
21 | }
22 | }))
23 |
24 | export const Question = ({children}) => {
25 | const classes = useStyles()
26 | return (
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export const Answer = ({children}) => {
38 | const classes = useStyles()
39 | return (
40 |
41 |
42 |
43 |
44 | {children}
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export const A = (props) =>
53 |
--------------------------------------------------------------------------------
/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default () => Welcome to Concept! Add or select a page on the left to get started.
4 |
--------------------------------------------------------------------------------
/src/components/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, Ref, PropsWithChildren } from 'react';
2 | import IconButton, { IconButtonProps } from '@material-ui/core/IconButton';
3 |
4 | import Tooltip from '@material-ui/core/Tooltip';
5 |
6 | import { makeStyles } from '@material-ui/core/styles';
7 |
8 | const useStyles = makeStyles(theme => ({
9 | active: {
10 | background: theme.palette.grey[300]
11 | },
12 | inactive: {
13 |
14 | }
15 | }))
16 |
17 | interface Props extends IconButtonProps {
18 | active?: boolean,
19 | title?: string,
20 | ariaLabel?: string
21 | }
22 |
23 | const MyIconButton = forwardRef(({ active, title, ariaLabel, ...props }: PropsWithChildren, ref: Ref) => {
24 | const classes = useStyles();
25 | const button = (
26 |
30 | )
31 | return title ? (
32 |
33 | {button}
34 |
35 | ) : button
36 | })
37 |
38 | export default MyIconButton
39 |
--------------------------------------------------------------------------------
/src/components/ImageUploader.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 |
3 | import { Transforms } from 'slate';
4 | import { useEditor } from 'slate-react';
5 | import Cropper from 'react-cropper';
6 | import 'cropperjs/dist/cropper.css';
7 | import uuid from 'uuid/v1';
8 | import auth from 'solid-auth-client';
9 |
10 | import { makeStyles } from '@material-ui/core/styles';
11 | import Button from '@material-ui/core/Button';
12 | import Dialog from '@material-ui/core/Dialog';
13 | import DialogContent from '@material-ui/core/DialogContent';
14 | import DialogActions from '@material-ui/core/DialogActions';
15 | import TextField from '@material-ui/core/TextField';
16 |
17 |
18 | import { insertionPoint, insertImage } from '../utils/editor';
19 | import Loader from './Loader';
20 |
21 | const useStyles = makeStyles(theme => ({
22 | uploader: {
23 | },
24 | cropper: {
25 | maxHeight: theme.spacing(50)
26 | },
27 | previewImage: {
28 | display: "block",
29 | height: theme.spacing(30),
30 | width: "auto",
31 | marginLeft: "auto",
32 | marginRight: "auto",
33 | },
34 | altTextField: {
35 | width: "100%",
36 | marginTop: theme.spacing(2),
37 | }
38 | }))
39 |
40 | const ImageEditingModal = ({src, onSave, onClose, ...props}) => {
41 | const [saving, setSaving] = useState()
42 | const classes = useStyles()
43 | const cropper = useRef()
44 | const save = async () => {
45 | setSaving(true)
46 | await onSave(cropper.current.getCroppedCanvas())
47 | setSaving(false)
48 | }
49 | return (
50 |
51 |
52 |
58 | {
59 | cropper.current.rotate(90)
60 | }}>
61 | rotate
62 |
63 |
64 |
65 | {saving ? (
66 |
67 | ) : (
68 | <>
69 |
70 | save
71 |
72 |
73 | cancel
74 |
75 | >
76 | )}
77 |
78 |
79 | )
80 | }
81 |
82 | const typesToExts = {
83 | "image/gif": "gif",
84 | "image/jpeg": "jpg",
85 | "image/png": "png",
86 | "image/svg+xml": "svg",
87 | "image/webp": "webp"
88 | }
89 |
90 | const extForFile = file => {
91 | const extFromType = typesToExts[file.type]
92 | if (extFromType) {
93 | return extFromType
94 | } else {
95 | return file.name.split(".").slice(-1)[0]
96 | }
97 | }
98 |
99 | const nameForFile = file => `${uuid()}.${extForFile(file)}`
100 |
101 | const uploadFromCanvas = (canvas, uri, type) => new Promise((resolve, reject) => {
102 | canvas.toBlob(async (blob) => {
103 | const response = await auth.fetch(uri, {
104 | method: 'PUT',
105 | force: true,
106 | headers: {
107 | 'content-type': type,
108 | credentials: 'include'
109 | },
110 | body: blob
111 | });
112 | if (response.ok){
113 | resolve(response)
114 | } else {
115 | reject(response)
116 | console.log("image upload failed: ", response)
117 | }
118 | }, type, 1)
119 |
120 | })
121 |
122 | const uploadFromFile = (file, uri) => new Promise((resolve, reject) => {
123 | const reader = new FileReader()
124 | reader.onload = async f => {
125 | const response = await auth.fetch(uri, {
126 | method: 'PUT',
127 | force: true,
128 | headers: {
129 | 'content-type': file.type,
130 | credentials: 'include'
131 | },
132 | body: f.target.result
133 | });
134 | if (response.ok){
135 | resolve(response)
136 | } else {
137 | reject(response)
138 | }
139 | }
140 | reader.readAsArrayBuffer(file);
141 | })
142 |
143 | const uriForOriginal = (editedUri) => {
144 | const parts = editedUri.split(".")
145 | return [...parts.slice(0, -1), "original", ...parts.slice(-1)].join(".")
146 | }
147 |
148 | export function ImageEditor({element, onClose, onSave, ...props}) {
149 |
150 | const {url, originalUrl, mime} = element
151 | return (
152 | {
155 | await uploadFromCanvas(canvas, url, mime)
156 | onSave(url)
157 | }} {...props}/>
158 | )
159 | }
160 |
161 | export default ({element, onClose, uploadDirectory, ...props}) => {
162 | const classes = useStyles()
163 | const inputRef = useRef()
164 | const editor = useEditor()
165 | const [file, setFile] = useState()
166 | const [originalSrc, setOriginalSrc] = useState()
167 | const [previewSrc, setPreviewSrc] = useState()
168 | const [altText, setAltText] = useState("")
169 | const [croppedCanvas, setCroppedCanvas] = useState()
170 | const [editing, setEditing] = useState(false)
171 |
172 | const insert = async () => {
173 | const editedUri = `${uploadDirectory}${nameForFile(file)}`
174 | const originalUri = uriForOriginal(editedUri)
175 | uploadFromFile(file, originalUri)
176 | await uploadFromCanvas(croppedCanvas, editedUri, file.type)
177 | const insertAt = insertionPoint(editor, element)
178 | insertImage(editor, {url: editedUri, originalUrl: originalUri, alt: altText, mime: file.type}, insertAt);
179 | Transforms.select(editor, insertAt)
180 | onClose()
181 | }
182 |
183 | useEffect(() => {
184 | let newSrc;
185 | if (file){
186 | newSrc = URL.createObjectURL(file)
187 | setOriginalSrc(newSrc)
188 | setPreviewSrc(newSrc)
189 | setEditing(true)
190 | }
191 | return () => {
192 | if (newSrc){
193 | URL.revokeObjectURL(newSrc)
194 | }
195 | }
196 | }, [file])
197 |
198 | const onFileChanged = event => {
199 | if (event.target.files) {
200 | const file = event.target.files[0]
201 | setFile(file)
202 | }
203 | }
204 |
205 | return (
206 |
207 |
208 | {previewSrc && (
209 | <>
210 |
211 | setAltText(e.target.value)}/>
214 | >
215 | )}
216 |
217 |
218 | inputRef.current.click()}>
219 | pick a file
220 |
221 | {croppedCanvas &&
222 | <>
223 | setEditing(true)}>
224 | edit
225 |
226 |
227 | insert
228 |
229 | >
230 | }
231 | onClose()}>
232 | cancel
233 |
234 |
235 |
242 | {
245 | setPreviewSrc(canvas.toDataURL(file.type))
246 | setCroppedCanvas(canvas)
247 | setEditing(false)
248 | }}/>
249 |
250 | )
251 | }
252 |
--------------------------------------------------------------------------------
/src/components/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Grid from '@material-ui/core/Grid';
7 | import Button from '@material-ui/core/Button';
8 |
9 | import { LogInButton } from './LogInLogOutButton'
10 | import { AppTitleGridRow } from './AppTitle'
11 | import SocialIcons from './SocialIcons'
12 | import logo from '../logo.svg'
13 |
14 |
15 | const useStyles = makeStyles(theme => ({
16 | name: {
17 | marginTop: theme.spacing(3),
18 | fontFamily: "'Special Elite', cursive"
19 | },
20 | logo: {
21 | width: theme.spacing(18)
22 | },
23 | actionButton: {
24 | fontSize: "1.6rem"
25 | },
26 | social: {
27 | marginTop: theme.spacing(6)
28 | },
29 | socialIcon: {
30 | color: "inherit"
31 | }
32 | }))
33 |
34 | function LandingPage() {
35 | const classes = useStyles({})
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Hello Friend!
49 |
50 | I'm sorry to let you know that Concept is no longer supported or
51 | actively developed.
52 |
53 |
54 | But good news!
55 |
56 |
57 | This code lives on (in some
58 | cases literally) in itme.online ,
59 | a Grant for the Web supported project from itme .
60 |
61 |
62 | If you're interested in following along, please subscribe to
63 | our blog or newsletter for
64 | the latest updates.
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
74 | sign up
75 |
76 |
77 |
78 | log in
79 |
80 |
81 |
82 |
83 |
84 |
85 | what is this?
86 |
87 |
88 |
89 | >
90 | )
91 | }
92 |
93 | export default LandingPage
94 |
--------------------------------------------------------------------------------
/src/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, forwardRef, Ref } from 'react'
2 | import { Link as RouterLink, LinkProps as RouterLinkProps } from "react-router-dom";
3 | import MuiLink, { LinkProps as MuiLinkProps } from '@material-ui/core/Link';
4 |
5 | type Partial = {
6 | [P in keyof T]?: T[P];
7 | }
8 |
9 | export type LinkProps = MuiLinkProps & Partial
10 |
11 | const Link: FunctionComponent = forwardRef((props, ref: Ref) => {
12 | if (props.href) {
13 | return
14 | } else {
15 | return
16 | }
17 | })
18 |
19 | export default Link
20 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react';
2 |
3 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"
4 | import Loader from 'react-loader-spinner'
5 | import { useTheme } from '@material-ui/core/styles';
6 |
7 | type Types = 'Triangle' | 'ThreeDots'
8 |
9 | type Props = { height?: number, width?: number, type?: Types, className?: string }
10 |
11 | const ConceptLoader = ({ height, width, type = "Triangle", ...props }: PropsWithChildren) => {
12 | const theme = useTheme();
13 | return (
14 |
19 | )
20 | }
21 |
22 | export const ButtonLoader = () =>
23 | export const BlockLoader = () =>
24 |
25 | export default ConceptLoader;
26 |
--------------------------------------------------------------------------------
/src/components/LogInLogOutButton.js:
--------------------------------------------------------------------------------
1 | import React, {useContext} from 'react';
2 | import Button from '@material-ui/core/Button';
3 |
4 | import AuthContext from "../context/auth"
5 | import {useLoggedIn} from '@solid/react';
6 |
7 | export function LogInButton(props) {
8 | const {logIn} = useContext(AuthContext)
9 | return (
10 | logIn()} {...props}/>
11 | )
12 | }
13 |
14 | export default function LogInLogOutButton(props) {
15 | const loggedIn = useLoggedIn();
16 | const {logIn, logOut} = useContext(AuthContext)
17 | return (
18 |
19 | {loggedIn ? "Log Out" : "Log In"}
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/ProfileLink.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import { foaf } from 'rdf-namespaces';
3 | import { useValueQuery } from '../hooks/data';
4 | import { handleHausUriForWebId, handleProfilePath, webIdProfilePath } from '../utils/urls'
5 | import Link, { LinkProps } from './Link'
6 |
7 |
8 | type ProfileLinkProps = { webId: string | undefined } & LinkProps
9 |
10 | const ProfileLink: FunctionComponent = ({ webId, ...props }) => {
11 | const [handle] = useValueQuery(webId && handleHausUriForWebId(webId), foaf.nick)
12 | const profilePath = handle ? handleProfilePath(handle) : webId ? webIdProfilePath(webId) : ""
13 | return (
14 |
15 | )
16 | }
17 |
18 | export default ProfileLink
19 |
--------------------------------------------------------------------------------
/src/components/PublicProfile.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { space, schema, vcard, foaf } from 'rdf-namespaces';
4 | import { useParams } from "react-router-dom";
5 | import { useWebId, LiveUpdate } from "@solid/react";
6 |
7 | import { makeStyles } from '@material-ui/core/styles';
8 | import List from '@material-ui/core/List';
9 | import ListItem from '@material-ui/core/ListItem';
10 | import Grid from '@material-ui/core/Grid';
11 | import Paper from '@material-ui/core/Paper';
12 | import Typography from '@material-ui/core/Typography';
13 | import Button from '@material-ui/core/Button';
14 |
15 | import { useListValuesQuery, useListQuery, useValueQuery } from '../hooks/data';
16 | import { follow, unfollow } from '../utils/data';
17 | import { conceptContainerUrl, publicPagesUrl } from '../utils/urls';
18 | import { metaForPageUri } from '../utils/model'
19 | import { pagePath } from '../utils/urls'
20 |
21 | import Loader, { ButtonLoader } from "./Loader";
22 | import Link from './Link'
23 | import ProfileLink from './ProfileLink'
24 |
25 | const useStyles = makeStyles(theme => ({
26 | profile: {
27 | padding: theme.spacing(2),
28 | height: "100%"
29 | },
30 | profileImage: {
31 | width: theme.spacing(10)
32 | },
33 | name: {
34 | textAlign: "left"
35 | },
36 | followButton: {
37 | ...theme.typography.button
38 | }
39 | }));
40 |
41 | function PublicPage({ pageUri }: { pageUri: string }) {
42 | const [name] = useValueQuery(pageUri, schema.name, { source: pageUri && metaForPageUri(pageUri) })
43 | return (
44 |
45 |
46 | {name || ""}
47 |
48 |
49 | )
50 | }
51 |
52 | function PublicPages({ url }: { url: string }) {
53 | const [pageUriTerms] = useListQuery(url, schema.itemListElement)
54 | return (
55 |
56 |
57 |
58 | Public Pages
59 |
60 |
61 | {pageUriTerms && pageUriTerms.map((pageUriTerm: any) => (
62 |
63 | ))}
64 |
65 | )
66 | }
67 |
68 | function Friend({ webId }: { webId: string }) {
69 | const [name] = useValueQuery(webId, vcard.fn);
70 | return (
71 |
72 |
73 | {name || ""}
74 |
75 |
76 | )
77 | }
78 |
79 | function Friends({ webId }: { webId: string }) {
80 | const [friends] = useListQuery(webId, foaf.knows)
81 | return (
82 |
83 |
84 |
85 | Friends
86 |
87 |
88 | {friends && friends.map((friend: any) => (
89 |
90 | ))}
91 |
92 | )
93 | }
94 |
95 | function FollowButton({ webId }: { webId: string }) {
96 | const currentUserWebId = useWebId();
97 | const [knowsWebIds, knowsLoading] = useListValuesQuery(currentUserWebId, foaf.knows);
98 | const knows = knowsWebIds && knowsWebIds.includes(webId)
99 | const onFollow = async () => {
100 | await follow(currentUserWebId, webId)
101 | }
102 | const onUnfollow = async () => {
103 | await unfollow(currentUserWebId, webId)
104 | }
105 | return (
106 |
107 | {knowsLoading ? (
108 |
109 | ) : (
110 | knows ? "Unfollow " : "Follow"
111 | )}
112 |
113 | )
114 | }
115 |
116 | function PublicInfo({ webId }: { webId: string }) {
117 | const currentUserWebId = useWebId();
118 | const [name] = useValueQuery(webId, vcard.fn);
119 | const [photo] = useValueQuery(webId, vcard.hasPhoto);
120 | const [storage] = useValueQuery(webId, space.storage);
121 | const conceptContainer = storage && conceptContainerUrl(storage)
122 | const publicPages = conceptContainer && publicPagesUrl(conceptContainer)
123 | const classes = useStyles()
124 | return (
125 | <>
126 |
127 |
128 | {photo && }
129 |
130 |
131 |
132 | {name}
133 |
134 |
135 |
136 |
137 | {webId && currentUserWebId && (webId !== currentUserWebId) && (
138 |
139 | )}
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | {publicPages && }
149 |
150 |
151 | >
152 | )
153 | }
154 |
155 | export function WebIdPublicProfile({ webId }: { webId: string }) {
156 | const classes = useStyles()
157 |
158 | return (
159 |
160 | {webId ? (
161 |
162 | ) : (
163 |
164 | )}
165 |
166 | )
167 | }
168 |
169 | export function EncodedWebIdPublicProfile() {
170 | const { encodedWebId } = useParams()
171 | if (encodedWebId) {
172 | return (
173 |
174 | )
175 | } else {
176 |
177 | }
178 | }
179 |
180 |
181 |
182 | export default function PublicProfile() {
183 | const { handle } = useParams();
184 | const [webId] = useValueQuery(handle && `https://handle.haus/handles/${handle}#Person`, "https://handle.haus/ontology#webId")
185 | return (
186 |
187 | )
188 | }
189 |
--------------------------------------------------------------------------------
/src/components/ReferencedByList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, FunctionComponent } from 'react'
2 |
3 | import { Slate } from 'slate-react';
4 |
5 | import { Ancestor, Node } from 'slate';
6 | import { schema } from 'rdf-namespaces';
7 |
8 | import { makeStyles } from '@material-ui/core/styles';
9 | import Typography from '@material-ui/core/Typography';
10 | import Paper from '@material-ui/core/Paper';
11 | import ExpansionPanel from '@material-ui/core/ExpansionPanel';
12 | import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
13 | import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
14 |
15 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
16 |
17 | import { useTranslation } from 'react-i18next';
18 |
19 | import Link from './Link';
20 | import Editable, { useNewEditor } from "./Editable";
21 | import { Concept, metaForPageUri } from '../utils/model'
22 | import { useValueQuery, usePage } from '../hooks/data'
23 | import { documentPath } from '../utils/urls'
24 | import { getConceptNodesMatchingName } from '../utils/slate'
25 |
26 | const useStyles = makeStyles(theme => ({
27 | excerpt: {
28 | padding: theme.spacing(1),
29 | backgroundColor: theme.palette.grey[50],
30 | marginTop: theme.spacing(1),
31 | marginBottom: theme.spacing(1)
32 | }
33 | }));
34 |
35 | type RBE = FunctionComponent<{
36 | node: Ancestor
37 | }>
38 |
39 | const ReferencedByExcerpt: RBE = ({ node }) => {
40 | const classes = useStyles()
41 | const editor = useNewEditor()
42 | return (
43 |
44 | { }}>
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | type RBID = FunctionComponent<{
52 | concept: Concept,
53 | referencedBy: string
54 | }>
55 |
56 | const ReferencedByItemDetails: RBID = ({ concept, referencedBy }) => {
57 | const [page] = usePage(referencedBy)
58 | const pageText = page && page.text
59 | const conceptUri = concept.uri
60 | const [excerptNodes, setExcerptNodes] = useState([])
61 | useEffect(() => {
62 | if (pageText) {
63 | const rootNode = { children: JSON.parse(pageText) }
64 | const paths = getConceptNodesMatchingName(rootNode, concept.name).map(([, path]) => path)
65 | setExcerptNodes(paths.map(path => Node.parent(rootNode, path)))
66 | }
67 | }, [pageText, conceptUri, concept.name])
68 | return (
69 |
70 | {excerptNodes.map((node, i) => )}
71 |
72 | )
73 | }
74 |
75 | type ReferencedByItemProps = {
76 | concept: Concept,
77 | referencedBy: string
78 | }
79 |
80 | const ReferencedByItem: FunctionComponent = ({ referencedBy, concept }) => {
81 | const [name] = useValueQuery(referencedBy, schema.name, { source: metaForPageUri(referencedBy) })
82 | return (
83 |
84 | }>
85 | {name && {name}}
86 |
87 |
88 |
89 |
90 |
91 | )
92 | }
93 |
94 | type ReferencedByListProps = {
95 | concept: Concept
96 | }
97 |
98 | const ReferencedByList: FunctionComponent = ({ concept }) => {
99 | const count = concept.referencedBy.length
100 | const { name } = concept
101 | const { t } = useTranslation()
102 | return (
103 | <>
104 |
105 | {t('referencedByList.title', { count, name })}
106 |
107 | {concept.referencedBy.map(referencedBy => (
108 |
109 | ))}
110 | >
111 | )
112 | }
113 |
114 | export default ReferencedByList
115 |
--------------------------------------------------------------------------------
/src/components/ReportErrorButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import Button from '@material-ui/core/Button';
3 | import * as Sentry from '@sentry/browser';
4 |
5 | type ErrorButtonProps = {
6 | eventId: string | undefined
7 | }
8 | const ReportErrorButton: FunctionComponent = ({ eventId }) => {
9 | return (
10 | Sentry.showReportDialog({ eventId })}>Tell Us About It
11 | )
12 | }
13 |
14 | export default ReportErrorButton
15 |
--------------------------------------------------------------------------------
/src/components/SharingModal.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useCallback, useContext} from 'react';
2 |
3 | import {useWebId} from '@solid/react';
4 | import data from '@solid/query-ldflex';
5 | import {namedNode} from '@rdfjs/data-model';
6 | import copy from 'copy-to-clipboard';
7 |
8 | import { makeStyles } from '@material-ui/core/styles';
9 | import Button from '@material-ui/core/Button';
10 | import Dialog from '@material-ui/core/Dialog';
11 | import DialogContent from '@material-ui/core/DialogContent';
12 | import DialogContentText from '@material-ui/core/DialogContentText';
13 | import DialogTitle from '@material-ui/core/DialogTitle';
14 | import Box from '@material-ui/core/Box';
15 | import TextField from '@material-ui/core/TextField';
16 | import IconButton from '@material-ui/core/IconButton';
17 | import Typography from '@material-ui/core/Typography';
18 | import Chip from '@material-ui/core/Chip';
19 | import Avatar from '@material-ui/core/Avatar';
20 | import Link from '@material-ui/core/Link';
21 | import Checkbox from '@material-ui/core/Checkbox';
22 | import FormControlLabel from '@material-ui/core/FormControlLabel';
23 |
24 | import CloseIcon from '@material-ui/icons/Close';
25 | import CopyIcon from '@material-ui/icons/FileCopy';
26 |
27 | import Autocomplete from '@material-ui/lab/Autocomplete';
28 |
29 | import Add from '@material-ui/icons/Add';
30 | import { schema, foaf, acl, vcard } from 'rdf-namespaces';
31 |
32 | import { useDrag, useDrop } from 'react-dnd'
33 |
34 | import Loader from './Loader';
35 | import { useAclExists, useParentAcl } from '../hooks/acls';
36 | import { useValueQuery, useListValuesQuery } from '../hooks/data';
37 | import { createDefaultAcl } from '../utils/acl';
38 | import { sharingUrl } from '../utils/urls';
39 | import { addPublicPage, removePublicPage, addPublicAccess, removePublicAccess } from '../utils/data';
40 | import WorkspaceContext from '../context/workspace';
41 |
42 | const useStyles = makeStyles(theme => ({
43 | closeButton: {
44 | position: 'absolute',
45 | right: theme.spacing(1),
46 | top: theme.spacing(1),
47 | color: theme.palette.grey[500]
48 | },
49 | permissionsType: {
50 | marginTop: theme.spacing(2),
51 | marginBottom: theme.spacing(2)
52 | },
53 | publicAccess: {
54 | marginTop: theme.spacing(2),
55 | marginBottom: theme.spacing(2)
56 | },
57 | publicAccessModes: {
58 | padding: theme.spacing(1),
59 | minHeight: theme.spacing(6),
60 | },
61 | listPublicly: {
62 | padding: theme.spacing(1),
63 | minHeight: theme.spacing(6),
64 | },
65 | agent: {
66 | cursor: ({readOnly}) => readOnly ? "default" : "pointer",
67 | },
68 | agents: ({draggedOver}) => ({
69 | padding: theme.spacing(1),
70 | minHeight: theme.spacing(6),
71 | backgroundColor: theme.palette.grey[draggedOver ? 200 : 50]
72 | }),
73 | sharingLink: {
74 | overflow: "hidden",
75 | whiteSpace: "nowrap",
76 | textOverflow: "ellipsis"
77 | }
78 | }));
79 |
80 | const Agent = ({agent, deleteAgent, readOnlyAcl, permissionsType}) => {
81 | const [avatarUri] = useValueQuery(agent, vcard.hasPhoto)
82 | const [name] = useValueQuery(agent, vcard.fn)
83 | const webId = useWebId();
84 | const readOnly = readOnlyAcl || (agent === webId)
85 | const [, chip] = useDrag({
86 | item: {
87 | type: "agent",
88 | draggedPermissionsType: permissionsType,
89 | draggedAgentWebId: agent,
90 | deleteDraggedAgentFromCurrent: deleteAgent
91 | }
92 | })
93 | const classes = useStyles({readOnly})
94 | return (
95 | }
99 | onDelete={readOnly ? null : deleteAgent }/>
100 | )
101 | }
102 |
103 | const ModeDescription = ({type}) => {
104 | if (type === "Owners") {
105 | return "Owners"
106 | } else if (type === "Writers") {
107 | return "Writers"
108 | } else if (type === "Readers") {
109 | return "Readers"
110 | } else {
111 | return "Custom Permissions"
112 | }
113 | }
114 |
115 | const PermissionsType = ({ aclUri, type, readOnly }) => {
116 | const uri = `${aclUri}#${type}`
117 | const [adding, setAdding] = useState(false)
118 | const [agents] = useListValuesQuery(uri, acl.agent)
119 | const webId = useWebId();
120 | const [friends] = useListValuesQuery(webId, foaf.knows)
121 | const addAgent = useCallback(async (agent) => {
122 | await data[uri][acl.agent].add(namedNode(agent))
123 | }, [uri])
124 | const deleteAgent = useCallback(async (agent) => {
125 | await data[uri][acl.agent].delete(namedNode(agent))
126 | }, [uri])
127 | const [{isOver}, drop] = useDrop({
128 | accept: "agent",
129 | drop: async ({draggedAgentWebId, draggedPermissionsType, deleteDraggedAgentFromCurrent}) => {
130 | if (type !== draggedPermissionsType) {
131 | await Promise.all([
132 | deleteDraggedAgentFromCurrent(),
133 | addAgent(draggedAgentWebId)
134 | ])
135 | }
136 | },
137 | collect: monitor => ({
138 | isOver: !!monitor.isOver()
139 | })
140 | })
141 | const classes = useStyles({draggedOver: isOver})
142 | return (
143 |
144 |
145 |
146 |
147 |
148 | {agents && agents.map(agent => (
149 | deleteAgent(agent)}
152 | key={agent}/>
153 | ))}
154 | {!readOnly && (
155 | setAdding(true)} size="small">
156 |
157 |
158 | )}
159 | {adding && (
160 | friend}
162 | onChange={(e, friend) => {
163 | addAgent(friend)
164 | setAdding(false)
165 | }}
166 | renderInput={params => }
167 | />
168 | )}
169 |
170 |
171 | )
172 | }
173 |
174 | const PageName = ({page}) => {
175 | const [name] = useValueQuery(page, schema.name);
176 | return <>{name || ""}>
177 | }
178 |
179 | function NoAclContent({page, aclUri}){
180 | const webId = useWebId()
181 | const {uri: parentAclUri, loading} = useParentAcl(page)
182 | return (
183 | <>
184 | {loading ? (
185 |
186 | ) : (
187 |
188 |
189 | This page is using the permissions of
190 | {parentAclUri && (
191 |
192 | )}
193 |
194 |
195 |
196 |
197 | {
198 | createDefaultAcl(webId, aclUri.split(".").slice(0, -1).join("."))
199 | }}>
200 | customize permissions for
201 |
202 |
203 | )}
204 | >
205 |
206 | )
207 | }
208 |
209 | function PublicAccess({page, aclUri}){
210 | const {workspace} = useContext(WorkspaceContext)
211 | const publicPages = workspace.publicPages
212 | const [saving, setSaving] = useState(false)
213 | const publicAccessUri = `${aclUri}#Public`
214 | const [publicAccessModes] = useListValuesQuery(publicAccessUri, acl.mode);
215 | const read = publicAccessModes && publicAccessModes.includes(acl.Read)
216 | const write = publicAccessModes && publicAccessModes.includes(acl.Write)
217 | const handleChange = useCallback(async event => {
218 | const checked = event.target.checked
219 | const name = event.target.name
220 | setSaving(true)
221 | if (checked){
222 | await addPublicAccess(publicAccessUri, name)
223 | } else {
224 | await removePublicAccess(publicAccessUri, name, publicPages, page, write)
225 | }
226 | setSaving(false)
227 | }, [page, publicPages, publicAccessUri, write])
228 |
229 | const [publicDocs] = useListValuesQuery(publicPages, schema.itemListElement);
230 | const listedPublicly = publicDocs && publicDocs.includes(page)
231 | const handleChangeListPublicly = useCallback(async event => {
232 | const checked = event.target.checked
233 | if (checked){
234 | await addPublicPage(publicPages, page)
235 | } else {
236 | await removePublicPage(publicPages, page)
237 | }
238 | }, [page, publicPages])
239 | const classes = useStyles()
240 | return (
241 |
242 |
243 | Public Access
244 |
245 |
246 | {publicAccessModes && (
247 | <>
248 |
251 | }/>
252 | {read && (
253 |
256 | }/>
257 | )}
258 | >
259 | )}
260 | {saving && }
261 |
262 | {(publicAccessModes && read) && (
263 |
264 |
267 | }/>
268 |
269 | )}
270 |
271 | )
272 | }
273 |
274 | function AclContent({page, aclUri}){
275 | const [name] = useValueQuery(page, schema.name);
276 | const url = sharingUrl(page)
277 | const classes = useStyles();
278 | return (
279 |
280 |
281 | Set sharing for {name && name.toString()}
282 |
283 |
284 | Sharing link:
285 |
286 |
287 | copy(url)} title="copy link to clipboard">
288 |
289 |
290 | {url}
291 |
292 |
293 |
294 |
295 |
296 |
297 | )
298 | }
299 |
300 | export default function SharingModal({document, aclUri, open, onClose}) {
301 | const {exists, loading} = useAclExists(aclUri)
302 | const classes = useStyles();
303 | return (
304 |
305 |
306 |
307 | Sharing
308 |
309 |
310 |
311 |
312 |
313 | {(!exists && loading) ? (
314 |
315 | ) : (
316 | exists ? (
317 |
318 | ) : (
319 |
320 | )
321 | )}
322 |
323 | )
324 | }
325 |
--------------------------------------------------------------------------------
/src/components/SignupPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Typography from '@material-ui/core/Typography';
4 |
5 | import { LogInButton } from './LogInLogOutButton'
6 | import { AppTitleGridRow, MottoGridRow } from './AppTitle'
7 | import { Question, Answer, A } from './FAQ'
8 | import SocialIcons from './SocialIcons'
9 |
10 | function SignupPage() {
11 | return (
12 | <>
13 |
14 |
15 |
16 | How can I sign up?
17 |
18 |
19 |
20 | Concept doesn't store your data. It's a new kind of app built on an emerging web standard called Solid .
21 |
22 |
23 | When you use a Solid app, you bring your own data, stored in your
24 | personal data Pod .
25 |
26 |
27 | To use Concept, first pick a provider and create your Pod .
28 |
29 |
30 | Once you've created your Pod, log in to Concept to get started organizing your world.
31 |
32 |
33 |
34 | >
35 | )
36 | }
37 |
38 | export default SignupPage
39 |
--------------------------------------------------------------------------------
/src/components/SocialIcons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Grid from '@material-ui/core/Grid';
4 | import Link from '@material-ui/core/Link';
5 | import Twitter from '@material-ui/icons/Twitter';
6 | import GitHub from '@material-ui/icons/GitHub';
7 | import { makeStyles } from '@material-ui/core/styles';
8 |
9 | const useStyles = makeStyles(theme => ({
10 | social: {
11 | marginTop: theme.spacing(6)
12 | },
13 | socialIcon: {
14 | color: "inherit"
15 | }
16 | }))
17 |
18 | export default function SocialIcons(){
19 | const classes = useStyles()
20 | return (
21 |
22 |
23 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/UnrecoverableErrorPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Grid from '@material-ui/core/Grid';
5 | import Box from '@material-ui/core/Box';
6 |
7 | import ReportErrorButton from './ReportErrorButton'
8 | import SocialIcons from './SocialIcons'
9 |
10 | const useStyles = makeStyles(theme => ({
11 | page: {
12 | marginTop: theme.spacing(9),
13 | marginLeft: theme.spacing(3),
14 | marginRight: theme.spacing(3)
15 | },
16 | message: {
17 | fontFamily: "'Special Elite', cursive",
18 | marginBottom: theme.spacing(6),
19 | }
20 | }))
21 |
22 | type UnrecoverableErrorPageProps = {
23 | eventId: string | undefined
24 | }
25 |
26 | const UnrecoverableErrorPage: FunctionComponent = ({ eventId }) => {
27 | const classes = useStyles({})
28 | return (
29 |
30 |
31 |
32 | Oh dear. We've encountered a possible infinite loop and couldn't recover. Reloading may solve the problem.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default UnrecoverableErrorPage
46 |
--------------------------------------------------------------------------------
/src/components/WhatPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Typography from '@material-ui/core/Typography';
4 |
5 | import { LogInButton } from './LogInLogOutButton'
6 | import { AppTitleGridRow, MottoGridRow } from './AppTitle'
7 | import { Question, Answer, A } from './FAQ'
8 | import SocialIcons from './SocialIcons'
9 |
10 | function LandingPage() {
11 | return (
12 | <>
13 |
14 |
15 |
16 | What is this?
17 |
18 |
19 |
20 | Concept is a collaborative workspace for organizing the world. It's
21 | pretty simple for now, but with your help we
22 | can make it the best workspace on the web.
23 |
24 |
25 | If you use Google Docs, Dropbox Paper or Notion but want to use an app that gives
26 | you full control over your data, help us make Concept the best of all
27 | possible collaboration tools.
28 |
29 |
30 |
31 | How can I get started?
32 |
33 |
34 |
35 | Concept is built on Solid, an emerging open web standard that puts you back in control of your data.
36 |
37 |
38 | To start using Concept you'll need to create your Pod and
39 | then log in to Concept using your WebId.
40 |
41 |
42 |
43 | Is my data safe?
44 |
45 |
46 |
47 | You're trusting your Pod provider with your data, so ultimately it's
48 | as safe as you think your Pod provider makes it.
49 |
50 |
51 | Concept is in Alpha at the moment, which means we might make
52 | non-backwards-compatible changes to the data model. We can't promise
53 | to support migration between different data formats for now. We're
54 | hoping to move to Beta soon, at which point we'll provide better
55 | support.
56 |
57 |
58 | Your data will always be accessible via your Pod provider's data
59 | browser, and we create backups of your active documents every 1, 5 and
60 | 10 minutes while you're editing. You definitely won't lose data as
61 | long as as your Pod provider doesn't lose data, it might just require
62 | expert-level skills to get your data back for now.
63 |
64 |
65 |
66 | Something went wrong! How do I get help?
67 |
68 |
69 |
70 | For now, please file bug reports in the issue tracker on GitHub.
71 |
72 |
73 |
74 | >
75 | )
76 | }
77 |
78 | export default LandingPage
79 |
--------------------------------------------------------------------------------
/src/components/Workspace.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Box from '@material-ui/core/Box';
5 |
6 | import { LiveUpdate } from "@solid/react";
7 | import {Switch, Route} from 'react-router-dom'
8 |
9 | import WorkspaceContext, {WorkspaceProvider} from "../context/workspace";
10 |
11 | import WorkspaceDrawer from './WorkspaceDrawer';
12 | import CurrentPage from "./CurrentPage"
13 | import CurrentConcept from "./CurrentConcept"
14 | import Home from "./Home"
15 | import PublicProfile, {EncodedWebIdPublicProfile} from './PublicProfile';
16 | import { drawerWidth } from '../constants'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | content: {
20 | marginLeft: drawerWidth,
21 | height: "100%",
22 | position: "relative"
23 | },
24 | }));
25 |
26 |
27 | const WorkspaceRoute = ({children, ...props}) => {
28 | const {workspace} = useContext(WorkspaceContext);
29 | return (
30 |
31 | {workspace && (
32 |
33 |
34 |
35 | )}
36 | {children}
37 |
38 | )
39 | }
40 |
41 |
42 | function WorkspaceContent(){
43 | const classes = useStyles()
44 | return (
45 | <>
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | >
66 | )
67 | }
68 |
69 | export default function Workspace(){
70 | return (
71 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/edit/Block.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, ReactNode, useRef, useState } from 'react'
2 | import { Element, Transforms } from 'slate';
3 | import { useEditor, useReadOnly, ReactEditor } from 'slate-react';
4 | import { useDrag, useDrop } from 'react-dnd'
5 |
6 | import { makeStyles } from '@material-ui/core/styles';
7 | import Menu, { MenuProps } from '@material-ui/core/Menu';
8 | import MenuItem from '@material-ui/core/MenuItem';
9 |
10 | import AddIcon from '@material-ui/icons/Add';
11 | import DragIcon from '@material-ui/icons/DragIndicator';
12 |
13 | import IconButton from '../IconButton';
14 | import InsertMenu from './InsertMenu'
15 | import TurnIntoMenu from './TurnIntoMenu'
16 |
17 | export const useStyles = makeStyles(theme => ({
18 | dragButton: {
19 | cursor: ({ isDragging }: { isDragging?: boolean }) => isDragging ? "move" : "grab"
20 | },
21 | block: {
22 | position: "relative",
23 | "&:hover $blockHoverButtons": {
24 | visibility: "visible"
25 | },
26 | left: -theme.spacing(7),
27 | paddingLeft: theme.spacing(7),
28 | borderBottomWidth: theme.spacing(0.5),
29 | borderBottomStyle: "solid",
30 | borderBottomColor: "transparent"
31 | },
32 | blockHoverButtons: {
33 | visibility: ({ menuOpen }: { menuOpen?: boolean }) => menuOpen ? "visible" : "hidden",
34 | opacity: 0.5,
35 | },
36 | blockContent: {
37 | borderBottomWidth: theme.spacing(0.5),
38 | borderBottomStyle: "solid",
39 | borderBottomColor: ({ isOver }: { isOver?: boolean }) => isOver ? theme.palette.info.light : "transparent"
40 | },
41 | blockButtons: {
42 | position: "absolute",
43 | left: -theme.spacing(0),
44 | "& button": {
45 | padding: 0
46 | }
47 | },
48 | }))
49 |
50 | type BlockMenuProps = MenuProps & { element: Element, onClose: () => void }
51 |
52 | const BlockMenu: FunctionComponent = ({ element, onClose, ...props }) => {
53 | const editor = useEditor()
54 | const turnIntoRef = useRef(null)
55 | const [turnIntoMenuOpen, setTurnIntoMenuOpen] = useState(false)
56 | return (
57 | <>
58 | {
69 | setTurnIntoMenuOpen(false)
70 | onClose()
71 | }}
72 | {...props}>
73 | {
74 | Transforms.removeNodes(editor, {
75 | at: ReactEditor.findPath(editor, element)
76 | })
77 | }}>
78 | delete
79 |
80 | setTurnIntoMenuOpen(true)}
82 | onMouseLeave={() => setTurnIntoMenuOpen(false)}
83 | >
84 | turn into ⩺
85 |
86 |
87 | {turnIntoMenuOpen && (
88 | setTurnIntoMenuOpen(true)}
90 | anchorEl={turnIntoRef.current}
91 | open={turnIntoMenuOpen} onClose={() => {
92 | setTurnIntoMenuOpen(false)
93 | onClose()
94 | }} />
95 | )}
96 | >
97 | )
98 | }
99 |
100 | type BlockProps = {
101 | children: ReactNode,
102 | element: Element
103 | }
104 |
105 | const Block: FunctionComponent = ({ children, element }) => {
106 | const editor = useEditor()
107 | const readOnly = useReadOnly()
108 | const buttonsRef = useRef(null)
109 | const [menuOpen, setMenuOpen] = useState(false)
110 | const insertRef = useRef(null)
111 | const [insertMenuOpen, setInsertMenuOpen] = useState(false)
112 | const [{ isDragging }, drag, preview] = useDrag({
113 | item: { type: "block", element },
114 | collect: monitor => ({
115 | isDragging: !!monitor.isDragging(),
116 | })
117 | })
118 | const [{ isOver }, drop] = useDrop({
119 | accept: "block",
120 | drop: (item: any) => {
121 | const sourcePath = ReactEditor.findPath(editor, item.element)
122 | const sourceIndex = sourcePath[sourcePath.length - 1]
123 | const targetPath = ReactEditor.findPath(editor, element)
124 | const targetIndex = targetPath[targetPath.length - 1]
125 | if (sourceIndex !== targetIndex) {
126 | const insertIndex = sourceIndex > targetIndex ? targetIndex + 1 : targetIndex
127 | Transforms.moveNodes(editor, {
128 | at: sourcePath,
129 | to: [...targetPath.slice(0, -1), insertIndex]
130 | })
131 | }
132 | },
133 | collect: monitor => ({
134 | isOver: !!monitor.isOver(),
135 | }),
136 | })
137 |
138 | const classes = useStyles({ menuOpen, isDragging, isOver })
139 |
140 | return (
141 |
142 | {!readOnly && (
143 | <>
144 |
setMenuOpen(false)} />
147 | {
150 | setInsertMenuOpen(false)
151 | }}
152 | onExiting={() => {
153 | ReactEditor.focus(editor)
154 | }} />
155 |
156 |
setInsertMenuOpen(!insertMenuOpen)}
158 | title="insert">
159 |
160 |
161 |
setMenuOpen(!menuOpen)} className={classes.dragButton}
163 | title="">
164 |
165 |
166 |
167 | >
168 | )}
169 |
170 | {children}
171 |
172 |
173 | )
174 | }
175 |
176 | export default Block
177 |
--------------------------------------------------------------------------------
/src/components/edit/ConceptElement.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useContext, FunctionComponent } from 'react'
2 | import { Transforms, Range, Element } from 'slate';
3 | import { useSelected, useEditor } from 'slate-react';
4 |
5 | import copy from 'copy-to-clipboard';
6 |
7 | import { makeStyles } from '@material-ui/core/styles';
8 | import TextField from '@material-ui/core/TextField';
9 | import Popover, { PopoverProps } from '@material-ui/core/Popover';
10 | import SaveIcon from '@material-ui/icons/Save';
11 | import EditIcon from '@material-ui/icons/Edit';
12 | import UnlinkIcon from '@material-ui/icons/LinkOff';
13 | import CopyIcon from '@material-ui/icons/FileCopy';
14 |
15 | import IconButton from '../IconButton';
16 | import Link from '../Link';
17 | import { setConceptProps, removeConcept } from '../../utils/editor';
18 | import { ElementProps } from "./"
19 | import { conceptPath, conceptUri, conceptUrl } from '../../utils/urls'
20 | import WorkspaceContext from '../../context/workspace'
21 |
22 | const useStyles = makeStyles(theme => ({
23 | aPopover: {
24 | padding: theme.spacing(1)
25 | },
26 | linkPopupButton: {
27 | padding: 0,
28 | marginLeft: theme.spacing(1)
29 | },
30 | }))
31 |
32 | type ConceptPopoverProps = {
33 | element: Element,
34 | editing: boolean,
35 | setEditing: (editing: boolean) => void,
36 | onClose: () => void
37 | } & PopoverProps
38 |
39 | const ConceptPopover: FunctionComponent = ({ element, editing, setEditing, onClose, ...props }) => {
40 | const { workspace } = useContext(WorkspaceContext)
41 | const editor = useEditor()
42 | const [selection, setSelection] = useState(null)
43 | const [editValue, setEditValue] = useState(element.name)
44 | const classes = useStyles()
45 | const editLink = () => {
46 | setSelection(editor.selection)
47 | setEditing(true)
48 | }
49 | const saveLink = () => {
50 | workspace && setConceptProps(editor, element, editValue, conceptUri(workspace.conceptContainerUri, editValue))
51 | onClose()
52 | setEditing(false)
53 | selection && Transforms.select(editor, selection)
54 | }
55 | return (
56 |
68 | {editing ? (
69 | setEditValue(e.target.value)}
70 | onKeyDown={event => {
71 | if (event.keyCode === 13) {
72 | event.preventDefault()
73 | saveLink()
74 | }
75 | }} />
76 | ) : (
77 | {element.name}
78 | )}
79 | {editing ? (
80 |
82 |
83 |
84 | ) : (
85 |
87 |
88 |
89 | )}
90 | removeConcept(editor)}>
92 |
93 |
94 | copy(conceptUrl(element.uri))}>
96 |
97 |
98 |
99 | )
100 | }
101 |
102 | const ConceptElement: FunctionComponent = ({ attributes, children, element }) => {
103 | const selected = useSelected()
104 | const editor = useEditor()
105 | const selectionCollapsed = !!(editor.selection && Range.isCollapsed(editor.selection))
106 | const aRef = useRef(null)
107 | const [editingLink, setEditingLink] = useState(false)
108 | const open = (!!aRef.current) && (editingLink || (selected && selectionCollapsed))
109 | return (
110 | <>
111 |
112 | {children}
113 |
114 | {open && (
115 | {
117 | setEditingLink(false)
118 | }}
119 | editing={editingLink} setEditing={setEditingLink} />
120 | )}
121 | >
122 | )
123 | }
124 |
125 | export default ConceptElement
126 |
--------------------------------------------------------------------------------
/src/components/edit/ImageElement.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, FunctionComponent } from 'react'
2 | import { Transforms } from 'slate';
3 | import { useSelected, useFocused, useEditor, ReactEditor } from 'slate-react';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Box from '@material-ui/core/Box';
7 | import EditIcon from '@material-ui/icons/Edit';
8 | import ArrowRight from '@material-ui/icons/ArrowRight';
9 |
10 | import { ImageEditor } from '../ImageUploader';
11 |
12 | import { ElementProps } from "./"
13 | import { useStyles as useBlockStyles } from "./Block"
14 |
15 | const useStyles = makeStyles(theme => ({
16 | image: {
17 | display: "block",
18 | width: ({ width }: { width?: number }) => width || theme.spacing(20),
19 | height: "auto",
20 | boxShadow: ({ selected, focused }: { selected?: boolean, focused?: boolean }) =>
21 | selected && focused ? '0 0 0 3px #B4D5FF' : 'none'
22 | },
23 | editImageIcon: {
24 | cursor: "pointer"
25 | },
26 | imageWidthDragHandle: {
27 | width: theme.spacing(3),
28 | cursor: "ew-resize",
29 | background: "transparent",
30 | display: "flex",
31 | flexDirection: "column",
32 | justifyContent: "center"
33 | }
34 | }))
35 |
36 | const ImageElement: FunctionComponent = ({ attributes, children, element }) => {
37 | const editor = useEditor()
38 | const image = useRef(null)
39 | const [editing, setEditing] = useState(false)
40 | const [dragStart, setDragStart] = useState(null)
41 | const [dragStartImageWidth, setDragStartImageWidth] = useState(null)
42 | const selected = useSelected()
43 | const focused = useFocused()
44 | const width = element.width;
45 | const classes = useStyles({ selected, focused, width })
46 | const blockClasses = useBlockStyles()
47 | const path = ReactEditor.findPath(editor, element)
48 | return (
49 |
50 |
51 |
56 |
60 | setEditing(true)} />
63 | {
67 | Transforms.select(editor, path)
68 | setDragStartImageWidth(image && image.current && image.current.clientWidth)
69 | setDragStart(e.clientX)
70 | }}
71 | onDrag={e => {
72 | if (dragStartImageWidth && dragStart) {
73 | const newWidth = dragStartImageWidth + (e.clientX - dragStart)
74 | if (width !== newWidth) {
75 | Transforms.setNodes(editor, { width: newWidth }, { at: path })
76 | }
77 | }
78 | }}>
79 |
80 |
81 |
82 |
83 | {children}
84 |
setEditing(false)}
86 | onSave={(savedUrl: string) => {
87 | const u = new URL(savedUrl)
88 | u.searchParams.set("updated", Date.now().toString())
89 | Transforms.setNodes(editor, { url: u.toString() }, { at: path })
90 | setEditing(false)
91 | }} />
92 |
93 | )
94 | }
95 |
96 | export default ImageElement
97 |
--------------------------------------------------------------------------------
/src/components/edit/InsertMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, forwardRef, useCallback, Ref, FunctionComponent, PropsWithChildren } from 'react';
2 | import { Transforms, Element } from 'slate';
3 | import { useEditor } from 'slate-react';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Menu, { MenuProps } from '@material-ui/core/Menu';
7 | import MenuItem from '@material-ui/core/MenuItem';
8 |
9 | import { insertBlock, insertionPoint } from '../../utils/editor';
10 | import DocumentContext from '../../context/document'
11 |
12 | import EmbedPicker from '../EmbedPicker'
13 | import ImageUploader from '../ImageUploader'
14 |
15 |
16 | const useStyles = makeStyles(theme => ({
17 | imageUploadPopover: {
18 | minWidth: theme.spacing(30),
19 | minHeight: theme.spacing(20)
20 | }
21 | }))
22 |
23 | type InsertItemProps = PropsWithChildren<{
24 | element: Element,
25 | format: string,
26 | onClose: () => void
27 | }>
28 |
29 | const InsertItem: FunctionComponent = forwardRef(({ element, format, onClose, ...props }, ref: Ref) => {
30 | const editor = useEditor()
31 | const onClick = useCallback(async () => {
32 | const insertAt = insertionPoint(editor, element)
33 | insertBlock(editor, format, insertAt)
34 | Transforms.select(editor, insertAt)
35 | onClose()
36 | }, [editor, format, element, onClose])
37 | return (
38 |
39 | )
40 | })
41 |
42 | type InsertImageItemProps = PropsWithChildren<{
43 | element: Element,
44 | onClose: () => void
45 | }>
46 |
47 | const InsertImageItem: FunctionComponent = forwardRef(({ element, onClose, ...props }, ref: Ref) => {
48 | const classes = useStyles()
49 | const document = useContext(DocumentContext)
50 | const [imagePickerOpen, setImagePickerOpen] = useState(false)
51 | return (
52 | <>
53 | setImagePickerOpen(true)} ref={ref} {...props} />
54 | {document && (
55 |
60 | )}
61 | >
62 | )
63 | })
64 |
65 | type InsertEmbedItemProps = PropsWithChildren<{
66 | element: Element,
67 | onClose: () => void
68 | }>
69 |
70 | const InsertEmbedItem: FunctionComponent = forwardRef(({ element, onClose, ...props }, ref: Ref) => {
71 | const [embedPickerOpen, setEmbedPickerOpen] = useState(false)
72 | const editor = useEditor()
73 |
74 | const close = useCallback(() => {
75 | setEmbedPickerOpen(false)
76 | onClose()
77 | }, [onClose])
78 | const save = useCallback(async (embedUrl, embedType) => {
79 | const insertAt = insertionPoint(editor, element)
80 | insertBlock(editor, 'embed', insertAt, { url: embedUrl, embedType })
81 | Transforms.select(editor, insertAt)
82 | onClose()
83 | }, [editor, element, onClose])
84 | return (
85 | <>
86 | setEmbedPickerOpen(true)} ref={ref} {...props} />
87 |
92 | >
93 | )
94 | })
95 |
96 | type InsertMenuProps = MenuProps & {
97 | element: Element,
98 | onClose: () => void
99 | }
100 |
101 | const InsertMenu: FunctionComponent = ({ element, onClose, ...props }) => {
102 | return (
103 |
115 |
116 | text
117 |
118 |
119 | heading 1
120 |
121 |
122 | heading 2
123 |
124 |
125 | heading 3
126 |
127 |
128 | quote
129 |
130 |
131 | numbered list
132 |
133 |
134 | bulleted list
135 |
136 |
137 | todo list
138 |
139 |
140 | image
141 |
142 |
143 | table
144 |
145 |
146 | embed
147 |
148 | )
149 | }
150 |
151 | export default InsertMenu
152 |
--------------------------------------------------------------------------------
/src/components/edit/LinkElement.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, FunctionComponent } from 'react'
2 | import { Transforms, Range, Element } from 'slate';
3 | import { useSelected, useEditor } from 'slate-react';
4 |
5 | import copy from 'copy-to-clipboard';
6 |
7 | import { makeStyles } from '@material-ui/core/styles';
8 | import Link from '@material-ui/core/Link';
9 | import TextField from '@material-ui/core/TextField';
10 | import Popover, { PopoverProps } from '@material-ui/core/Popover';
11 | import SaveIcon from '@material-ui/icons/Save';
12 | import EditIcon from '@material-ui/icons/Edit';
13 | import UnlinkIcon from '@material-ui/icons/LinkOff';
14 | import CopyIcon from '@material-ui/icons/FileCopy';
15 |
16 | import IconButton from '../IconButton';
17 | import { setLinkUrl, removeLink } from '../../utils/editor';
18 | import { ElementProps } from "./"
19 |
20 | const useStyles = makeStyles(theme => ({
21 | aPopover: {
22 | padding: theme.spacing(1)
23 | },
24 | linkPopupButton: {
25 | padding: 0,
26 | marginLeft: theme.spacing(1)
27 | },
28 | }))
29 |
30 | type LinkPopoverProps = {
31 | element: Element,
32 | editing: boolean,
33 | setEditing: (editing: boolean) => void,
34 | onClose: () => void
35 | } & PopoverProps
36 |
37 | const LinkPopover: FunctionComponent = ({ element, editing, setEditing, onClose, ...props }) => {
38 | const editor = useEditor()
39 | const [selection, setSelection] = useState(null)
40 | const [editValue, setEditValue] = useState(element.url)
41 | const classes = useStyles()
42 | const editLink = () => {
43 | setSelection(editor.selection)
44 | setEditing(true)
45 | }
46 | const saveLink = () => {
47 | setLinkUrl(editor, element, editValue)
48 | onClose()
49 | setEditing(false)
50 | selection && Transforms.select(editor, selection)
51 | }
52 | return (
53 |
65 | {editing ? (
66 | setEditValue(e.target.value)}
67 | onKeyDown={event => {
68 | if (event.keyCode === 13) {
69 | event.preventDefault()
70 | saveLink()
71 | }
72 | }} />
73 | ) : (
74 | {element.url}
75 | )}
76 | {editing ? (
77 |
79 |
80 |
81 | ) : (
82 |
84 |
85 |
86 | )}
87 | removeLink(editor)}>
89 |
90 |
91 | copy(element.url)}>
93 |
94 |
95 |
96 | )
97 | }
98 |
99 | const LinkElement: FunctionComponent = ({ attributes, children, element }) => {
100 | const selected = useSelected()
101 | const editor = useEditor()
102 | const selectionCollapsed = !!(editor.selection && Range.isCollapsed(editor.selection))
103 | const aRef = useRef(null)
104 | const [editingLink, setEditingLink] = useState(false)
105 | const open = (!!aRef.current) && (editingLink || (selected && selectionCollapsed))
106 | return (
107 | <>
108 |
109 | {children}
110 |
111 | {open && (
112 | {
114 | setEditingLink(false)
115 | }}
116 | editing={editingLink} setEditing={setEditingLink} />
117 | )}
118 | >
119 | )
120 | }
121 |
122 | export default LinkElement
123 |
--------------------------------------------------------------------------------
/src/components/edit/Table.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | import { useEditor } from 'slate-react';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Box from '@material-ui/core/Box';
7 | import ArrowRight from '@material-ui/icons/ArrowRight';
8 | import ArrowDown from '@material-ui/icons/ArrowDropDown';
9 | import ArrowLeft from '@material-ui/icons/ArrowLeft';
10 | import ArrowUp from '@material-ui/icons/ArrowDropUp';
11 |
12 | import {
13 | insertRow, insertColumn, removeRow, removeColumn,
14 | } from '../../utils/editor';
15 |
16 | import { ElementProps } from "./"
17 | import IconButton from '../IconButton';
18 | import { useStyles as useBlockStyles } from './Block'
19 |
20 | const useStyles = makeStyles(theme => ({
21 | table: {
22 | border: "1px solid black",
23 | borderCollapse: "collapse"
24 | },
25 | tr: {
26 | },
27 | td: {
28 | border: `2px solid ${theme.palette.grey[300]}`,
29 | padding: theme.spacing(1)
30 | },
31 | columnButtons: {
32 | display: "flex",
33 | flexDirection: "column",
34 | verticalAlign: "top"
35 | },
36 | rowButtons: {
37 | display: "flex"
38 | },
39 | }))
40 |
41 |
42 |
43 | const Table: FunctionComponent = ({ attributes, children, element }) => {
44 | const editor = useEditor()
45 | const classes = useStyles()
46 | const blockClasses = useBlockStyles()
47 | return (
48 | <>
49 |
50 |
53 |
55 | insertColumn(editor, element)}>
57 |
58 |
59 | removeColumn(editor, element)}>
61 |
62 |
63 |
64 |
65 |
66 | insertRow(editor, element)}>
68 |
69 |
70 | removeRow(editor, element)}>
72 |
73 |
74 |
75 | >
76 | )
77 | }
78 | export default Table
79 |
--------------------------------------------------------------------------------
/src/components/edit/TurnIntoMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { Ref, FunctionComponent, forwardRef, useCallback } from 'react';
2 | import { Element } from 'slate';
3 | import { useEditor, ReactEditor } from 'slate-react';
4 |
5 | import { makeStyles } from '@material-ui/core/styles';
6 | import Menu, { MenuProps } from '@material-ui/core/Menu';
7 | import MenuItem from '@material-ui/core/MenuItem';
8 |
9 | import { makeBlock } from '../../utils/editor';
10 |
11 | const useStyles = makeStyles(theme => ({
12 | turnIntoMenu: {
13 | pointerEvents: "none"
14 | },
15 | turnIntoMenuPaper: {
16 | pointerEvents: "auto"
17 | },
18 | }))
19 |
20 | type TurnIntoItemProps = {
21 | element: Element,
22 | format: string,
23 | onClose: () => void
24 | }
25 |
26 | const TurnIntoItem: FunctionComponent = forwardRef(({ element, format, onClose, ...props }, ref: Ref) => {
27 | const editor = useEditor()
28 | const onClick = useCallback(() => {
29 | makeBlock(editor, format, ReactEditor.findPath(editor, element))
30 | onClose()
31 | }, [editor, format, element, onClose])
32 | return (
33 |
34 | )
35 | })
36 |
37 | type TurnIntoMenuProps = MenuProps & {
38 | element: Element,
39 | onClose: () => void
40 | }
41 |
42 | const TurnIntoMenu: FunctionComponent = ({ element, onClose, ...props }) => {
43 | const classes = useStyles()
44 | return (
45 |
60 |
61 | text
62 |
63 |
64 | heading 1
65 |
66 |
67 | heading 2
68 |
69 |
70 | heading 3
71 |
72 |
73 | quote
74 |
75 |
76 | numbered list
77 |
78 |
79 | bulleted list
80 |
81 |
82 | todo list
83 |
84 | )
85 | }
86 |
87 | export default TurnIntoMenu
88 |
--------------------------------------------------------------------------------
/src/components/edit/index.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Element } from 'slate';
3 |
4 |
5 | export type ElementProps = {
6 | attributes: { [key: string]: any },
7 | element: Element,
8 | children: ReactNode
9 | }
10 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const drawerWidth = 240;
2 |
--------------------------------------------------------------------------------
/src/context/auth.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 | import auth, { Session } from 'solid-auth-client';
3 | import { useHistory } from "react-router-dom";
4 |
5 | type AuthProviderContextType = {
6 | logIn: () => Promise,
7 | logOut: () => Promise
8 | }
9 |
10 | async function logIn() {
11 | return await auth.popupLogin({ popupUri: "/popup.html" })
12 | }
13 | async function logOut() {
14 | return await auth.logout()
15 | }
16 |
17 | const AuthContext = createContext({ logIn, logOut })
18 |
19 | const { Provider } = AuthContext;
20 |
21 | export const AuthProvider = (props: any) => {
22 | const history = useHistory()
23 |
24 | async function logOutAndGoHome() {
25 | await logOut()
26 | history.push("/")
27 | }
28 |
29 | async function logInAndGoHome() {
30 | await logIn()
31 | history.push("/")
32 | }
33 | return (
34 |
35 | )
36 | }
37 |
38 | export const useAuthContext = () => useContext(AuthContext)
39 |
40 | export default AuthContext
41 |
--------------------------------------------------------------------------------
/src/context/document.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { Document } from '../utils/model'
3 |
4 | export default createContext(null)
5 |
--------------------------------------------------------------------------------
/src/context/preferences.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState } from 'react';
2 |
3 | export interface Preferences {
4 | devMode: boolean
5 | setDevMode: (v: boolean) => void
6 | }
7 |
8 | var devMode = false
9 | const setDevMode = (m: boolean) => { }
10 |
11 | const PreferencesContext = createContext({ devMode, setDevMode })
12 |
13 | const { Provider } = PreferencesContext
14 |
15 | export const usePreferences = () => useContext(PreferencesContext)
16 |
17 | export const PreferencesProvider = (props: any) => {
18 | const [devMode, setDevMode] = useState(false)
19 | return (
20 |
21 | )
22 | }
23 |
24 | export default PreferencesContext;
25 |
--------------------------------------------------------------------------------
/src/context/workspace.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, createContext, useCallback, useEffect, useMemo } from 'react';
2 | import { space, schema, dct } from 'rdf-namespaces';
3 | import { useWebId } from '@solid/react';
4 | import data from '@solid/query-ldflex';
5 | import { namedNode } from '@rdfjs/data-model';
6 | import { createNonExistentDocument } from '../utils/ldflex-helper';
7 | import { createDefaultAcl } from '../utils/acl';
8 | import { listResolver, listValuesResolver } from '../utils/data';
9 | import { useValueQuery } from '../hooks/data';
10 | import * as m from "../utils/model"
11 |
12 | type AddPageType = (props: m.PageProps, pageListProps: m.PageListItemProps) => Promise
13 | type AddConceptType = (props: m.ConceptProps) => Promise
14 | type AddSubPageType = (props: m.PageProps, parentPageListItem: m.PageListItem) => Promise
15 | type UpdateTextType = (document: m.Document, value: string, conceptUris: string[]) => Promise
16 | type UpdateNameType = (page: m.Page, value: any) => Promise
17 | type DeleteDocumentType = (document: m.Document) => Promise
18 |
19 | export interface WorkspaceContextType {
20 | conceptContainer?: string,
21 | publicPages?: string,
22 | workspace?: m.Workspace,
23 | addPage?: AddPageType,
24 | addConcept?: AddConceptType,
25 | addSubPage?: AddSubPageType,
26 | updateText?: UpdateTextType,
27 | updateName?: UpdateNameType,
28 | deleteDocument?: DeleteDocumentType
29 | }
30 |
31 | const WorkspaceContext = createContext({});
32 |
33 | const { Provider } = WorkspaceContext;
34 |
35 | type WorkspaceProviderProps = {
36 | children: ReactNode
37 | }
38 |
39 | export const WorkspaceProvider = ({ children }: WorkspaceProviderProps) => {
40 | const webId = useWebId();
41 | const [storage] = useValueQuery(webId, space.storage)
42 | const workspace = useMemo(
43 | () => (storage === undefined) ? undefined : m.workspaceFromStorage(storage),
44 | [storage]
45 | )
46 |
47 | useEffect(() => {
48 | if (workspace && workspace.docUri) {
49 | const createWorkspace = async () => {
50 | Promise.all([
51 | createNonExistentDocument(workspace.docUri),
52 | createDefaultAcl(webId, workspace.containerUri)
53 | ])
54 | }
55 | createWorkspace();
56 | }
57 | }, [workspace, webId])
58 |
59 | const addPage: AddPageType = async ({ name = "Untitled" }, pageListItemProps) => {
60 | if (workspace !== undefined) {
61 | return await m.addPage(workspace, { name }, pageListItemProps)
62 | } else {
63 | return null
64 | }
65 | }
66 |
67 | const addConcept: AddConceptType = async ({ name }) => {
68 | if (workspace !== undefined) {
69 | return await m.addConcept(workspace, name)
70 | } else {
71 | return null
72 | }
73 | }
74 |
75 | const addSubPage: AddSubPageType = async ({ name = "Untitled" }, parentPageListItem) => {
76 | const subPageList = await listResolver(data[parentPageListItem.pageUri][schema.itemListElement])
77 | return await m.addSubPage(parentPageListItem, { name }, { position: subPageList.length })
78 | }
79 |
80 | const updateName = useCallback(async (page: m.Page, value: string) => {
81 | await Promise.all([
82 | data[page.uri][schema.name].set(value),
83 | data[page.inListItem][schema.name].set(value),
84 | data.from(page.metaUri)[page.uri][schema.name].set(value)
85 | ])
86 | }, [])
87 |
88 | const updateText = useCallback(async (doc: m.Document, value: string, conceptUris: string[]) => {
89 | const conceptUrisSet = new Set(conceptUris)
90 | const references: string[] = await listValuesResolver(data[doc.uri][dct.references])
91 | const referencesSet = new Set(references)
92 | const toAdd = conceptUris.filter(x => !referencesSet.has(x))
93 | const toDelete = references.filter(x => !conceptUrisSet.has(x))
94 | if (workspace) {
95 | await m.setDocumentText(workspace, doc, value, toAdd, toDelete)
96 | }
97 |
98 | }, [workspace])
99 |
100 | const deleteDocument = useCallback(async (document: m.Document) => {
101 | await data[document.parentUri][schema.itemListElement].delete(namedNode(document.inListItem))
102 | }, [])
103 |
104 | return (
105 |
108 | )
109 | }
110 |
111 | export default WorkspaceContext;
112 |
--------------------------------------------------------------------------------
/src/hooks/acls.ts:
--------------------------------------------------------------------------------
1 | import solid from 'solid-auth-client';
2 | import { useState, useEffect } from 'react';
3 | import { useLiveUpdate } from '@solid/react';
4 | import { getAccessInfo, getParentACLUri, AccessInfo } from "../utils/acl"
5 |
6 | export function useAccessInfo(documentUri: string) {
7 | const [error, setError] = useState(undefined);
8 | const [accessInfo, setAccessInfo] = useState({});
9 | const [loading, setLoading] = useState(false);
10 | useEffect(() => {
11 | if (documentUri) {
12 | const fetchUri = async () => {
13 | setLoading(true)
14 | try {
15 | setAccessInfo(await getAccessInfo(documentUri))
16 | } catch (e) {
17 | setError(e)
18 | }
19 | setLoading(false)
20 | }
21 | fetchUri()
22 | }
23 | }, [documentUri])
24 | return { loading, error, ...accessInfo }
25 | }
26 |
27 | export function useAclExists(aclUri: string) {
28 | const [error, setError] = useState(undefined);
29 | const [exists, setExists] = useState({});
30 | const [loading, setLoading] = useState(false);
31 | const { timestamp, url } = useLiveUpdate()
32 | const [thisTimestamp, setThisTimestamp] = useState(timestamp)
33 | if ((timestamp !== thisTimestamp) && (url === aclUri)) {
34 | setThisTimestamp(timestamp)
35 | }
36 | useEffect(() => {
37 | if (aclUri) {
38 | const fetchUri = async () => {
39 | setLoading(true)
40 | try {
41 | const response = await solid.fetch(aclUri, { method: 'HEAD' })
42 | setExists(response.ok)
43 | } catch (e) {
44 | setError(e)
45 | }
46 | setLoading(false)
47 | }
48 | fetchUri()
49 | }
50 | }, [aclUri, timestamp])
51 | return { loading, error, exists }
52 | }
53 |
54 | export function useParentAcl(pageUri: string) {
55 | const [error, setError] = useState();
56 | const [aclUri, setAclUri] = useState();
57 | const [loading, setLoading] = useState(false);
58 | useEffect(() => {
59 | if (pageUri) {
60 | const fetchUri = async () => {
61 | setLoading(true)
62 | try {
63 | setAclUri(await getParentACLUri(pageUri))
64 | } catch (e) {
65 | setError(e)
66 | }
67 | setLoading(false)
68 | }
69 | fetchUri()
70 | }
71 | }, [pageUri])
72 | return { loading, error, uri: aclUri }
73 | }
74 |
--------------------------------------------------------------------------------
/src/hooks/backup.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, MutableRefObject } from 'react'
2 | import { schema, dct } from 'rdf-namespaces';
3 | import solidNamespace from 'solid-namespace';
4 | import data from '@solid/query-ldflex';
5 | import { namedNode, literal } from '@rdfjs/data-model';
6 | import { Node } from 'slate'
7 | import { resourceExists, createNonExistentDocument } from '../utils/ldflex-helper'
8 | import { backupFolderForPage } from '../utils/backups'
9 | import concept from '../ontology'
10 | import { Document } from '../utils/model'
11 |
12 | const ns = solidNamespace()
13 |
14 | const EVERY_MINUTE = 60 * 1000
15 | const EVERY_FIVE_MINUTES = 5 * EVERY_MINUTE
16 | const EVERY_TEN_MINUTES = 10 * EVERY_MINUTE
17 | const EVERY_THIRTY_MINUTES = 30 * EVERY_MINUTE
18 |
19 | type BodyRef = MutableRefObject
20 |
21 | async function ensureBackupFileExists(pageUri: string, backup: string) {
22 | const exists = await resourceExists(backup)
23 | if (!exists) {
24 | await createNonExistentDocument(backup)
25 | }
26 | const backupOf = data[backup][concept.backupOf]
27 | if (!await backupOf) {
28 | await backupOf.set(namedNode(pageUri))
29 | }
30 | }
31 |
32 | async function ensureBackupFolderExists(backupFolder: string) {
33 | const metaFile = `${backupFolder}.meta`
34 | const exists = await resourceExists(metaFile)
35 | if (!exists) {
36 | await createNonExistentDocument(metaFile)
37 | }
38 | }
39 |
40 | export async function createBackup(pageUri: string, backupFile: string, value: string) {
41 | const folder = backupFolderForPage(pageUri)
42 | const metaFile = `${folder}.meta`
43 | const backup = `${folder}${backupFile}`
44 | await ensureBackupFileExists(pageUri, backup)
45 | await data[backup][schema.text].set(value)
46 | await data[backup][dct.modified].set(literal(new Date().toISOString(), ns.xsd("dateTime")))
47 | await data.from(metaFile)[backup][dct.modified].set(literal(new Date().toISOString(), ns.xsd("dateTime")))
48 | }
49 |
50 | function createBackupInterval(bodyRef: BodyRef, page: Document, backupFile: string, interval: number) {
51 | return setInterval(async () => {
52 | if (bodyRef.current !== undefined) {
53 | await createBackup(page.uri, backupFile, JSON.stringify(bodyRef.current))
54 | }
55 | }, interval)
56 | }
57 |
58 | export function useBackups(page: Document, value: Node[] | undefined) {
59 | const bodyRef: BodyRef = useRef()
60 | bodyRef.current = value
61 | useEffect(() => {
62 | const backupFolder = backupFolderForPage(page.uri)
63 | ensureBackupFolderExists(backupFolder)
64 | const one = createBackupInterval(bodyRef, page, `oneMinute.ttl`, EVERY_MINUTE)
65 | const five = createBackupInterval(bodyRef, page, `fiveMinutes.ttl`, EVERY_FIVE_MINUTES)
66 | const ten = createBackupInterval(bodyRef, page, `tenMinutes.ttl`, EVERY_TEN_MINUTES)
67 | const thirty = createBackupInterval(bodyRef, page, `thirtyMinutes.ttl`, EVERY_THIRTY_MINUTES)
68 | return () => {
69 | clearInterval(one)
70 | clearInterval(five)
71 | clearInterval(ten)
72 | clearInterval(thirty)
73 | }
74 | }, [page, bodyRef])
75 | }
76 |
--------------------------------------------------------------------------------
/src/hooks/concepts.ts:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import { useConcept } from "./data"
3 | import { Concept } from "../utils/model"
4 |
5 | export function useCurrentConcept(): [Concept | undefined, boolean, Error | undefined] {
6 | const { selectedConcept } = useParams();
7 | return useConcept(selectedConcept ? decodeURIComponent(selectedConcept) : undefined)
8 | }
9 |
10 | export function useCurrentConceptUri(): string | undefined {
11 | const { selectedConcept } = useParams();
12 | return selectedConcept ? decodeURIComponent(selectedConcept) : undefined
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/data.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useMemo } from 'react';
2 | import { useWebId, useLiveUpdate } from '@solid/react';
3 | import { space, schema } from 'rdf-namespaces';
4 | import { appContainerUrl } from '../utils/urls';
5 | import {
6 | Resolver, dateResolver, valueResolver, listResolver, listValuesResolver,
7 | pageListItemsResolver, conceptListItemsResolver,
8 | pageResolver, conceptResolver, documentResolver
9 | } from '../utils/data';
10 | import data from '@solid/query-ldflex';
11 | import { Document, Concept, Page, PageContainer, PageListItem, ConceptContainer, ConceptListItem } from '../utils/model'
12 |
13 | export function useAppContainer() {
14 | const webId = useWebId();
15 | const [storage] = useValueQuery(webId, space.storage)
16 | return storage && appContainerUrl(storage)
17 | }
18 |
19 | export function useWorkspace() {
20 | const appContainer = useAppContainer()
21 | const workspace = useMemo(() => {
22 | const workspaceContainer = appContainer && `${appContainer}workspace/`
23 | const docUri = workspaceContainer && `${workspaceContainer}index.ttl`
24 | return docUri && {
25 | docUri,
26 | uri: `${docUri}#Workspace`,
27 | containerUri: workspaceContainer,
28 | subpageContainerUri: `${workspaceContainer}pages/`
29 | }
30 | }, [appContainer]);
31 | return workspace
32 | }
33 |
34 | type QueryTerm = string | undefined
35 | type UseQueryResult = [T | undefined, boolean, Error | undefined]
36 | type QueryOptions = { source?: string, skip?: boolean, resolver?: Resolver }
37 |
38 | const defaultResolver: Resolver = async (query: any) => query
39 |
40 | function useQuery(subject: QueryTerm, predicate: QueryTerm | null, { source = subject, skip = false, resolver = defaultResolver }: QueryOptions): UseQueryResult {
41 | const { url: updatedUri, timestamp } = useLiveUpdate()
42 | const [updatedTimestamp, setUpdatedTimestamp] = useState(timestamp)
43 | useEffect(() => {
44 | if (!skip && source) {
45 | try {
46 | const url = new URL(source)
47 | url.hash = ''
48 | const docUri = url.toString()
49 | if (updatedUri === docUri) {
50 | setUpdatedTimestamp(timestamp)
51 | }
52 | } catch (e) {
53 | setError(e)
54 | }
55 | }
56 | }, [source, updatedUri, timestamp, skip])
57 |
58 | const [loading, setLoading] = useState(false)
59 | const [error, setError] = useState()
60 | const [result, setResult] = useState()
61 | useEffect(() => {
62 | if (!skip && (source !== undefined) && (subject !== undefined) && (predicate !== undefined)) {
63 | const updateResult = async () => {
64 | setLoading(true)
65 | try {
66 | var query = data.from(source)[subject]
67 | if (predicate !== null) {
68 | query = query[predicate]
69 | }
70 | setResult(await resolver(query))
71 | } catch (e) {
72 | setError(e)
73 | }
74 | setLoading(false)
75 | }
76 | updateResult()
77 | }
78 | }, [source, subject, predicate, updatedTimestamp, resolver, skip])
79 | return [result, loading, error]
80 | }
81 |
82 | export function useListQuery(subject: QueryTerm, predicate: QueryTerm, options: QueryOptions = {}) {
83 | return useQuery(subject, predicate, { resolver: listResolver, ...options })
84 | }
85 |
86 | export function useListValuesQuery(subject: QueryTerm, predicate: QueryTerm, options: QueryOptions = {}) {
87 | return useQuery(subject, predicate, { resolver: listValuesResolver, ...options })
88 | }
89 |
90 | export function useValueQuery(subject: QueryTerm, predicate: QueryTerm, options: QueryOptions = {}) {
91 | return useQuery(subject, predicate, { resolver: valueResolver, ...options })
92 | }
93 |
94 | export function useDateQuery(subject: QueryTerm, predicate: QueryTerm, options: QueryOptions = {}) {
95 | return useQuery(subject, predicate, { resolver: dateResolver, ...options })
96 | }
97 |
98 |
99 | export function usePageListItems(parent: PageContainer | undefined, options: QueryOptions = {}) {
100 | return useQuery(parent && parent.pagesUri, schema.itemListElement, { resolver: pageListItemsResolver, ...options })
101 | }
102 |
103 | export function useConceptListItems(parent: ConceptContainer | undefined, options: QueryOptions = {}) {
104 | return useQuery(parent && parent.conceptsUri, schema.itemListElement, { resolver: conceptListItemsResolver, ...options })
105 | }
106 |
107 | export function usePageFromPageListItem(pageListItem: PageListItem, options: QueryOptions = {}) {
108 | return useQuery(pageListItem && pageListItem.pageUri, null, { resolver: pageResolver, ...options })
109 | }
110 |
111 | export function useConcept(conceptUri: string | undefined, options: QueryOptions = {}) {
112 | return useQuery(conceptUri, null, { resolver: conceptResolver, ...options })
113 | }
114 |
115 | export function usePage(pageUri: string | undefined, options: QueryOptions = {}) {
116 | return useQuery(pageUri, null, { resolver: pageResolver, ...options })
117 | }
118 |
119 | export function useDocument(documentUri: string | undefined, options: QueryOptions = {}) {
120 | return useQuery(documentUri, null, { resolver: documentResolver, ...options })
121 | }
122 |
--------------------------------------------------------------------------------
/src/hooks/pages.ts:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import { usePage } from "./data"
3 | import { Page } from "../utils/model"
4 |
5 | export function useCurrentPage(): [Page | undefined, boolean, Error | undefined] {
6 | const { selectedPage } = useParams();
7 | return usePage(selectedPage ? decodeURIComponent(selectedPage) : undefined)
8 | }
9 |
10 | export function useCurrentPageUri(): string | undefined {
11 | const { selectedPage } = useParams();
12 | return selectedPage ? decodeURIComponent(selectedPage) : undefined
13 | }
14 |
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 |
4 | import Backend from 'i18next-xhr-backend';
5 | import LanguageDetector from 'i18next-browser-languagedetector';
6 | // not like to use this?
7 | // have a look at the Quick start guide
8 | // for passing in lng and translations on init
9 |
10 | i18n
11 | // load translation using xhr -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
12 | // learn more: https://github.com/i18next/i18next-xhr-backend
13 | .use(Backend)
14 | // detect user language
15 | // learn more: https://github.com/i18next/i18next-browser-languageDetector
16 | .use(LanguageDetector)
17 | // pass the i18n instance to react-i18next.
18 | .use(initReactI18next)
19 | // init i18next
20 | // for all options read: https://www.i18next.com/overview/configuration-options
21 | .init({
22 | fallbackLng: 'en',
23 | debug: true,
24 |
25 | interpolation: {
26 | escapeValue: false, // not needed for react as it escapes by default
27 | }
28 | });
29 |
30 |
31 | export default i18n;
32 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 | import './i18n'
7 | import * as Sentry from '@sentry/browser';
8 |
9 | Sentry.init({
10 | dsn: "https://2837c0eb80034804b6315f25c0a0e519@o382054.ingest.sentry.io/5211463",
11 | environment: process.env.NODE_ENV,
12 | release: `concept@${process.env.REACT_APP_VERSION}`
13 | });
14 |
15 | ReactDOM.render( , document.getElementById('root'));
16 |
17 | // If you want your app to work offline and load faster, you can change
18 | // unregister() to register() below. Note this comes with some pitfalls.
19 | // Learn more about service workers: https://bit.ly/CRA-PWA
20 | serviceWorker.unregister();
21 |
--------------------------------------------------------------------------------
/src/ontology.ts:
--------------------------------------------------------------------------------
1 | const root = "https://useconcept.art/ontology#"
2 |
3 | interface ConceptOntology {
4 | backupOf: string,
5 | parent: string,
6 | inListItem: string
7 | }
8 |
9 | const ns: ConceptOntology = {
10 | backupOf: `${root}backupOf`,
11 | parent: `${root}parent`,
12 | inListItem: `${root}inListItem`,
13 | }
14 |
15 | export default ns
16 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/solid-namespace/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'solid-namespace';
2 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles';
2 |
3 | export default createMuiTheme({
4 | zIndex: {
5 | mobileStepper: 800,
6 | speedDial: 850,
7 | appBar: 900,
8 | drawer: 950,
9 | modal: 1000,
10 | snackbar: 1050,
11 | tooltip: 1100
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/src/utils/acl.ts:
--------------------------------------------------------------------------------
1 | import solid from 'solid-auth-client';
2 | import { parse as parseWAC } from 'wac-allow'
3 |
4 | type Allowed = {
5 | user: Set,
6 | public: Set
7 | }
8 |
9 | export type AccessInfo = {
10 | aclUri?: string,
11 | allowed?: Allowed
12 | }
13 |
14 | export async function getAccessInfo(pageUri: string): Promise {
15 | try {
16 | const response = await solid.fetch(pageUri, { method: 'HEAD' });
17 | const allowed = parseWAC(response.headers.get('WAC-Allow'))
18 | // for concept, acls are stored at the container level to ensure subpages inherit permissions by default
19 | const aclUri = `${pageUri.split("/").slice(0, -1).join("/")}/.acl`
20 | return { aclUri, allowed }
21 | } catch (error) {
22 | throw error;
23 | }
24 | };
25 |
26 | export const defaultAcl = (webId: string, container: string) => `@prefix acl: .
27 | @prefix foaf: .
28 | @prefix n: .
29 | @prefix rdf: .
30 | @prefix : <${container}.acl#>.
31 |
32 | :Owners a acl:Authorization;
33 | acl:accessTo <${container}>;
34 | acl:default <${container}>;
35 | acl:agent <${webId}>;
36 | acl:mode acl:Read, acl:Write, acl:Control.
37 | :Writers a acl:Authorization;
38 | acl:accessTo <${container}>;
39 | acl:default <${container}>;
40 | acl:mode acl:Read, acl:Write.
41 | :Readers a acl:Authorization;
42 | acl:accessTo <${container}>;
43 | acl:default <${container}>;
44 | acl:mode acl:Read.
45 | :Public a acl:Authorization;
46 | acl:accessTo <${container}>;
47 | acl:default <${container}>;
48 | acl:agentClass foaf:Agent.
49 | `
50 |
51 | export const createDefaultAcl = (webId: string, container: string) => solid.fetch(`${container}.acl`, {
52 | method: 'PUT',
53 | headers: {
54 | 'Content-Type': 'text/turtle'
55 | },
56 | body: defaultAcl(webId, container)
57 | })
58 |
59 | // thanks, https://github.com/inrupt/solid-react-components/blob/develop/src/lib/classes/access-control-list.js
60 | export const getParentACLUri = async (url: string): Promise => {
61 | const newURL = new URL(url);
62 | const { pathname } = newURL;
63 | const hasParent = pathname.length > 1;
64 | if (!hasParent) return undefined;
65 | const isContainer = pathname.endsWith('/');
66 | let newPathname = isContainer ? pathname.slice(0, pathname.length - 1) : pathname;
67 | newPathname = `${newPathname.slice(0, newPathname.lastIndexOf('/'))}/`;
68 | const parentURI = `${newURL.origin}${newPathname}`;
69 | const result = await solid.fetch(`${parentURI}.acl`, { method: "HEAD" });
70 | if (result.status === 404) return getParentACLUri(parentURI);
71 | if (result.status === 200) return `${parentURI}.acl`;
72 |
73 | return undefined;
74 | };
75 |
--------------------------------------------------------------------------------
/src/utils/backups.ts:
--------------------------------------------------------------------------------
1 | export function backupFolderForPage(pageUri: string) {
2 | return pageUri && `${pageUri.split(".").slice(0, -1).join(".")}/backups/`
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/data.ts:
--------------------------------------------------------------------------------
1 | import data from '@solid/query-ldflex';
2 | import { namedNode } from '@rdfjs/data-model';
3 | import { acl, schema, dc, dct, foaf } from 'rdf-namespaces';
4 | import concept from '../ontology'
5 | import { pageUris, conceptUris, documentUris } from './model'
6 | import { patchDocument } from '../utils/ldflex-helper'
7 | import { Document, Concept, Page, PageListItem, ConceptListItem } from '../utils/model'
8 |
9 | const aclNamespace: any = acl
10 |
11 | export const follow = (webId: string, followWebId: string) =>
12 | data[webId][foaf.knows].add(namedNode(followWebId))
13 |
14 | export const unfollow = (webId: string, unfollowWebId: string) =>
15 | data[webId][foaf.knows].delete(namedNode(unfollowWebId))
16 |
17 | export const addPublicPage = (publicPageListUri: string, page: string) =>
18 | data[publicPageListUri][schema.itemListElement].add(namedNode(page))
19 |
20 | export const removePublicPage = (publicPageListUri: string, page: string) =>
21 | data[publicPageListUri][schema.itemListElement].delete(namedNode(page))
22 |
23 | export const addPublicAccess = (publicAccessUri: string, accessType: string) =>
24 | data[publicAccessUri][acl.mode].add(namedNode(aclNamespace[accessType]))
25 |
26 | export const removePublicAccess = async (publicAccessUri: string, accessType: string, publicPages: string, page: string, hasWrite: boolean) => {
27 | if (accessType === "Write") {
28 | await data[publicAccessUri][acl.mode].delete(namedNode(acl.Write))
29 | } else if (accessType === "Read") {
30 | await Promise.all([
31 | hasWrite ? (
32 | patchDocument(publicAccessUri, `
33 | DELETE DATA {
34 | <${publicAccessUri}>
35 | <${acl.mode}> <${acl.Read}> ;
36 | <${acl.mode}> <${acl.Write}> .
37 | }
38 | `)
39 | ) : (
40 | data[publicAccessUri][acl.mode].delete(namedNode(acl.Read))
41 | ),
42 | removePublicPage(publicPages, page)
43 | ])
44 | }
45 | }
46 |
47 | export type Resolver = (query: any) => Promise
48 |
49 | export const listResolver: Resolver = async query => {
50 | const newResult = []
51 | for await (const result of query) {
52 | newResult.push(result)
53 | }
54 | return newResult
55 | }
56 |
57 | export const resolveValues: (terms: any[]) => any[] = terms => terms.map(term => term && term.value)
58 |
59 | export const listValuesResolver: Resolver = async query => resolveValues(await listResolver(query))
60 |
61 | export const valueResolver: Resolver = async query => {
62 | const term = await query
63 | return term && term.value
64 | }
65 |
66 | export const dateResolver: Resolver = async query => {
67 | const term = await query
68 | return term && new Date(term.value)
69 | }
70 |
71 | export const pageListItemResolver: Resolver = async query => {
72 | const [uri, name, pageUri] = resolveValues(await Promise.all([
73 | query,
74 | query[schema.name],
75 | query[schema.item]
76 | ]))
77 | return { uri, name, pageUri }
78 | }
79 |
80 | export const pageListItemsResolver: Resolver = async query => {
81 | const itemQueries = await listResolver(query.sort(schema.position))
82 | return Promise.all(itemQueries.map(pageListItemResolver))
83 | }
84 |
85 | export const conceptListItemResolver: Resolver = async query => {
86 | const [uri, name, conceptUri] = resolveValues(await Promise.all([
87 | query,
88 | query[schema.name],
89 | query[schema.item]
90 | ]))
91 | return { uri, name, conceptUri }
92 | }
93 |
94 | export const conceptListItemsResolver: Resolver = async query => {
95 | const itemQueries = await listResolver(query.sort(schema.name))
96 | return Promise.all(itemQueries.map(conceptListItemResolver))
97 | }
98 |
99 | export const pageResolver: Resolver = async query => {
100 | const [uri, id, text, name, inListItem, parentUri] = resolveValues(await Promise.all([
101 | query, query[dc.identifier], query[schema.text], query[schema.name],
102 | query[concept.inListItem], query[concept.parent]
103 | ]))
104 | return { id, text, name, uri, parentUri, inListItem, ...pageUris(uri) }
105 | }
106 |
107 | export const conceptResolver: Resolver = async query => {
108 | const [uri, id, text, name, inListItem, parentUri] = resolveValues(await Promise.all([
109 | query, query[dc.identifier], query[schema.text], query[schema.name],
110 | query[concept.inListItem], query[concept.parent]
111 | ]))
112 | const referencedBy = await listValuesResolver(query[dct.isReferencedBy])
113 | return { id, text, name, uri, parentUri, inListItem, referencedBy, ...conceptUris(uri) }
114 | }
115 |
116 | export const documentResolver: Resolver = async query => {
117 | const [uri, id, text, name, inListItem, parentUri] = resolveValues(await Promise.all([
118 | query, query[dc.identifier], query[schema.text], query[schema.name],
119 | query[concept.inListItem], query[concept.parent]
120 | ]))
121 | return { id, text, name, uri, parentUri, inListItem, ...documentUris(uri) }
122 | }
123 |
--------------------------------------------------------------------------------
/src/utils/editor.js:
--------------------------------------------------------------------------------
1 | import { Editor, Transforms, Range, Point, Element, Text, Path } from 'slate';
2 | import { ReactEditor} from 'slate-react';
3 |
4 | import imageExtensions from 'image-extensions'
5 | import isUrl from 'is-url'
6 |
7 |
8 | const LIST_TYPES = ['numbered-list', 'bulleted-list']
9 |
10 | export const isMarkActive = (editor, format) => {
11 | const marks = Editor.marks(editor)
12 | return marks ? marks[format] === true : false
13 | }
14 |
15 | export const toggleMark = (editor, format) => {
16 | const isActive = isMarkActive(editor, format)
17 | if (isActive) {
18 | Editor.removeMark(editor, format)
19 | } else {
20 | Editor.addMark(editor, format, true)
21 | }
22 | }
23 |
24 |
25 | export const isBlockActive = (editor, format, at=editor.selection) => {
26 | const [match] = Editor.nodes(editor, {
27 | at,
28 | match: n => n.type === format,
29 | })
30 |
31 | return !!match
32 | }
33 |
34 | export const toggleBlock = (editor, format, at=editor.selection) => {
35 | const isActive = isBlockActive(editor, format, at)
36 | const isList = LIST_TYPES.includes(format)
37 |
38 | Transforms.unwrapNodes(editor, {
39 | at,
40 | match: n => LIST_TYPES.includes(n.type),
41 | split: true,
42 | })
43 |
44 | Transforms.setNodes(editor, {
45 | type: isActive ? 'paragraph' : isList ? 'list-item' : format,
46 | }, { at })
47 |
48 | if (!isActive && isList) {
49 | const block = { type: format, children: [] }
50 | Transforms.wrapNodes(editor, block, { at })
51 | }
52 | }
53 |
54 | export const makeBlock = (editor, format, at=editor.selection) => {
55 | const isList = LIST_TYPES.includes(format)
56 |
57 | Transforms.unwrapNodes(editor, {
58 | at,
59 | match: n => LIST_TYPES.includes(n.type),
60 | split: true,
61 | })
62 |
63 | Transforms.setNodes(editor, {
64 | type: isList ? 'list-item' : format,
65 | }, { at })
66 |
67 | if (isList) {
68 | const block = { type: format, children: [] }
69 | Transforms.wrapNodes(editor, block, { at })
70 | }
71 | }
72 |
73 | export const insertBlock = (editor, format, at=editor.selection, attributes={}) => {
74 | const isList = LIST_TYPES.includes(format)
75 | if (format === "table") {
76 | Transforms.insertNodes(editor, {
77 | type: "table",
78 | children: [{
79 | type: "table-row",
80 | children: [{
81 | type: "table-cell",
82 | children: [{text: ""}]
83 | }]
84 | }],
85 | ...attributes
86 | }, { at })
87 | } else if (isList) {
88 | Transforms.insertNodes(editor, {
89 | type: format, children: [ { type: "list-item", children: []}],
90 | ...attributes
91 | }, { at })
92 | } else {
93 | Transforms.insertNodes(editor,
94 | { type: format, children: [],
95 | ...attributes
96 | },
97 | { at })
98 | }
99 | }
100 |
101 | export const insertRow = (editor, table) => {
102 | const path = ReactEditor.findPath(editor, table)
103 | Transforms.insertNodes(editor, {
104 | type: "table-row", children: Array(table.children[0].children.length).fill().map(
105 | () => ({ type: "table-cell", children: [{text: ""}]})
106 | )
107 | }, { at: [...path, table.children.length] })
108 | }
109 |
110 | export const removeRow = (editor, table) => {
111 | const path = ReactEditor.findPath(editor, table)
112 | Transforms.removeNodes(editor, { at: [...path, table.children.length - 1] })
113 | }
114 |
115 | export const insertColumn = (editor, table) => {
116 | const firstRow = table.children[0]
117 | const firstRowPath = ReactEditor.findPath(editor, firstRow)
118 | for (let i = 0; i < table.children.length; i++){
119 | Transforms.insertNodes(editor, {
120 | type: "table-cell", children: [{text: ""}]
121 | }, { at: [...firstRowPath.slice(0, -1), i, firstRow.children.length] })
122 | }
123 | }
124 |
125 | export const removeColumn = (editor, table) => {
126 | const firstRow = table.children[0]
127 | const firstRowPath = ReactEditor.findPath(editor, firstRow)
128 | for (let i = 0; i < table.children.length; i++){
129 | Transforms.removeNodes(editor, {
130 | at: [...firstRowPath.slice(0, -1), i, firstRow.children.length - 1]
131 | })
132 | }
133 | }
134 |
135 | const isImageUrl = url => {
136 | if (!url) return false
137 | if (!isUrl(url)) return false
138 | const ext = new URL(url).pathname.split('.').pop()
139 | return imageExtensions.includes(ext)
140 | }
141 |
142 | export const withImages = editor => {
143 | const { insertData, isVoid } = editor
144 |
145 | editor.isVoid = element => {
146 | return element.type === 'image' ? true : isVoid(element)
147 | }
148 |
149 | editor.insertData = data => {
150 | const text = data.getData('text/plain')
151 | const { files } = data
152 |
153 | if (files && files.length > 0) {
154 | for (const file of files) {
155 | const reader = new FileReader()
156 | const [mime] = file.type.split('/')
157 |
158 | if (mime === 'image') {
159 | reader.addEventListener('load', () => {
160 | const url = reader.result
161 | insertImage(editor, {url})
162 | })
163 |
164 | reader.readAsDataURL(file)
165 | }
166 | }
167 | } else if (isImageUrl(text)) {
168 | insertImage(editor, {url: text})
169 | } else {
170 | insertData(data)
171 | }
172 | }
173 |
174 | return editor
175 | }
176 |
177 | export const insertImage = (editor, attributes, at=editor.selection) => {
178 | const text = { text: '' }
179 | const image = { type: 'image', children: [text], ...attributes }
180 | Transforms.insertNodes(editor, image, {at})
181 | }
182 |
183 | export const isLinkActive = editor => {
184 | const [link] = Editor.nodes(editor, { match: n => n.type === 'link' })
185 | return !!link
186 | }
187 |
188 | const unwrapLink = editor => {
189 | Transforms.unwrapNodes(editor, { match: n => n.type === 'link' })
190 | }
191 |
192 | const wrapLink = (editor, url) => {
193 | if (isLinkActive(editor)) {
194 | unwrapLink(editor)
195 | }
196 |
197 | const { selection } = editor
198 | const isCollapsed = selection && Range.isCollapsed(selection)
199 | const link = {
200 | type: 'link',
201 | url,
202 | children: isCollapsed ? [{ text: url }] : [],
203 | }
204 |
205 | if (isCollapsed) {
206 | Transforms.insertNodes(editor, link)
207 | } else {
208 | Transforms.wrapNodes(editor, link, { split: true })
209 | Transforms.collapse(editor, { edge: 'end' })
210 | }
211 | }
212 |
213 | const disallowEmpty = (type, editor) => {
214 | const { normalizeNode } = editor
215 |
216 | editor.normalizeNode = entry => {
217 | const [node, path] = entry
218 | if (Element.isElement(node) && (node.type === type) &&
219 | (node.children.length === 1) && Text.isText(node.children[0]) &&
220 | (node.children[0].text === "")) {
221 | const currentlySelected = Path.isCommon(path, editor.selection.anchor.path)
222 | Transforms.removeNodes(editor, {at: path})
223 | if (currentlySelected) {
224 | Transforms.select(editor, path)
225 | Transforms.collapse(editor)
226 | }
227 | }
228 | normalizeNode(entry)
229 | }
230 | }
231 |
232 | export const withLinks = editor => {
233 | const { insertData, insertText, isInline } = editor
234 |
235 | editor.isInline = element => {
236 | return element.type === 'link' ? true : isInline(element)
237 | }
238 |
239 | editor.insertText = text => {
240 | if (text && isUrl(text)) {
241 | wrapLink(editor, text)
242 | } else {
243 | insertText(text)
244 | }
245 | }
246 |
247 | editor.insertData = data => {
248 | const text = data.getData('text/plain')
249 |
250 | if (text && isUrl(text)) {
251 | wrapLink(editor, text)
252 | } else {
253 | insertData(data)
254 | }
255 | }
256 |
257 | disallowEmpty("link", editor)
258 |
259 | return editor
260 | }
261 |
262 | export const insertLink = (editor, url) => {
263 | if (editor.selection) {
264 | wrapLink(editor, url)
265 | }
266 | }
267 |
268 | export const removeLink = (editor) => {
269 | unwrapLink(editor)
270 | }
271 |
272 | export const setLinkUrl = (editor, link, url) => {
273 | const path = ReactEditor.findPath(editor, link)
274 | Transforms.setNodes(editor, {url}, {at: path})
275 | }
276 |
277 | export const setConceptProps = (editor, concept, name, uri) => {
278 | const path = ReactEditor.findPath(editor, concept)
279 | Transforms.setNodes(editor, {name, uri}, {at: path})
280 | }
281 |
282 | const unwrapConcept = editor => {
283 | Transforms.unwrapNodes(editor, { match: n => n.type === 'concept' })
284 | }
285 |
286 | const wrapConcept = (editor, name, uri) => {
287 | if (isConceptActive(editor)) {
288 | unwrapConcept(editor)
289 | }
290 |
291 | const { selection } = editor
292 | const isCollapsed = selection && Range.isCollapsed(selection)
293 | const concept = {
294 | type: 'concept',
295 | name,
296 | uri,
297 | children: isCollapsed ? [{ text: name }] : [],
298 | }
299 |
300 | if (isCollapsed) {
301 | Transforms.insertNodes(editor, concept)
302 | } else {
303 | Transforms.wrapNodes(editor, concept, { split: true })
304 | Transforms.collapse(editor, { edge: 'end' })
305 | }
306 | }
307 |
308 | export const removeConcept = (editor) => {
309 | unwrapConcept(editor)
310 | }
311 |
312 | export const isConceptActive = editor => {
313 | const [concept] = Editor.nodes(editor, { match: n => n.type === 'concept' })
314 | return !!concept
315 | }
316 |
317 | export const insertConcept = (editor, name, uri) => {
318 | if (editor.selection) {
319 | wrapConcept(editor, name, uri)
320 | }
321 | }
322 |
323 | export const withConcepts = editor => {
324 | const { isInline } = editor
325 |
326 | editor.isInline = element => (element.type === 'concept') ? true : isInline(element)
327 | disallowEmpty("concept", editor)
328 |
329 | return editor
330 | }
331 |
332 | export const withChecklists = editor => {
333 | const { deleteBackward } = editor
334 |
335 | editor.deleteBackward = (...args) => {
336 | const { selection } = editor
337 |
338 | if (selection && Range.isCollapsed(selection)) {
339 | const [match] = Editor.nodes(editor, {
340 | match: n => n.type === 'check-list-item',
341 | })
342 |
343 | if (match) {
344 | const [, path] = match
345 | const start = Editor.start(editor, path)
346 |
347 | if (Point.equals(selection.anchor, start)) {
348 | Transforms.setNodes(
349 | editor,
350 | { type: 'paragraph' },
351 | { match: n => n.type === 'check-list-item' }
352 | )
353 | return
354 | }
355 | }
356 | }
357 |
358 | deleteBackward(...args)
359 | }
360 |
361 | return editor
362 | }
363 |
364 | export const withLists = editor => {
365 | const { deleteBackward } = editor
366 |
367 | editor.deleteBackward = (...args) => {
368 | const { selection } = editor
369 |
370 | if (selection && Range.isCollapsed(selection)) {
371 | const [match] = Editor.nodes(editor, {
372 | match: n => n.type === 'list-item',
373 | })
374 | if (match) {
375 | const [, path] = match
376 | const start = Editor.start(editor, path)
377 | if (Point.equals(selection.anchor, start)) {
378 | Transforms.unwrapNodes(editor, {
379 | match: n => LIST_TYPES.includes(n.type),
380 | split: true,
381 | })
382 |
383 | Transforms.setNodes(
384 | editor,
385 | { type: 'paragraph' },
386 | { match: n => n.type === 'list-item' }
387 | )
388 | return
389 | }
390 | }
391 | }
392 |
393 | deleteBackward(...args)
394 | }
395 |
396 | return editor
397 | }
398 |
399 | export const withTables = editor => {
400 | const { deleteBackward, deleteForward, insertBreak } = editor
401 |
402 | editor.deleteBackward = unit => {
403 | const { selection } = editor
404 |
405 | if (selection && Range.isCollapsed(selection)) {
406 | const [cell] = Editor.nodes(editor, {
407 | match: n => n.type === 'table-cell',
408 | })
409 |
410 | if (cell) {
411 | const [, cellPath] = cell
412 | const start = Editor.start(editor, cellPath)
413 |
414 | if (Point.equals(selection.anchor, start)) {
415 | return
416 | }
417 | }
418 | }
419 |
420 | deleteBackward(unit)
421 | }
422 |
423 | editor.deleteForward = unit => {
424 | const { selection } = editor
425 |
426 | if (selection && Range.isCollapsed(selection)) {
427 | const [cell] = Editor.nodes(editor, {
428 | match: n => n.type === 'table-cell',
429 | })
430 |
431 | if (cell) {
432 | const [, cellPath] = cell
433 | const end = Editor.end(editor, cellPath)
434 |
435 | if (Point.equals(selection.anchor, end)) {
436 | return
437 | }
438 | }
439 | }
440 |
441 | deleteForward(unit)
442 | }
443 |
444 | editor.insertBreak = () => {
445 | const { selection } = editor
446 |
447 | if (selection) {
448 | const [table] = Editor.nodes(editor, { match: n => n.type === 'table' })
449 |
450 | if (table) {
451 | return
452 | }
453 | }
454 |
455 | insertBreak()
456 | }
457 |
458 | return editor
459 | }
460 |
461 | export const withEmbeds = editor => {
462 | const { isVoid } = editor
463 | editor.isVoid = element => (element.type === 'embed' ? true : isVoid(element))
464 | return editor
465 | }
466 |
467 | export const insertionPoint = (editor, element) => {
468 | const path = ReactEditor.findPath(editor, element)
469 | return (
470 | [...path.slice(0, -1), path.slice(-1)[0] + 1]
471 | )
472 | }
473 |
--------------------------------------------------------------------------------
/src/utils/fonts.js:
--------------------------------------------------------------------------------
1 | export const titleFont = "'Special Elite', cursive"
2 |
--------------------------------------------------------------------------------
/src/utils/ldflex-helper.js:
--------------------------------------------------------------------------------
1 | import auth from 'solid-auth-client';
2 | import ldflex from '@solid/query-ldflex';
3 |
4 | /*
5 | Thanks, https://github.com/inrupt/generator-solid-react/blob/develop/generators/app/templates/src/utils/ldflex-helper.js
6 | */
7 |
8 | export const documentExists = async documentUri =>
9 | auth.fetch(documentUri, {
10 | method: 'HEAD',
11 | headers: {
12 | 'Content-Type': 'text/turtle'
13 | }
14 | });
15 |
16 | export const createDoc = async (documentUri, options) => {
17 | try {
18 | return await auth.fetch(documentUri, options);
19 | } catch (e) {
20 | throw e;
21 | }
22 | };
23 |
24 | export const deleteFile = async url => {
25 | try {
26 | return await auth.fetch(url, { method: 'DELETE' });
27 | } catch (e) {
28 | throw e;
29 | }
30 | };
31 |
32 | export const createDocument = async (documentUri, body = '') => {
33 | try {
34 | const options = {
35 | method: 'PUT',
36 | headers: {
37 | 'Content-Type': 'text/turtle'
38 | },
39 | body
40 | };
41 | return await createDoc(documentUri, options);
42 | } catch (e) {
43 | throw e;
44 | }
45 | };
46 |
47 | export const patchDocument = async (documentUri, body = '') => {
48 | try {
49 | const options = {
50 | method: 'PATCH',
51 | headers: {
52 | 'Content-Type': 'application/sparql-update'
53 | },
54 | body
55 | };
56 | return await auth.fetch(documentUri, options);
57 | } catch (e) {
58 | throw e;
59 | }
60 | };
61 |
62 | export const createDocumentWithTurtle = async (documentUri, body = '') => {
63 | try {
64 | return createDoc(documentUri, {
65 | method: 'PUT',
66 | headers: {
67 | 'Content-Type': 'text/turtle'
68 | },
69 | body
70 | });
71 | } catch (e) {
72 | throw e;
73 | }
74 | };
75 |
76 | export const createNonExistentDocument = async (documentUri, body = '') => {
77 | try {
78 | const result = await documentExists(documentUri);
79 |
80 | return result.status === 404 ? createDocument(documentUri, body) : null;
81 | } catch (e) {
82 | throw e;
83 | }
84 | };
85 |
86 | export const fetchLdflexDocument = async documentUri => {
87 | try {
88 | const result = await documentExists(documentUri);
89 | if (result.status === 404) return null;
90 | const document = await ldflex[documentUri];
91 | return document;
92 | } catch (e) {
93 | throw e;
94 | }
95 | };
96 |
97 | export const resourceExists = async resourcePath => {
98 | try {
99 | const result = await auth.fetch(resourcePath, {method: 'HEAD'});
100 | return result.status === 403 || result.status === 200;
101 | } catch (e) {
102 | console.log("error: ", e)
103 | }
104 | };
105 |
106 | export const discoverInbox = async document => {
107 | try {
108 | const documentExists = await resourceExists(document);
109 | if (!documentExists) return false;
110 |
111 | const inboxDocument = await ldflex[document]['ldp:inbox'];
112 | const inbox = inboxDocument ? await inboxDocument.value : false;
113 | return inbox;
114 | } catch (error) {
115 | throw error;
116 | }
117 | };
118 |
119 | /**
120 | * Given a resource link, find an inbox linked from it, if any exist
121 | * @param resourcePath
122 | * @returns {Promise}
123 | */
124 | export const getLinkedInbox = async resourcePath => {
125 | try {
126 | const inboxLinkedPath = await ldflex[resourcePath].inbox;
127 | if (inboxLinkedPath) {
128 | return inboxLinkedPath.value;
129 | }
130 | return '';
131 | } catch (error) {
132 | throw error;
133 | }
134 | };
135 |
--------------------------------------------------------------------------------
/src/utils/model.ts:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid/v1';
2 | import { schema, rdf, dc, dct } from 'rdf-namespaces';
3 | import data from '@solid/query-ldflex';
4 | import { resourceExists, createDocument, patchDocument } from './ldflex-helper';
5 | import cpt from '../ontology';
6 | import { pageResolver } from './data';
7 | import { conceptNameToUrlSafeId, urlSafeIdToConceptName, conceptContainerUrl, publicPagesUrl } from '../utils/urls';
8 |
9 | export interface Subject {
10 | uri: string,
11 | docUri: string,
12 | containerUri: string
13 | }
14 |
15 | export interface PageContainer extends Subject {
16 | subpageContainerUri: string,
17 | pagesUri: string
18 | }
19 |
20 | export interface ConceptContainer extends Subject {
21 | conceptContainerUri: string
22 | conceptsUri: string,
23 | }
24 |
25 | export interface Document extends Subject {
26 | id: string,
27 | name: string,
28 | text: string,
29 | imageContainerUri: string,
30 | metaUri: string,
31 | inListItem: string,
32 | parentUri: string
33 | }
34 |
35 | export interface Workspace extends PageContainer, ConceptContainer {
36 | publicPages: string,
37 | }
38 |
39 | export interface Concept extends Document {
40 | referencedBy: string[]
41 | }
42 |
43 | export interface Page extends PageContainer, Document {
44 | }
45 |
46 | export interface PageListItem {
47 | uri: string,
48 | name: string,
49 | pageUri: string
50 | }
51 |
52 | export interface ConceptListItem {
53 | uri: string,
54 | name: string,
55 | conceptUri: string
56 | }
57 |
58 | export interface PageProps {
59 | name?: string
60 | }
61 |
62 | export interface ConceptProps {
63 | name: string
64 | }
65 |
66 | export interface PageListItemProps {
67 | position?: number
68 | }
69 |
70 | export function isPage(document: Document): document is Page {
71 | return (document as Page).pagesUri !== undefined
72 | }
73 |
74 | export function isConcept(document: Document): document is Concept {
75 | return (document as Concept).referencedBy !== undefined
76 | }
77 |
78 | const initialDocumentText = JSON.stringify([
79 | {
80 | type: 'paragraph',
81 | children: [{ text: '' }]
82 | }
83 | ])
84 |
85 | export function conceptUris(uri: string) {
86 | const { containerUri, docUri, metaUri, imageContainerUri } = documentUris(uri)
87 | return ({ containerUri, docUri, uri, imageContainerUri, metaUri })
88 | }
89 |
90 | type ConceptOptions = {
91 | referencedBy?: string
92 | }
93 |
94 | export function newConcept(workspace: Workspace, name: string, options: ConceptOptions = {}): Concept {
95 | // ok this looks insane but we want to support arbitrary characters in names and there are
96 | // some issues with % chars in path segments in browsers: https://github.com/ReactTraining/history/issues/505
97 | const id = conceptNameToUrlSafeId(name)
98 | const inListItem = `${workspace.docUri}#${id}`
99 | const referencedBy = options.referencedBy ? [options.referencedBy] : []
100 | return ({
101 | id,
102 | name,
103 | parentUri: workspace.conceptsUri,
104 | text: initialDocumentText,
105 | inListItem,
106 | referencedBy,
107 | ...conceptUris(`${workspace.conceptContainerUri}${id}/index.ttl#Concept`)
108 | })
109 | }
110 |
111 |
112 | const addConceptMetadata = async (parent: ConceptContainer, concept: Concept) => {
113 | await Promise.all([
114 | patchDocument(parent.docUri, `
115 | INSERT DATA {
116 | <${concept.inListItem}>
117 | <${rdf.type}> <${schema.ListItem}> ;
118 | <${schema.name}> """${concept.name}""" ;
119 | <${schema.item}> <${concept.uri}> .
120 |
121 | <${parent.conceptsUri}> <${schema.itemListElement}> <${concept.inListItem}> .
122 | }
123 | `),
124 | createDocument(concept.metaUri, `
125 | <${concept.uri}> <${schema.name}> """${concept.name}""" .
126 | `)
127 | ])
128 | return concept
129 | }
130 |
131 |
132 | const optionalConceptDoubles = ({ referencedBy }: ConceptOptions) => {
133 | if (referencedBy) {
134 | return `<${dct.isReferencedBy}> <${referencedBy}> ;`
135 | } else {
136 | return ""
137 | }
138 | }
139 |
140 | export const addConcept = async (workspace: Workspace, name: string, options: ConceptOptions = {}) => {
141 | const concept = newConcept(workspace, name, options)
142 | await Promise.all([
143 | createDocument(concept.docUri, `
144 | <${concept.uri}>
145 | <${rdf.type}> <${schema.DigitalDocument}> ;
146 | <${dc.identifier}> "${concept.id}" ;
147 | <${schema.text}> """${concept.text}""" ;
148 | <${schema.name}> """${concept.name}""" ;
149 | ${optionalConceptDoubles(options)}
150 | <${cpt.parent}> <${workspace.conceptsUri}> .
151 | `),
152 | addConceptMetadata(workspace, concept)
153 | ])
154 | return concept
155 | }
156 |
157 | export function metaForPageUri(pageUri: string) {
158 | return `${pageUri.split("/").slice(0, -1).join("/")}/.meta`
159 | }
160 |
161 | export function documentUris(uri: string) {
162 | const containerUri = `${uri.split("/").slice(0, -1).join("/")}/`
163 | const docUri = `${containerUri}index.ttl`
164 | const metaUri = `${containerUri}.meta`
165 | const imageContainerUri = `${containerUri}images/`
166 | return { containerUri, docUri, metaUri, imageContainerUri }
167 | }
168 |
169 | export function pageUris(uri: string) {
170 | const { containerUri, docUri, metaUri, imageContainerUri } = documentUris(uri)
171 | const subpageContainerUri = `${containerUri}pages/`
172 | const pagesUri = `${docUri}#Pages`
173 | return ({ containerUri, docUri, uri, subpageContainerUri, imageContainerUri, metaUri, pagesUri })
174 | }
175 |
176 | export function newPage(parent: PageContainer, { name = "Untitled" } = {}): Page {
177 | const id = uuid()
178 | const inListItem = `${parent.docUri}#${id}`
179 | return ({
180 | id,
181 | name,
182 | text: initialDocumentText,
183 | inListItem,
184 | parentUri: parent.pagesUri,
185 | ...pageUris(`${parent.subpageContainerUri}${id}/index.ttl#Page`)
186 | })
187 | }
188 |
189 | const addPageMetadata = async (parent: PageContainer, page: Page, props: PageListItemProps = {}) => {
190 | await Promise.all([
191 | patchDocument(parent.docUri, `
192 | INSERT DATA {
193 | <${page.inListItem}>
194 | <${rdf.type}> <${schema.ListItem}> ;
195 | <${schema.item}> <${page.uri}> ;
196 | <${schema.name}> """${page.name}""" ;
197 | <${schema.position}> "${props.position || 0}"^^ .
198 | <${parent.pagesUri}> <${schema.itemListElement}> <${page.inListItem}> .
199 | }
200 | `),
201 | createDocument(page.metaUri, `
202 | <${page.uri}> <${schema.name}> """${page.name}""" .
203 | `)
204 | ])
205 | return page
206 | }
207 |
208 | export const addPage = async (parent: PageContainer, pageProps = {}, pageListItemProps = {}) => {
209 | const page = newPage(parent, pageProps)
210 | await createDocument(page.docUri, `
211 | <${page.uri}>
212 | <${rdf.type}> <${schema.DigitalDocument}> ;
213 | <${dc.identifier}> "${page.id}" ;
214 | <${schema.text}> """${page.text}""" ;
215 | <${schema.name}> """${page.name}""" ;
216 | <${cpt.parent}> <${parent.pagesUri}> ;
217 | <${cpt.inListItem}> <${page.inListItem}> .
218 | `)
219 | await addPageMetadata(parent, page, pageListItemProps)
220 | return page
221 | }
222 |
223 | const conceptDocFromConceptUri = (conceptUri: string) =>
224 | conceptUri.split("#").slice(0, -1).join("")
225 |
226 | const conceptNameFromConceptUri = (conceptUri: string) =>
227 | urlSafeIdToConceptName(conceptUri.split("/").slice(-2)[0])
228 |
229 | const addConceptReferencedBy = async (workspace: Workspace, docUri: string, conceptUri: string) => {
230 | const resourceUri = conceptDocFromConceptUri(conceptUri)
231 | if (await resourceExists(resourceUri)) {
232 | await patchDocument(resourceUri, `
233 | INSERT DATA {
234 | <${conceptUri}> <${dct.isReferencedBy}> <${docUri}>
235 | }
236 | `)
237 | } else {
238 | await addConcept(workspace, conceptNameFromConceptUri(conceptUri), { referencedBy: docUri })
239 | }
240 | }
241 |
242 | const addConceptReferencedBys = async (workspace: Workspace, docUri: string, conceptUris: string[]) => {
243 | await Promise.all(conceptUris.map(
244 | conceptUri => addConceptReferencedBy(workspace, docUri, conceptUri)
245 | ))
246 | }
247 |
248 | const deleteConceptReferencedBy = async (docUri: string, conceptUri: string) => {
249 | await patchDocument(conceptDocFromConceptUri(conceptUri), `
250 | DELETE DATA {
251 | <${conceptUri}> <${dct.isReferencedBy}> <${docUri}>
252 | }
253 | `)
254 | }
255 |
256 | const deleteConceptReferencedBys = async (docUri: string, conceptUris: string[]) => {
257 | await Promise.all(conceptUris.map(
258 | conceptUri => deleteConceptReferencedBy(docUri, conceptUri)
259 | ))
260 | }
261 |
262 | const referenceDoubles = (references: string[]) =>
263 | references.map(reference => `<${dct.references}> <${reference}> ;`).join("")
264 |
265 | export const setDocumentText = async (workspace: Workspace, doc: Document, newText: string, referencesToAdd: string[], referencesToDelete: string[]) => {
266 | await Promise.all([
267 | patchDocument(doc.docUri, `
268 | DELETE DATA {
269 | <${doc.uri}>
270 | ${referenceDoubles(referencesToDelete)}
271 | <${schema.text}> """${doc.text}""" .
272 | } ;
273 | INSERT DATA {
274 | <${doc.uri}>
275 | ${referenceDoubles(referencesToAdd)}
276 | <${schema.text}> """${newText}""" .
277 | }
278 | `),
279 | deleteConceptReferencedBys(doc.uri, referencesToDelete),
280 | addConceptReferencedBys(workspace, doc.uri, referencesToAdd)
281 | ])
282 | }
283 |
284 |
285 | export const addSubPage = async (pageListItem: PageListItem, pageProps = {}, pageListItemProps = {}) => {
286 | const parentPage = await pageResolver(data[pageListItem.pageUri])
287 | return await addPage(parentPage, pageProps, pageListItemProps)
288 | }
289 |
290 | export function workspaceFromStorage(storage: string): Workspace {
291 | const conceptContainer = conceptContainerUrl(storage)
292 | const publicPages = publicPagesUrl(conceptContainer)
293 | const workspaceContainer = `${conceptContainer}workspace/`
294 | const workspaceDoc = `${workspaceContainer}index.ttl`
295 | const uri = `${workspaceDoc}#Workspace`
296 | const pagesUri = `${workspaceDoc}#Pages`
297 | const conceptsUri = `${workspaceDoc}#Concepts`
298 | return ({
299 | containerUri: workspaceContainer,
300 | uri,
301 | pagesUri,
302 | conceptsUri,
303 | docUri: workspaceDoc,
304 | subpageContainerUri: `${workspaceContainer}pages/`,
305 | conceptContainerUri: `${workspaceContainer}concepts/`,
306 | publicPages
307 | })
308 |
309 | }
310 |
--------------------------------------------------------------------------------
/src/utils/slate.ts:
--------------------------------------------------------------------------------
1 | import { Node } from "slate"
2 |
3 | export const getConceptNodes = (node: Node) => Array.from(Node.nodes(node)).filter(([n]) => {
4 | return (n.type === 'concept')
5 | })
6 |
7 | export const getConceptNodesMatchingName = (node: Node, name: string) => Array.from(Node.nodes(node)).filter(([n]) => {
8 | return (n.type === 'concept') && (n.name === name)
9 | })
10 |
--------------------------------------------------------------------------------
/src/utils/urls.ts:
--------------------------------------------------------------------------------
1 | import base32 from 'base32'
2 |
3 | export const conceptContainerUrl = (storage: string) => `${storage}private/concept/v5.12/`
4 | export const appContainerUrl = conceptContainerUrl
5 | export const publicPagesUrl = (conceptContainer: string) => `${conceptContainer}publicPages.ttl`
6 | export const pagePath = (page: string) => `/page/${encodeURIComponent(page)}`
7 | export const sharingUrl = (page: string) => `https://useconcept.art${pagePath(page)}`
8 | export const webIdProfilePath = (webId: string) => `/webid/${encodeURIComponent(webId)}`
9 | export const handleProfilePath = (handle: string) => `/for/${handle}`
10 | export const handleHausUriForWebId = (webId: string) => `https://handle.haus/webids/${encodeURIComponent(webId)}#Person`
11 |
12 | export const conceptNameToUrlSafeId = (name: string) =>
13 | base32.encode(encodeURIComponent(name))
14 | export const urlSafeIdToConceptName = (id: string) =>
15 | decodeURIComponent(base32.decode(id))
16 |
17 | export const conceptPath = (conceptUri: string) => `/concept/${encodeURIComponent(conceptUri)}`
18 | export const conceptUrl = (conceptUri: string) => {
19 | const u = new URL(window.location.toString())
20 | u.pathname = conceptPath(conceptUri)
21 | return u.toString()
22 | }
23 | export const conceptUri = (container: string, name: string) => `${container}${conceptNameToUrlSafeId(name)}/index.ttl#Concept`
24 | export const documentPath = (documentUri: string) => {
25 | if (documentUri.endsWith("Concept")) {
26 | return conceptPath(documentUri)
27 | } else if (documentUri.endsWith("Page")) {
28 | return pagePath(documentUri)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/wac-allow/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'wac-allow';
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------