From c2a7f87500539464e7bc6790b34241096d0c76c1 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:23:45 +0100 Subject: [PATCH 01/16] Icons open on new tabs e099ef --- data-browser/src/components/SideBar/About.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data-browser/src/components/SideBar/About.tsx b/data-browser/src/components/SideBar/About.tsx index 184d4b128..04a2b2996 100644 --- a/data-browser/src/components/SideBar/About.tsx +++ b/data-browser/src/components/SideBar/About.tsx @@ -38,6 +38,8 @@ export function About() { {aboutMenuItems.map(({ href, icon, helper }) => ( Date: Tue, 21 Feb 2023 09:31:27 +0100 Subject: [PATCH 02/16] Update links in readme --- data-browser/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/data-browser/README.md b/data-browser/README.md index 034a54816..f76d05ecf 100644 --- a/data-browser/README.md +++ b/data-browser/README.md @@ -77,11 +77,11 @@ You can set the Agent on the `/app/agent` route. The tests are located in `tests` and have `.spec` in their filename. They use the PlayWright framework and run in the browser. -- make sure the data-browser server is running (`pnpm start`) at `http://localhost:8080` -- make sure an [`atomic-server`](https://crates.io/crates/atomic-server/) instance is running at `http://localhost` +- make sure the data-browser server is running (`pnpm start`) at `http://localhost:5173` +- make sure an [`atomic-server`](https://crates.io/crates/atomic-server/) instance is running at `http://localhost:9883` - make sure the `http://localhost/setup` invite has at least one available usage. You can set a higher amount [here](http://localhost/app/edit?subject=http%3A%2F%2Flocalhost%2Fsetup), or run `atomic-server --inititalize` to reset it to 1. -- Install the Playwright dependencies: `npx playwright install-deps` -- `pnpm test` launches the E2E tests (make sure the dev server is running at `http://localhost:8080`) +- Install the Playwright dependencies: `pnpm playwright-install` +- `pnpm test` launches the E2E tests (make sure the dev server is running at `http://localhost:5173`) - `pnpm test-debug` launches the E2E tests in debug mode (a window opens with debug tools) - `pnpm test-new` create new tests by clicking through the app - Use the `data-test` attribute in HTML elements to make playwright tests more maintainable (and prevent failing tests on changing translations) @@ -92,9 +92,8 @@ They use the PlayWright framework and run in the browser. GitHub Action / Workflow is used for: - Linting (ESlint) -- Testing (in the browser using `playwright`, using an `atomic-server` docker image) - Building -- Deploying JS build artefacts & assets to GH pages (note that `atomic-server` hosts the JS assets by itself) +- Testing (in the browser using `playwright`, using an `atomic-server` docker image) ## Contribute From 3755832f34426b994b58fe4bce6858d62d4ffb2b Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:25:29 +0100 Subject: [PATCH 03/16] Import JSON AD string function --- CHANGELOG.md | 3 +++ lib/src/endpoints.ts | 11 +++++++++++ lib/src/store.ts | 16 ++++++++++++++++ react/src/useImporter.ts | 6 +++++- 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 lib/src/endpoints.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b770a373b..16fc6f501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This changelog covers all three packages, as they are (for now) updated as a who - Add `Store.parseMetaTags` to load JSON-AD objects stored in the DOM. Speeds up initial page load by allowing server to set JSON-AD objects in the initial HTML response. - Move static assets around, align build with server and fix PWA #292 +- `store.createSubject` allows creating nested paths +- Add `useChildren` hook and `Store.getChildren` method +- Add `Store.postToServer` method, add `endpoints`, `import_json_ad_string` ## v0.35.0 diff --git a/lib/src/endpoints.ts b/lib/src/endpoints.ts new file mode 100644 index 000000000..087a2b12c --- /dev/null +++ b/lib/src/endpoints.ts @@ -0,0 +1,11 @@ +import { Store } from './index.js'; +/** Endpoints are Resources that can respond to query parameters or POST bodies */ + +/** POSTs a JSON-AD object to the Server */ +export function importJsonAdString( + store: Store, + importerUrl: string, + jsonAdString: string, +) { + return store.postToServer(importerUrl, jsonAdString); +} diff --git a/lib/src/store.ts b/lib/src/store.ts index 1928c219c..30ab83dfe 100644 --- a/lib/src/store.ts +++ b/lib/src/store.ts @@ -435,6 +435,22 @@ export class Store { }); } + /** Sends an HTTP POST request to the server to the Subject. Parses the returned Resource and adds it to the store. */ + public async postToServer( + parent: string, + data: ArrayBuffer | string, + ): Promise { + const url = new URL(parent); + url.searchParams.set('parent', parent); + url.pathname = '/import'; + + return this.fetchResourceFromServer(url.toString(), { + body: data, + noWebSocket: true, + method: 'POST', + }); + } + /** Removes (destroys / deletes) resource from this store */ public removeResource(subject: string): void { const resource = this.resources.get(subject); diff --git a/react/src/useImporter.ts b/react/src/useImporter.ts index 7a7b37ae8..174bd7172 100644 --- a/react/src/useImporter.ts +++ b/react/src/useImporter.ts @@ -1,5 +1,9 @@ import { useEffect, useState } from 'react'; -import { useResource } from './index.js'; +import { + importJsonAdString as importJsonAdString, + useResource, + useStore, +} from './index.js'; /** Easily send JSON-AD or a URL containing it to your server. */ export function useImporter(importerUrl?: string) { From 6496a3de5e412387b22370a753cf66ccc370f123 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 14 Feb 2023 23:43:25 +0100 Subject: [PATCH 04/16] Add script tag for atomic-server --- data-browser/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data-browser/index.html b/data-browser/index.html index 83f6c9e67..48cc22277 100644 --- a/data-browser/index.html +++ b/data-browser/index.html @@ -34,6 +34,8 @@ + + From 6f7b877e7507e0be084f4ef8d56839666c997c57 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:26:01 +0100 Subject: [PATCH 05/16] Improve error handler --- data-browser/src/components/Toaster.tsx | 54 +++++++++++++++----- data-browser/src/handlers/errorHandler.ts | 6 +-- data-browser/src/helpers/loggingHandlers.tsx | 2 + 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/data-browser/src/components/Toaster.tsx b/data-browser/src/components/Toaster.tsx index 5f052454a..8e91beb8d 100644 --- a/data-browser/src/components/Toaster.tsx +++ b/data-browser/src/components/Toaster.tsx @@ -1,6 +1,6 @@ import React from 'react'; import toast, { ToastBar, Toaster as ReactHotToast } from 'react-hot-toast'; -import { FaTimes } from 'react-icons/fa'; +import { FaCopy, FaTimes } from 'react-icons/fa'; import { useTheme } from 'styled-components'; import { zIndex } from '../styling'; import { Button } from './Button'; @@ -37,22 +37,48 @@ export function Toaster(): JSX.Element { }} > {({ icon, message }) => ( - <> - {icon} - {message} - {t.type !== 'loading' && ( - - )} - + )} )} ); } + +function ToastMessage({ icon, message, t }) { + let text = message.props.children; + + function handleCopy() { + toast.success('Copied error to clipboard'); + navigator.clipboard.writeText(message.props.children); + toast.dismiss(t.id); + } + + if (text.length > 100) { + text = text.substring(0, 100) + '...'; + } + + return ( + <> + {icon} + {text} + {t.type !== 'loading' && ( +
+ + {t.type !== 'success' && ( + + )} +
+ )} + + ); +} diff --git a/data-browser/src/handlers/errorHandler.ts b/data-browser/src/handlers/errorHandler.ts index 71713c7ae..947bde90d 100644 --- a/data-browser/src/handlers/errorHandler.ts +++ b/data-browser/src/handlers/errorHandler.ts @@ -4,11 +4,7 @@ import { handleError } from '../helpers/loggingHandlers'; export const errorHandler = (e: Error) => { handleError(e); - let message = e.message; - - if (e.message.length > 100) { - message = e.message.substring(0, 100) + '...'; - } + const message = e.message; toast.error(message); }; diff --git a/data-browser/src/helpers/loggingHandlers.tsx b/data-browser/src/helpers/loggingHandlers.tsx index e2374ee76..0cea23394 100644 --- a/data-browser/src/helpers/loggingHandlers.tsx +++ b/data-browser/src/helpers/loggingHandlers.tsx @@ -6,6 +6,8 @@ import React from 'react'; import { isDev } from '../config'; export function handleError(e: Error): void { + // We already toast in the `errorHandler` + // toast.error(e.message); console.error(e); if (!isDev) { From cb25616a00050b704abcb40cab6b9af14644ad49 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 14 Feb 2023 23:41:59 +0100 Subject: [PATCH 06/16] Add `useChildren` hook and `Store.getChildren` method --- lib/src/resource.ts | 15 +++++++++++++++ lib/src/websockets.ts | 4 +++- react/src/index.ts | 1 + react/src/useChildren.ts | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 react/src/useChildren.ts diff --git a/lib/src/resource.ts b/lib/src/resource.ts index 2e76a06cf..dff4d413b 100644 --- a/lib/src/resource.ts +++ b/lib/src/resource.ts @@ -176,6 +176,21 @@ export class Resource { return this.commitBuilder; } + /** Returns the subject of the list of Children */ + public getChildrenCollection(): string | undefined { + // We create a collection that contains all children of the current Subject + const generatedCollectionURL = new URL(this.subject); + generatedCollectionURL.pathname = '/collections'; + generatedCollectionURL.searchParams.set('property', properties.parent); + generatedCollectionURL.searchParams.set('value', this.subject); + + const childrenCollection = generatedCollectionURL.toString(); + + console.log('Children collection', childrenCollection); + + return childrenCollection; + } + /** Returns the subject URL of the Resource */ public getSubject(): string { return this.subject; diff --git a/lib/src/websockets.ts b/lib/src/websockets.ts index 9c316622d..56437c909 100644 --- a/lib/src/websockets.ts +++ b/lib/src/websockets.ts @@ -75,7 +75,9 @@ export async function authenticate(client: WebSocket, store: Store) { !client.url.startsWith('ws://localhost:') && agent?.subject?.startsWith('http://localhost') ) { - console.warn("Can't authenticate localhost Agent over websocket"); + console.warn( + "Can't authenticate localhost Agent over websocket with remote server, because the server will nog be able to retrieve your Agent and verify your public key.", + ); return; } diff --git a/react/src/index.ts b/react/src/index.ts index 5ee70cf05..606664ddb 100644 --- a/react/src/index.ts +++ b/react/src/index.ts @@ -25,6 +25,7 @@ export * from './hooks.js'; export * from './useServerURL.js'; export * from './useCurrentAgent.js'; +export * from './useChildren.js'; export * from './useDebounce.js'; export * from './useImporter.js'; export * from './useLocalStorage.js'; diff --git a/react/src/useChildren.ts b/react/src/useChildren.ts new file mode 100644 index 000000000..4a034334b --- /dev/null +++ b/react/src/useChildren.ts @@ -0,0 +1,15 @@ +// Sorry for the name of this +import { properties, Resource } from '@tomic/lib'; +import { useArray, useResource } from './index.js'; + +/** Creates a Collection and returns all children */ +export const useChildren = (resource: Resource) => { + const childrenUrl = resource.getChildrenCollection(); + const childrenCollection = useResource(childrenUrl); + const [children] = useArray( + childrenCollection, + properties.collection.members, + ); + + return children; +}; From 28087878af50ce4b8d5a7c46fe3a7899f37ab9a2 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:29:01 +0100 Subject: [PATCH 07/16] #282 cookie clean up --- lib/src/authentication.ts | 10 +++++++--- lib/src/store.ts | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/src/authentication.ts b/lib/src/authentication.ts index 7173a6dac..b72befaab 100644 --- a/lib/src/authentication.ts +++ b/lib/src/authentication.ts @@ -3,6 +3,7 @@ import { getTimestampNow, HeadersObject, signToBase64, + Store, } from './index.js'; /** Returns a JSON-AD resource of an Authentication */ @@ -91,10 +92,13 @@ const setCookieExpires = ( document.cookie = cookieString; }; +const COOKIE_NAME_AUTH = 'atomic_session'; + /** Sets a cookie for the current Agent, signing the Authentication. It expires after some default time. */ -export const setCookieAuthentication = (serverUrl: string, agent: Agent) => { - createAuthentication(serverUrl, agent).then(auth => { - setCookieExpires('atomic_session', btoa(JSON.stringify(auth)), serverUrl); +export const setCookieAuthentication = (store: Store, agent: Agent) => { + const serverURL = store.getServerUrl(); + createAuthentication(serverURL, agent).then(auth => { + setCookieExpires(COOKIE_NAME_AUTH, btoa(JSON.stringify(auth)), serverURL); }); }; diff --git a/lib/src/store.ts b/lib/src/store.ts index 30ab83dfe..82980b344 100644 --- a/lib/src/store.ts +++ b/lib/src/store.ts @@ -1,4 +1,7 @@ -import { setCookieAuthentication } from './authentication.js'; +import { + removeCookieAuthentication, + setCookieAuthentication, +} from './authentication.js'; import { EventManager } from './EventManager.js'; import { Agent, @@ -504,6 +507,8 @@ export class Store { this.fetchResourceFromServer(r.getSubject()); } }); + } else { + removeCookieAuthentication(); } this.eventManager.emit(StoreEvents.AgentChanged, agent); From ee954cbfeeb537517c0e870eada49e75873614f7 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:30:25 +0100 Subject: [PATCH 08/16] Fix ChatRoom message input height and autoresize --- data-browser/src/views/ChatRoomPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data-browser/src/views/ChatRoomPage.tsx b/data-browser/src/views/ChatRoomPage.tsx index e16e8eb9f..a31489a9c 100644 --- a/data-browser/src/views/ChatRoomPage.tsx +++ b/data-browser/src/views/ChatRoomPage.tsx @@ -21,6 +21,7 @@ import { CommitDetail } from '../components/CommitDetail'; import Markdown from '../components/datatypes/Markdown'; import { Detail } from '../components/Detail'; import { EditableTitle } from '../components/EditableTitle'; +import { NavBarSpacer } from '../components/NavBarSpacer'; import { editURL } from '../helpers/navigation'; import { ResourceInline } from './ResourceInline'; import { ResourcePageProps } from './ResourcePage'; @@ -136,7 +137,7 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { // in Firefox, scrollHeight only works if overflow is set to scroll const height = e.target.scrollHeight; e.target.style.overflow = overflowStyle; - const rowHeight = 25; + const rowHeight = 30; const trows = Math.ceil(height / rowHeight) - 1; if (trows !== textAreaHight) { @@ -180,6 +181,7 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { Send + ); } From 7068f0565f8bf841beb91e766868443b31cf6319 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Thu, 27 Oct 2022 08:46:37 +0200 Subject: [PATCH 09/16] Update PR template --- pull_request_template.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pull_request_template.md b/pull_request_template.md index ab40dda5c..2d9eae341 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,6 +1,7 @@ PR Checklist: -- [ ] Link to related issues: +- [ ] Link to related issues: #number - [ ] Add changelog entry linking to issue - [ ] Add tests (if needed) -- [ ] (If new feature) added in description / readme +- [ ] If dependent on server-side changes: link to PR on `atomic-data-rust` +- [ ] If new feature: added in description / readme From a2932dcf3ef011338b7ec665e6eab2c758352023 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Sun, 16 Oct 2022 12:18:58 +0200 Subject: [PATCH 10/16] #248 open only one menu on keyboard shortcut --- data-browser/src/components/Dropdown/index.tsx | 5 ++++- data-browser/src/components/Navigation.tsx | 1 + .../src/components/ResourceContextMenu/index.tsx | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/data-browser/src/components/Dropdown/index.tsx b/data-browser/src/components/Dropdown/index.tsx index 5989d5811..fe1766258 100644 --- a/data-browser/src/components/Dropdown/index.tsx +++ b/data-browser/src/components/Dropdown/index.tsx @@ -26,6 +26,8 @@ interface DropdownMenuProps { /** The list of menu items */ items: Item[]; trigger: DropdownTriggerRenderFunction; + /** Enables the keyboard shortcut */ + isMainMenu?: boolean; } /** Gets the index of an array and loops around when at the beginning or end */ @@ -88,6 +90,7 @@ function normalizeItems(items: Item[]) { export function DropdownMenu({ items, trigger, + isMainMenu, }: DropdownMenuProps): JSX.Element { const menuId = useId(); const dropdownRef = useRef(null); @@ -167,7 +170,7 @@ export function DropdownMenu({ handleToggle(); setUseKeys(true); }, - {}, + { enabled: !!isMainMenu }, [isActive], ); // Click / open the item diff --git a/data-browser/src/components/Navigation.tsx b/data-browser/src/components/Navigation.tsx index 35986a936..b59ca357f 100644 --- a/data-browser/src/components/Navigation.tsx +++ b/data-browser/src/components/Navigation.tsx @@ -136,6 +136,7 @@ function NavBar(): JSX.Element { {showButtons && subject && ( diff --git a/data-browser/src/components/ResourceContextMenu/index.tsx b/data-browser/src/components/ResourceContextMenu/index.tsx index 5b839e0b0..95d66a180 100644 --- a/data-browser/src/components/ResourceContextMenu/index.tsx +++ b/data-browser/src/components/ResourceContextMenu/index.tsx @@ -31,6 +31,8 @@ export interface ResourceContextMenuProps { hide?: string[]; trigger?: DropdownTriggerRenderFunction; simple?: boolean; + /** If it's the primary menu in the navbar. Used for triggering keyboard shortcut */ + isMainMenu?: boolean; } /** Dropdown menu that opens a bunch of actions for some resource */ @@ -39,6 +41,7 @@ function ResourceContextMenu({ hide, trigger, simple, + isMainMenu, }: ResourceContextMenuProps) { const store = useStore(); const navigate = useNavigate(); @@ -149,7 +152,13 @@ function ResourceContextMenu({ const triggerComp = trigger ?? buildDefaultTrigger(); - return ; + return ( + + ); } export default ResourceContextMenu; From 40f5730d0e6582a205089308f8c266dcce0c1c98 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Wed, 12 Oct 2022 13:26:18 +0200 Subject: [PATCH 11/16] Fix empty agent --- react/src/useLocalStorage.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/react/src/useLocalStorage.ts b/react/src/useLocalStorage.ts index b26a9899a..95aa48669 100644 --- a/react/src/useLocalStorage.ts +++ b/react/src/useLocalStorage.ts @@ -17,6 +17,10 @@ export function useLocalStorage( // Get from local storage by key const item = window.localStorage.getItem(key); + if (item === 'undefined') { + return initialValue; + } + // Parse stored json or if none return initialValue return item ? JSON.parse(item) : initialValue; } catch (error) { From 84eda8632beadb3a6854fb1490dc4068410bc023 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Wed, 12 Oct 2022 21:48:24 +0200 Subject: [PATCH 12/16] Open new folders without dialog --- .../NewInstanceButton/NewFolderButton.tsx | 88 ------------- .../components/NewInstanceButton/index.tsx | 2 - .../NewInstanceButton/useCreateAndNavigate.ts | 2 +- .../useDefaultNewInstanceHandler.tsx | 117 +++++++++++------- lib/src/urls.ts | 1 + 5 files changed, 72 insertions(+), 138 deletions(-) delete mode 100644 data-browser/src/components/NewInstanceButton/NewFolderButton.tsx diff --git a/data-browser/src/components/NewInstanceButton/NewFolderButton.tsx b/data-browser/src/components/NewInstanceButton/NewFolderButton.tsx deleted file mode 100644 index 36f68b443..000000000 --- a/data-browser/src/components/NewInstanceButton/NewFolderButton.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { classes, properties, useResource, useTitle } from '@tomic/react'; -import React, { FormEvent, useCallback, useState } from 'react'; -import { Button } from '../Button'; -import { - Dialog, - DialogActions, - DialogContent, - DialogTitle, - useDialog, -} from '../Dialog'; -import Field from '../forms/Field'; -import { InputStyled, InputWrapper } from '../forms/InputStyles'; -import { Base } from './Base'; -import { useCreateAndNavigate } from './useCreateAndNavigate'; -import { NewInstanceButtonProps } from './NewInstanceButtonProps'; - -export function NewFolderButton({ - klass, - subtle, - icon, - IconComponent, - parent, - children, - label, -}: NewInstanceButtonProps): JSX.Element { - const resource = useResource(klass); - const [title] = useTitle(resource); - const [name, setName] = useState(''); - - const [dialogProps, show, hide] = useDialog(); - - const createResourceAndNavigate = useCreateAndNavigate(klass, parent); - - const onDone = useCallback( - (e: FormEvent) => { - e.preventDefault(); - - createResourceAndNavigate('Folder', { - [properties.name]: name, - [properties.displayStyle]: classes.displayStyles.list, - [properties.isA]: [classes.folder], - }); - }, - [name], - ); - - return ( - <> - - {children} - - - -

New Folder

-
- -
- - - setName(e.target.value)} - /> - - -
-
- - - - -
- - ); -} diff --git a/data-browser/src/components/NewInstanceButton/index.tsx b/data-browser/src/components/NewInstanceButton/index.tsx index 9277c09e0..af291bfcf 100644 --- a/data-browser/src/components/NewInstanceButton/index.tsx +++ b/data-browser/src/components/NewInstanceButton/index.tsx @@ -4,14 +4,12 @@ import { NewBookmarkButton } from './NewBookmarkButton'; import { NewInstanceButtonProps } from './NewInstanceButtonProps'; import { NewInstanceButtonDefault } from './NewInstanceButtonDefault'; import { useSettings } from '../../helpers/AppSettings'; -import { NewFolderButton } from './NewFolderButton'; type InstanceButton = (props: NewInstanceButtonProps) => JSX.Element; /** If your New Instance button requires custom logic, such as a custom dialog */ const classMap = new Map([ [classes.bookmark, NewBookmarkButton], - [classes.folder, NewFolderButton], ]); /** A button for creating a new instance of some thing */ diff --git a/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts b/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts index 9a8c3ba24..402ba779e 100644 --- a/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts +++ b/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts @@ -12,7 +12,7 @@ import { useNavigate } from 'react-router-dom'; import { constructOpenURL } from '../../helpers/navigation'; /** - * Hook that builds a function that will create a new resoure with the given + * Hook that builds a function that will create a new resource with the given * properties and then navigate to it. * * @param klass The type of resource to create a new instance of. diff --git a/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx b/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx index feb60307b..dbdc02315 100644 --- a/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx +++ b/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx @@ -11,6 +11,13 @@ import { useSettings } from '../../helpers/AppSettings'; import { newURL } from '../../helpers/navigation'; import { useCreateAndNavigate } from './useCreateAndNavigate'; +/** + * Returns a function that can be used to create a new instance of the given Class. + * This is the place where you can add custom behavior for certain classes. + * By default, we're redirected to an empty Form for the new instance. + * For some Classes, though, we'd rather have some values are pre-filled (e.g. a new ChatRoom with a `new chatroom` title). + * For others, we want to render a custom form, perhaps with a different layout. + */ export function useDefaultNewInstanceHandler(klass: string, parent?: string) { const store = useStore(); const { setDrive } = useSettings(); @@ -22,61 +29,77 @@ export function useDefaultNewInstanceHandler(klass: string, parent?: string) { const createResourceAndNavigate = useCreateAndNavigate(klass, parent); const onClick = useCallback(async () => { - switch (klass) { - case classes.chatRoom: { - createResourceAndNavigate('chatRoom', { - [properties.name]: 'New ChatRoom', - [properties.isA]: [classes.chatRoom], - }); - break; - } - - case classes.document: { - createResourceAndNavigate('documents', { - [properties.isA]: [classes.document], - [properties.name]: 'Untitled Document', - }); - break; - } + try { + switch (klass) { + case classes.chatRoom: { + createResourceAndNavigate('chatRoom', { + [properties.name]: 'Untitled ChatRoom', + [properties.isA]: [classes.chatRoom], + }); + break; + } - case classes.importer: { - createResourceAndNavigate('importer', { - [properties.isA]: [classes.importer], - }); - break; - } + case classes.document: { + createResourceAndNavigate('document', { + [properties.isA]: [classes.document], + [properties.name]: 'Untitled Document', + }); + break; + } - case classes.drive: { - const agent = store.getAgent(); + case classes.folder: { + createResourceAndNavigate('folder', { + [properties.isA]: [classes.folder], + [properties.name]: 'Untitled Folder', + [properties.displayStyle]: classes.displayStyles.list, + }); + break; + } - if (!agent || agent.subject === undefined) { - throw new Error( - 'No agent set in the Store, required when creating a Drive', - ); + case classes.importer: { + createResourceAndNavigate('importer', { + [properties.isA]: [classes.importer], + }); + break; } - const newResource = await createResourceAndNavigate( - 'drive', - { - [properties.isA]: [classes.drive], - [properties.write]: [agent.subject], - [properties.read]: [agent.subject], - }, - undefined, - true, - ); + case classes.drive: { + const agent = store.getAgent(); - const agentResource = await store.getResourceAsync(agent.subject); - agentResource.pushPropVal(properties.drives, newResource.getSubject()); - agentResource.save(store); - setDrive(newResource.getSubject()); - break; - } + if (!agent || agent.subject === undefined) { + throw new Error( + 'No agent set in the Store, required when creating a Drive', + ); + } + + const newResource = await createResourceAndNavigate( + 'drive', + { + [properties.isA]: [classes.drive], + [properties.write]: [agent.subject], + [properties.read]: [agent.subject], + }, + undefined, + true, + ); - default: { - // Opens an `Edit` form with the class and a decent subject name - navigate(newURL(klass, parent, store.createSubject(shortname))); + const agentResource = await store.getResourceAsync(agent.subject); + agentResource.pushPropVal( + properties.drives, + newResource.getSubject(), + ); + agentResource.save(store); + setDrive(newResource.getSubject()); + break; + } + + default: { + // Opens an `Edit` form with the class and a decent subject name + navigate(newURL(klass, parent, store.createSubject(shortname))); + } } + } catch (e) { + store.handleError(e); } }, [klass, store, parent, createResourceAndNavigate]); diff --git a/lib/src/urls.ts b/lib/src/urls.ts index fc4eab8a3..ac866ab4d 100644 --- a/lib/src/urls.ts +++ b/lib/src/urls.ts @@ -134,6 +134,7 @@ export const datatypes = { export const instances = { publicAgent: 'https://atomicdata.dev/agents/publicAgent', + displayStyleGrid: 'https://atomicdata.dev/agents/publicAgent', }; export const urls = { From 5bb2df1ce5db5a07c54e60d244f09158d730aaea Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Wed, 19 Oct 2022 10:43:38 +0200 Subject: [PATCH 13/16] Add extra icons and fix spec --- data-browser/src/components/ClassDetail.tsx | 8 +++++-- data-browser/src/views/FolderPage/iconMap.ts | 10 ++++++++ data-browser/tests/e2e.spec.ts | 25 ++++++++------------ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/data-browser/src/components/ClassDetail.tsx b/data-browser/src/components/ClassDetail.tsx index 452767d61..bf4a29f66 100644 --- a/data-browser/src/components/ClassDetail.tsx +++ b/data-browser/src/components/ClassDetail.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { properties, Resource, useString } from '@tomic/react'; import { ResourceInline } from '../views/ResourceInline'; import { Detail } from './Detail'; +import { getIconForClass } from '../views/FolderPage/iconMap'; type Props = { resource: Resource; @@ -15,8 +16,11 @@ export function ClassDetail({ resource }: Props): JSX.Element { {klass && ( - {'is a '} - + <> + {'is a '} + {getIconForClass(klass)} + + )} diff --git a/data-browser/src/views/FolderPage/iconMap.ts b/data-browser/src/views/FolderPage/iconMap.ts index 9f78d3136..29c58693a 100644 --- a/data-browser/src/views/FolderPage/iconMap.ts +++ b/data-browser/src/views/FolderPage/iconMap.ts @@ -5,10 +5,15 @@ import { FaBook, FaClock, FaComment, + FaCube, + FaCubes, FaFile, FaFileAlt, + FaFileImport, FaFolder, FaHdd, + FaListAlt, + FaShareSquare, } from 'react-icons/fa'; const iconMap = new Map([ @@ -19,6 +24,11 @@ const iconMap = new Map([ [classes.file, FaFile], [classes.drive, FaHdd], [classes.commit, FaClock], + [classes.importer, FaFileImport], + [classes.invite, FaShareSquare], + [classes.collection, FaListAlt], + [classes.class, FaCube], + [classes.property, FaCubes], ]); export function getIconForClass( diff --git a/data-browser/tests/e2e.spec.ts b/data-browser/tests/e2e.spec.ts index 98d83b4ed..42fc9f1a3 100644 --- a/data-browser/tests/e2e.spec.ts +++ b/data-browser/tests/e2e.spec.ts @@ -399,25 +399,18 @@ test.describe('data-browser', async () => { // Create a new folder await newResource('folder', page); - - // Fetch `example.com - const input = page.locator('[placeholder="New Folder"]'); - await input.click(); - await input.fill('RAM Downloads'); - await page.locator(currentDialogOkButton).click(); - - await expect(page.locator('h1:text("Ram Downloads")')).toBeVisible(); - + // Createa sub-resource await page.click('text=New Resource'); await page.click('button:has-text("Document")'); await page.locator(editableTitle).click(); await page.keyboard.type('RAM Downloading Strategies'); await page.keyboard.press('Enter'); - await page.click('[data-test="sidebar"] >> text=RAM Downloads'); + await page.click('[data-test="sidebar"] >> text=Untitled folder'); await expect( page.locator( '[data-test="folder-list"] >> text=RAM Downloading Strategies', ), + 'Created document not visible', ).toBeVisible(); }); @@ -430,8 +423,9 @@ test.describe('data-browser', async () => { .getAttribute('aria-controls'); await page.click(sideBarDriveSwitcher); - await page.click(`[id="${dropdownId}"] >> text=Atomic Data`); - await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); + // temp disable for trailing slash + // await page.click(`[id="${dropdownId}"] >> text=Atomic Data`); + // await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); // Cleanup drives for signed in user await page.click('text=user settings'); @@ -443,10 +437,11 @@ test.describe('data-browser', async () => { test('configure drive page', async ({ page }) => { await signIn(page); await openDriveMenu(page); - await expect(page.locator(currentDriveTitle)).toHaveText('localhost'); + await expect(page.locator(currentDriveTitle)).toHaveText('Main drive'); - await page.click(':text("https://atomicdata.dev") + button:text("Select")'); - await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); + // temp disable this, because of trailing slash in base URL + // await page.click(':text("https://atomicdata.dev") + button:text("Select")'); + // await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); await openDriveMenu(page); await page.fill('[data-test="server-url-input"]', 'https://example.com'); From 5d4f21abebb069456c58eb40e0d2fd8e9b22d2b5 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:36:03 +0100 Subject: [PATCH 14/16] Fix remove cookie auth --- lib/src/authentication.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/authentication.ts b/lib/src/authentication.ts index b72befaab..1b8e082f6 100644 --- a/lib/src/authentication.ts +++ b/lib/src/authentication.ts @@ -114,3 +114,7 @@ export const checkAuthenticationCookie = (): boolean => { return matches.length > 0; }; + +export const removeCookieAuthentication = () => { + document.cookie = `${COOKIE_NAME_AUTH}=;Max-Age=-99999999`; +}; From 15cf8cfec6d62852b8efc2887f3c31d6da6dc91e Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 13:51:26 +0100 Subject: [PATCH 15/16] Importer uses Post --- data-browser/src/routes/AboutRoute.tsx | 4 +- lib/src/client.ts | 51 ++++++++++++++++++-------- lib/src/index.ts | 1 + lib/src/store.ts | 8 ++++ react/src/useImporter.ts | 20 ++++++++-- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/data-browser/src/routes/AboutRoute.tsx b/data-browser/src/routes/AboutRoute.tsx index 830646e1e..30f4c4a5b 100644 --- a/data-browser/src/routes/AboutRoute.tsx +++ b/data-browser/src/routes/AboutRoute.tsx @@ -43,7 +43,7 @@ export const About: React.FunctionComponent = () => {

The back-end of this app is{' '} - + atomic-server , which you can think of as an open source, web-native database. @@ -63,7 +63,7 @@ export const About: React.FunctionComponent = () => {

Run your own server

The easiest way to run an{' '} - + atomic-server {' '} is by using Docker: diff --git a/lib/src/client.ts b/lib/src/client.ts index e0351a63d..27ffaa7d8 100644 --- a/lib/src/client.ts +++ b/lib/src/client.ts @@ -41,13 +41,18 @@ interface FetchResourceOptions { * fetch through that server. */ from?: string; + method?: 'GET' | 'POST'; + /** The body is only used combined with the `POST` method */ + body?: ArrayBuffer | string; } -interface HTTPResult { +/** Contains one or more Resources */ +interface HTTPResourceResult { resource: Resource; createdResources: Resource[]; } +/** Contains a `fetch` instance, provides methods to GET and POST several types */ export class Client { private __fetchOverride?: typeof fetch; @@ -110,8 +115,8 @@ export class Client { public async fetchResourceHTTP( subject: string, opts: FetchResourceOptions = {}, - ): Promise { - const { signInfo, from } = opts; + ): Promise { + const { signInfo, from, body: bodyReq } = opts; let createdResources: Resource[] = []; const parser = new JSONADParser(); let resource = new Resource(subject); @@ -143,6 +148,8 @@ export class Client { const response = await this.fetch(url, { headers: requestHeaders, + method: bodyReq ? 'POST' : 'GET', + body: bodyReq, }); const body = await response.text(); @@ -256,16 +263,30 @@ export class Client { return resources; } - /** Instructs an Atomic Server to fetch a URL and get its JSON-AD */ - public async importJsonAdUrl( - /** The URL of the JSON-AD to import */ - jsonAdUrl: string, - /** Importer URL. Servers tend to have one at `example.com/import` */ - importerUrl: string, - ): Promise { - const url = new URL(importerUrl); - url.searchParams.set('url', jsonAdUrl); - - return this.fetchResourceHTTP(url.toString()); - } + // /** Instructs an Atomic Server to fetch a URL and get its JSON-AD */ + // public async importJsonAdUrl( + // /** The URL of the JSON-AD to import */ + // jsonAdUrl: string, + // /** Importer URL. Servers tend to have one at `example.com/import` */ + // importerUrl: string, + // ): Promise { + // const url = new URL(importerUrl); + // url.searchParams.set('url', jsonAdUrl); + + // return this.fetchResourceHTTP(url.toString()); + // } + + // /** Instructs an Atomic Server to fetch a URL and get its JSON-AD */ + // public async importJsonAdString( + // /** The JSON-AD to import */ + // jsonAdString: string, + // /** Importer URL. Servers tend to have one at `example.com/import` */ + // importerUrl: string, + // ): Promise { + // const url = new URL(importerUrl); + + // return this.fetchResourceHTTP(url.toString(), { + // body: jsonAdString, + // }); + // } } diff --git a/lib/src/index.ts b/lib/src/index.ts index d4daa070c..b80890d16 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -35,6 +35,7 @@ export * from './class.js'; export * from './client.js'; export * from './commit.js'; export * from './error.js'; +export * from './endpoints.js'; export * from './datatypes.js'; export * from './parse.js'; export * from './resource.js'; diff --git a/lib/src/store.ts b/lib/src/store.ts index 82980b344..ce452242b 100644 --- a/lib/src/store.ts +++ b/lib/src/store.ts @@ -189,6 +189,10 @@ export class Store { setLoading?: boolean; /** Do not use WebSockets, use HTTP(S) */ noWebSocket?: boolean; + /** HTTP Method, defaults to GET */ + method?: 'GET' | 'POST'; + /** HTTP Body for POSTing */ + body?: ArrayBuffer | string; } = {}, ): Promise { if (opts.setLoading) { @@ -206,8 +210,10 @@ export class Store { supportsWebSockets() && ws?.readyState === WebSocket.OPEN ) { + // Use WebSocket await fetchWebSocket(ws, subject); } else { + // Use HTTPS const signInfo = this.agent ? { agent: this.agent, serverURL: this.getServerUrl() } : undefined; @@ -216,6 +222,8 @@ export class Store { subject, { from: opts.fromProxy ? this.getServerUrl() : undefined, + method: opts.method, + body: opts.body, signInfo, }, ); diff --git a/react/src/useImporter.ts b/react/src/useImporter.ts index 174bd7172..d4318b97c 100644 --- a/react/src/useImporter.ts +++ b/react/src/useImporter.ts @@ -10,6 +10,7 @@ export function useImporter(importerUrl?: string) { const [url, setUrl] = useState(importerUrl); const [success, setSuccess] = useState(false); const resource = useResource(url); + const store = useStore(); // Get the error from the resource useEffect(() => { @@ -29,10 +30,21 @@ export function useImporter(importerUrl?: string) { setUrl(parsed.toString()); } - function importJsonAd(jsonAdString: string) { - const parsed = new URL(importerUrl!); - parsed.searchParams.set('json', jsonAdString); - setUrl(parsed.toString()); + async function importJsonAd(jsonAdString: string) { + if (!importerUrl) { + throw Error('No importer URL given'); + } + + try { + const resp = await importJsonAdString(store, importerUrl, jsonAdString); + + if (resp.error) { + throw resp.error; + } + } catch (e) { + store.notifyError(e); + setSuccess(false); + } } return { importJsonAd, importURL, resource, success }; From befb9b594f58dd7220d14a902586dddbefbe4e45 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 27 Feb 2023 14:17:51 +0100 Subject: [PATCH 16/16] Fix tests --- data-browser/tests/e2e.spec.ts | 33 +++++++++++++-------------------- lib/src/authentication.ts | 3 +-- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/data-browser/tests/e2e.spec.ts b/data-browser/tests/e2e.spec.ts index 42fc9f1a3..77ed25b99 100644 --- a/data-browser/tests/e2e.spec.ts +++ b/data-browser/tests/e2e.spec.ts @@ -38,6 +38,12 @@ const currentDialogOkButton = 'dialog[open] >> footer >> text=Ok'; // Depends on server index throttle time, `commit_monitor.rs` const REBUILD_INDEX_TIME = 6000; +async function setTitle(page, title: string) { + await page.locator(editableTitle).click(); + await page.fill(editableTitle, title); + await page.waitForTimeout(300); +} + test.describe('data-browser', async () => { test.beforeEach(async ({ page }) => { if (!serverUrl) { @@ -125,8 +131,7 @@ test.describe('data-browser', async () => { // Create folder called 'Not This folder' await page.locator('[data-test="sidebar-new-resource"]').click(); await page.locator('button:has-text("folder")').click(); - await page.locator('[placeholder="New Folder"]').fill('Not This Folder'); - await page.locator(currentDialogOkButton).click(); + await setTitle(page, 'Not This Folder'); // Create document called 'Avocado Salad' await page.locator('button:has-text("New Resource")').click(); @@ -140,8 +145,7 @@ test.describe('data-browser', async () => { // Create folder called 'This folder' await page.locator('button:has-text("folder")').click(); - await page.locator('[placeholder="New Folder"]').fill('This Folder'); - await page.locator(currentDialogOkButton).click(); + await setTitle(page, 'This Folder'); // Create document called 'Avocado Salad' await page.locator('button:has-text("New Resource")').click(); @@ -437,7 +441,7 @@ test.describe('data-browser', async () => { test('configure drive page', async ({ page }) => { await signIn(page); await openDriveMenu(page); - await expect(page.locator(currentDriveTitle)).toHaveText('Main drive'); + await expect(page.locator(currentDriveTitle)).toHaveText('localhost'); // temp disable this, because of trailing slash in base URL // await page.click(':text("https://atomicdata.dev") + button:text("Select")'); @@ -494,31 +498,20 @@ test.describe('data-browser', async () => { await newDrive(page); // create a resource, make sure its visible in the sidebar (and after refresh) - const klass = 'importer'; + const klass = 'folder'; await newResource(klass, page); await expect( - page.locator('[data-test="sidebar"] >> text=importer'), + page.locator(`[data-test="sidebar"] >> text=${klass}`), ).toBeVisible(); - // await page.reload(); - // await expect( - // page.locator('[data-test="sidebar"] >> text=importer'), - // ).toBeVisible(); - - async function setTitle(title: string) { - await page.locator(editableTitle).click(); - await page.fill(editableTitle, title); - await page.waitForTimeout(300); - } - const d0 = 'depth0'; - await setTitle(d0); + await setTitle(page, d0); // Create a subresource, and later check it in the sidebar await page.locator(`[data-test="sidebar"] >> text=${d0}`).hover(); await page.locator(`[title="Create new resource under ${d0}"]`).click(); await page.click(`button:has-text("${klass}")`); const d1 = 'depth1'; - await setTitle(d1); + await setTitle(page, d1); await expect( page.locator(`[data-test="sidebar"] >> text=${d1}`), diff --git a/lib/src/authentication.ts b/lib/src/authentication.ts index 1b8e082f6..ea5193ab4 100644 --- a/lib/src/authentication.ts +++ b/lib/src/authentication.ts @@ -95,8 +95,7 @@ const setCookieExpires = ( const COOKIE_NAME_AUTH = 'atomic_session'; /** Sets a cookie for the current Agent, signing the Authentication. It expires after some default time. */ -export const setCookieAuthentication = (store: Store, agent: Agent) => { - const serverURL = store.getServerUrl(); +export const setCookieAuthentication = (serverURL: string, agent: Agent) => { createAuthentication(serverURL, agent).then(auth => { setCookieExpires(COOKIE_NAME_AUTH, btoa(JSON.stringify(auth)), serverURL); });