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}
-
-
- >
- );
-}
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);
});