From b69687daba00219fc86b9f4e0f316a48288b2c59 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Wed, 20 Dec 2023 16:29:37 +0100 Subject: [PATCH 1/6] Fix server rebuilding js even when ther are no changes --- server/build.rs | 74 ++++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/server/build.rs b/server/build.rs index 58d6e9b33..b244e2a8c 100644 --- a/server/build.rs +++ b/server/build.rs @@ -1,4 +1,8 @@ -use std::{path::PathBuf, time::SystemTime}; +use std::{ + fs::{self, Metadata}, + path::PathBuf, + time::SystemTime, +}; macro_rules! p { ($($tokens: tt)*) => { @@ -27,6 +31,7 @@ fn main() -> std::io::Result<()> { if should_build(&dirs) { build_js(&dirs); + let _ = fs::remove_dir_all(&dirs.js_dist_tmp); dircpy::copy_dir(&dirs.js_dist_source, &dirs.js_dist_tmp)?; } else if dirs.js_dist_tmp.exists() { p!("Found {}, skipping copy", dirs.js_dist_tmp.display()); @@ -61,38 +66,19 @@ fn should_build(dirs: &Dirs) -> bool { if let Ok(tmp_dist_index_html) = std::fs::metadata(format!("{}/index.html", dirs.js_dist_tmp.display())) { - let dist_time = tmp_dist_index_html - .modified() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - for entry in walkdir::WalkDir::new(&dirs.src_browser) + let has_changes = walkdir::WalkDir::new(&dirs.src_browser) .into_iter() - .filter_map(|e| { - // ignore ds store - if let Ok(e) = e { - if e.path().to_str().unwrap().contains(".DS_Store") { - return None; - } - Some(e) - } else { - None - } + .filter_entry(|entry| { + entry + .file_name() + .to_str() + .map(|s| !s.starts_with(".DS_Store")) + .unwrap_or(false) }) - { - if entry.path().is_file() { - let src_time = entry - .metadata() - .unwrap() - .modified() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - if src_time >= dist_time { - p!("Source file modified: {:?}, rebuilding...", entry.path()); - return true; - } - } + .any(|entry| is_older_than(&entry.unwrap(), &tmp_dist_index_html)); + + if has_changes { + return true; } p!("No changes in JS source files, skipping JS build."); @@ -145,3 +131,29 @@ fn build_js(dirs: &Dirs) { ); } } + +fn is_older_than(dir_entry: &walkdir::DirEntry, dist_meta: &Metadata) -> bool { + let dist_time = dist_meta + .modified() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + + if dir_entry.path().is_file() { + let src_time = dir_entry + .metadata() + .unwrap() + .modified() + .unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + if src_time >= dist_time { + p!( + "Source file modified: {:?}, rebuilding...", + dir_entry.path() + ); + return true; + } + } + false +} From 22e539924429c636be4e0e58103d9f933db41bea Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Fri, 22 Dec 2023 17:39:27 +0100 Subject: [PATCH 2/6] WIP #747 New Resource Refactor --- browser/data-browser/src/App.tsx | 11 +- .../IconButton/IconButton.story.mdx | 55 ------- .../src/components/IconButton/IconButton.tsx | 4 +- .../NewInstanceButton/NewBookmarkButton.tsx | 17 +- .../NewInstanceButton/NewOntologyButton.tsx | 113 ++----------- .../NewInstanceButton/NewTableButton.tsx | 113 ++----------- .../NewInstanceButton/useCreateAndNavigate.ts | 32 ++-- .../useDefaultNewInstanceHandler.tsx | 62 +++---- .../NewForm/CustomForms/NewBookmarkDialog.tsx | 87 ++++++++++ .../NewForm/CustomForms/NewOntologyDialog.tsx | 105 ++++++++++++ .../NewForm/CustomForms/NewTableDialog.tsx | 130 +++++++++++++++ .../components/forms/NewForm/NewFormPage.tsx | 5 +- .../components/forms/NewForm/NewFormTitle.tsx | 38 +++-- .../forms/NewForm/useNewResourceUI.tsx | 70 ++++++++ .../components/forms/SearchBox/SearchBox.tsx | 8 +- .../forms/SearchBox/SearchBoxWindow.tsx | 50 ++++-- .../src/routes/NewResource/BaseButtons.tsx | 26 +++ .../src/routes/NewResource/ButtonSection.tsx | 45 ++++++ .../src/routes/NewResource/ClassButton.tsx | 27 ++++ .../src/routes/NewResource/NewRoute.tsx | 100 ++++++++++++ .../routes/NewResource/OntologySections.tsx | 54 +++++++ browser/data-browser/src/routes/NewRoute.tsx | 153 ------------------ browser/data-browser/src/routes/Routes.tsx | 2 +- .../src/views/Article/ArticlePage.tsx | 20 +-- browser/react/src/useServerSearch.tsx | 3 +- 25 files changed, 818 insertions(+), 512 deletions(-) delete mode 100644 browser/data-browser/src/components/IconButton/IconButton.story.mdx create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx create mode 100644 browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx create mode 100644 browser/data-browser/src/routes/NewResource/BaseButtons.tsx create mode 100644 browser/data-browser/src/routes/NewResource/ButtonSection.tsx create mode 100644 browser/data-browser/src/routes/NewResource/ClassButton.tsx create mode 100644 browser/data-browser/src/routes/NewResource/NewRoute.tsx create mode 100644 browser/data-browser/src/routes/NewResource/OntologySections.tsx delete mode 100644 browser/data-browser/src/routes/NewRoute.tsx diff --git a/browser/data-browser/src/App.tsx b/browser/data-browser/src/App.tsx index fb97b5cd2..f6b09701f 100644 --- a/browser/data-browser/src/App.tsx +++ b/browser/data-browser/src/App.tsx @@ -69,6 +69,7 @@ if (isDev()) { } import isPropValid from '@emotion/is-prop-valid'; +import { NewResourceUIProvider } from './components/forms/NewForm/useNewResourceUI'; // This implements the default behavior from styled-components v5 function shouldForwardProp(propName, target) { @@ -107,10 +108,12 @@ function App(): JSX.Element { - - - - + + + + + + diff --git a/browser/data-browser/src/components/IconButton/IconButton.story.mdx b/browser/data-browser/src/components/IconButton/IconButton.story.mdx deleted file mode 100644 index f935a27b4..000000000 --- a/browser/data-browser/src/components/IconButton/IconButton.story.mdx +++ /dev/null @@ -1,55 +0,0 @@ -import { IconButton, IconButtonVariant } from './index'; -import { Row } from '../Row'; -import { FaPoo } from 'react-icons/fa'; - -# IconButton - -## Demo - -### Default - - - - - -### Color - - - - - -### Size - - - - - -### HTML Button attributes - -IconButton accepts all html button attributes - - - - - -### Variants - -IconButton has the following variants: - -```ts -IconButtonVariant.Simple; -IconButtonVariant.Outline; -IconButtonVariant.Fill; -``` - - - - - - - - - - - - diff --git a/browser/data-browser/src/components/IconButton/IconButton.tsx b/browser/data-browser/src/components/IconButton/IconButton.tsx index ec7f6e4bf..6751c93b4 100644 --- a/browser/data-browser/src/components/IconButton/IconButton.tsx +++ b/browser/data-browser/src/components/IconButton/IconButton.tsx @@ -89,7 +89,7 @@ const IconButtonBase = styled.button` color: ${p => p.theme.colors.text}; font-size: ${p => p.size ?? '1em'}; border: none; - + user-select: none; padding: var(--button-padding); width: calc(${p => p.size} + var(--button-padding) * 2); height: calc(${p => p.size} + var(--button-padding) * 2); @@ -115,7 +115,7 @@ const SimpleIconButton = styled(IconButtonBase)` background-color: ${p => p.theme.colors.bg1}; } - :active { + &:active { background-color: ${p => p.theme.colors.bg2}; } } diff --git a/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx index b44d3e12e..c40293c7c 100644 --- a/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx +++ b/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx @@ -1,4 +1,4 @@ -import { classes, properties, useResource, useTitle } from '@tomic/react'; +import { dataBrowser, properties, useResource, useTitle } from '@tomic/react'; import { FormEvent, useCallback, useState } from 'react'; import { Button } from '../Button'; import { @@ -38,7 +38,7 @@ export function NewBookmarkButton({ const [dialogProps, show, hide] = useDialog(); - const createResourceAndNavigate = useCreateAndNavigate(klass, parent); + const createResourceAndNavigate = useCreateAndNavigate(); const onDone = useCallback( (e: FormEvent) => { @@ -46,11 +46,14 @@ export function NewBookmarkButton({ const normalizedUrl = normalizeWebAddress(url); - createResourceAndNavigate('bookmark', { - [properties.name]: 'New Bookmark', - [properties.bookmark.url]: normalizedUrl, - [properties.isA]: [classes.bookmark], - }); + createResourceAndNavigate( + dataBrowser.classes.bookmark, + { + [properties.name]: 'New Bookmark', + [properties.bookmark.url]: normalizedUrl, + }, + parent, + ); }, [url], ); diff --git a/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx index fdacc35e8..3cffdf9ab 100644 --- a/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx +++ b/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx @@ -1,20 +1,8 @@ -import { - Datatype, - classes, - properties, - useResource, - validateDatatype, -} from '@tomic/react'; -import { FormEvent, useCallback, useState } from 'react'; -import { Button } from '../Button'; -import { Dialog, DialogActions, DialogContent, useDialog } from '../Dialog'; -import Field from '../forms/Field'; -import { InputStyled, InputWrapper } from '../forms/InputStyles'; +import { core, useResource } from '@tomic/react'; import { Base } from './Base'; -import { useCreateAndNavigate } from './useCreateAndNavigate'; import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { stringToSlug } from '../../helpers/stringToSlug'; -import { styled } from 'styled-components'; +import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; +import { useSettings } from '../../helpers/AppSettings'; export function NewOntologyButton({ klass, @@ -26,91 +14,24 @@ export function NewOntologyButton({ label, }: NewInstanceButtonProps): JSX.Element { const ontology = useResource(klass); - const [shortname, setShortname] = useState(''); - const [valid, setValid] = useState(false); + const { drive } = useSettings(); - const createResourceAndNavigate = useCreateAndNavigate(klass, parent); + const showNewResourceUI = useNewResourceUI(); - const onSuccess = useCallback(async () => { - createResourceAndNavigate('ontology', { - [properties.shortname]: shortname, - [properties.isA]: [classes.ontology], - [properties.description]: 'description', - [properties.classes]: [], - [properties.properties]: [], - [properties.instances]: [], - }); - }, [shortname, createResourceAndNavigate]); - - const [dialogProps, show, hide] = useDialog({ onSuccess }); - - const onShortnameChange = (e: React.ChangeEvent) => { - const value = stringToSlug(e.target.value); - setShortname(value); - - try { - validateDatatype(value, Datatype.SLUG); - setValid(true); - } catch (_) { - setValid(false); - } + const show = () => { + showNewResourceUI(core.classes.ontology, parent ?? drive); }; return ( - <> - - {children} - - -

New Ontology

- -
{ - e.preventDefault(); - hide(true); - }} - > - - An ontology is a collection of classes and properties that - together describe a concept. Great for data models. - - - - - - -
-
- - - - -
- + + {children} + ); } - -const H1 = styled.h1` - margin: 0; -`; - -const Explanation = styled.p` - color: ${p => p.theme.colors.textLight}; - max-width: 60ch; -`; diff --git a/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx index 7bd52fb6b..d70b63759 100644 --- a/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx +++ b/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx @@ -1,31 +1,8 @@ -import { - classes, - properties, - useResource, - useStore, - useTitle, -} from '@tomic/react'; -import { 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 { dataBrowser, useResource } from '@tomic/react'; import { Base } from './Base'; -import { useCreateAndNavigate } from './useCreateAndNavigate'; import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { stringToSlug } from '../../helpers/stringToSlug'; -import { styled } from 'styled-components'; -import { BetaBadge } from '../BetaBadge'; - -const instanceOpts = { - newResource: true, -}; +import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; +import { useSettings } from '../../helpers/AppSettings'; export function NewTableButton({ klass, @@ -36,46 +13,19 @@ export function NewTableButton({ children, label, }: NewInstanceButtonProps): JSX.Element { - const store = useStore(); - const resource = useResource(klass); - const [instanceSubject] = useState(() => store.createSubject('class')); - const instanceResource = useResource(instanceSubject, instanceOpts); - - const [title] = useTitle(resource); - const [name, setName] = useState(''); - - const createResourceAndNavigate = useCreateAndNavigate(klass, parent); - - const onCancel = useCallback(() => { - instanceResource.destroy(store); - }, []); + const classResource = useResource(klass); + const { drive } = useSettings(); + const showNewResourceUI = useNewResourceUI(); - const onSuccess = useCallback(async () => { - await instanceResource.set(properties.shortname, stringToSlug(name), store); - await instanceResource.set( - properties.description, - `Represents a row in the ${name} table`, - store, - ); - await instanceResource.set(properties.isA, [classes.class], store); - await instanceResource.set(properties.parent, parent, store); - await instanceResource.set(properties.recommends, [properties.name], store); - await instanceResource.save(store); - - createResourceAndNavigate('table', { - [properties.name]: name, - [properties.classType]: instanceResource.getSubject(), - [properties.isA]: [classes.table], - }); - }, [name, instanceResource]); - - const [dialogProps, show, hide] = useDialog({ onCancel, onSuccess }); + const show = () => { + showNewResourceUI(dataBrowser.classes.table, parent ?? drive); + }; return ( <> {children} - - -

New Table

- -
- -
{ - e.preventDefault(); - hide(true); - }} - > - - - setName(e.target.value)} - /> - - -
-
- - - - -
); } - -const WiderDialogContent = styled(DialogContent)` - width: min(80vw, 20rem); -`; - -const RelativeDialogTitle = styled(DialogTitle)` - display: flex; - align-items: flex-start; - gap: 1ch; -`; diff --git a/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts b/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts index 7ae28c84c..97dd74575 100644 --- a/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts +++ b/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts @@ -1,11 +1,4 @@ -import { - JSONValue, - properties, - Resource, - useResource, - useStore, - useTitle, -} from '@tomic/react'; +import { Core, JSONValue, Resource, core, useStore } from '@tomic/react'; import { useCallback } from 'react'; import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; @@ -20,40 +13,41 @@ import { getNamePartFromProps } from '../../helpers/getNamePartFromProps'; * @param parent The parent resource of the new resource. * @returns A createAndNavigate function. */ -export function useCreateAndNavigate(klass: string, parent?: string) { +export function useCreateAndNavigate() { const store = useStore(); - const classTypeResource = useResource(klass); - const [title] = useTitle(classTypeResource); const navigate = useNavigate(); return useCallback( async ( - className: string, + isA: string, propVals: Record, + parent?: string, /** Query parameters for the resource / endpoint */ extraParams?: Record, - /** Do not set a parent for the new resource. Useful for top-level resources */ - noParent?: boolean, ): Promise => { + const classResource = await store.getResourceAsync(isA); + const namePart = getNamePartFromProps(propVals); const newSubject = await store.buildUniqueSubjectFromParts( - [className, namePart], - noParent ? undefined : parent, + [classResource.props.shortname, namePart], + parent, ); const resource = new Resource(newSubject, true); + await resource.addClasses(store, isA); + await Promise.all([ ...Object.entries(propVals).map(([key, val]) => resource.set(key, val, store), ), - !noParent && resource.set(properties.parent, parent, store), + !!parent && resource.set(core.properties.parent, parent, store), ]); try { await resource.save(store); navigate(constructOpenURL(newSubject, extraParams)); - toast.success(`${title} created`); + toast.success(`${classResource.title} created`); store.notifyResourceManuallyCreated(resource); } catch (e) { store.notifyError(e); @@ -61,6 +55,6 @@ export function useCreateAndNavigate(klass: string, parent?: string) { return resource; }, - [store, classTypeResource, title, navigate, parent], + [store, navigate, parent], ); } diff --git a/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx b/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx index 53e35df09..afc770dd0 100644 --- a/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx +++ b/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx @@ -1,6 +1,8 @@ import { classes, - properties, + core, + dataBrowser, + server, useResource, useStore, useString, @@ -24,39 +26,48 @@ export function useDefaultNewInstanceHandler(klass: string, parent?: string) { const navigate = useNavigate(); const classResource = useResource(klass); - const [shortname] = useString(classResource, properties.shortname); + const [shortname] = useString(classResource, core.properties.shortname); - const createResourceAndNavigate = useCreateAndNavigate(klass, parent); + const createResourceAndNavigate = useCreateAndNavigate(); const onClick = useCallback(async () => { try { switch (klass) { - case classes.chatRoom: { - createResourceAndNavigate('chatRoom', { - [properties.name]: 'Untitled ChatRoom', - [properties.isA]: [classes.chatRoom], - }); + case dataBrowser.classes.chatroom: { + createResourceAndNavigate( + dataBrowser.classes.chatroom, + { + [core.properties.name]: 'Untitled ChatRoom', + }, + parent, + ); break; } - case classes.document: { - createResourceAndNavigate('document', { - [properties.isA]: [classes.document], - [properties.name]: 'Untitled Document', - }); + case dataBrowser.classes.document: { + createResourceAndNavigate( + dataBrowser.classes.document, + { + [core.properties.name]: 'Untitled Document', + }, + parent, + ); break; } - case classes.folder: { - createResourceAndNavigate('folder', { - [properties.isA]: [classes.folder], - [properties.name]: 'Untitled Folder', - [properties.displayStyle]: classes.displayStyles.list, - }); + case dataBrowser.classes.folder: { + createResourceAndNavigate( + dataBrowser.classes.folder, + { + [core.properties.name]: 'Untitled Folder', + [dataBrowser.properties.displayStyle]: classes.displayStyles.list, + }, + parent, + ); break; } - case classes.drive: { + case server.classes.drive: { const agent = store.getAgent(); if (!agent || agent.subject === undefined) { @@ -66,18 +77,15 @@ export function useDefaultNewInstanceHandler(klass: string, parent?: string) { } const newResource = await createResourceAndNavigate( - 'drive', + server.classes.drive, { - [properties.isA]: [classes.drive], - [properties.write]: [agent.subject], - [properties.read]: [agent.subject], + [core.properties.write]: [agent.subject], + [core.properties.read]: [agent.subject], }, - undefined, - true, ); const agentResource = await store.getResourceAsync(agent.subject); - agentResource.pushPropVal(properties.drives, [ + agentResource.pushPropVal(server.properties.drives, [ newResource.getSubject(), ]); agentResource.save(store); diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx new file mode 100644 index 000000000..0c9525073 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx @@ -0,0 +1,87 @@ +import { core, dataBrowser } from '@tomic/react'; +import { useState, useCallback, FormEvent, useEffect, FC } from 'react'; +import { Button } from '../../../Button'; +import { + useDialog, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '../../../Dialog'; +import { useCreateAndNavigate } from '../../../NewInstanceButton'; +import Field from '../../Field'; +import { InputWrapper, InputStyled } from '../../InputStyles'; +import { CustomResourceDialogProps } from '../useNewResourceUI'; + +function normalizeWebAddress(url: string) { + if (/^[http://|https://]/i.test(url)) { + return url; + } + + return `https://${url}`; +} + +export const NewBookmarkDialog: FC = ({ + parent, + onClose, +}) => { + const [url, setUrl] = useState(''); + + const [dialogProps, show, hide] = useDialog({ onCancel: onClose }); + + const createResourceAndNavigate = useCreateAndNavigate(); + + const onDone = useCallback( + (e: FormEvent) => { + e.preventDefault(); + + const normalizedUrl = normalizeWebAddress(url); + + createResourceAndNavigate( + dataBrowser.classes.bookmark, + { + [core.properties.name]: 'New Bookmark', + [dataBrowser.properties.url]: normalizedUrl, + }, + parent, + ); + + onClose(); + }, + [url, onClose], + ); + + useEffect(() => { + show(); + }, []); + + return ( + + +

New Bookmark

+
+ +
+ + + setUrl(e.target.value)} + /> + + +
+
+ + + + +
+ ); +}; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx new file mode 100644 index 000000000..d771eb3d8 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx @@ -0,0 +1,105 @@ +import { validateDatatype, Datatype, core } from '@tomic/react'; +import { useState, useCallback, FormEvent, FC, useEffect } from 'react'; +import styled from 'styled-components'; +import { stringToSlug } from '../../../../helpers/stringToSlug'; +import { Button } from '../../../Button'; +import { + useDialog, + Dialog, + DialogContent, + DialogActions, +} from '../../../Dialog'; +import { useCreateAndNavigate } from '../../../NewInstanceButton'; +import Field from '../../Field'; +import { InputWrapper, InputStyled } from '../../InputStyles'; +import { CustomResourceDialogProps } from '../useNewResourceUI'; + +export const NewOntologyDialog: FC = ({ + parent, + onClose, +}) => { + const [shortname, setShortname] = useState(''); + const [valid, setValid] = useState(false); + + const createResourceAndNavigate = useCreateAndNavigate(); + + const onSuccess = useCallback(async () => { + createResourceAndNavigate( + core.classes.ontology, + { + [core.properties.shortname]: shortname, + [core.properties.description]: 'description', + [core.properties.classes]: [], + [core.properties.properties]: [], + [core.properties.instances]: [], + }, + parent, + ); + + onClose(); + }, [shortname, createResourceAndNavigate, onClose, parent]); + + const [dialogProps, show, hide] = useDialog({ onSuccess, onCancel: onClose }); + + const onShortnameChange = (e: React.ChangeEvent) => { + const value = stringToSlug(e.target.value); + setShortname(value); + + try { + validateDatatype(value, Datatype.SLUG); + setValid(true); + } catch (_) { + setValid(false); + } + }; + + useEffect(() => { + show(); + }, []); + + return ( + +

New Ontology

+ +
{ + e.preventDefault(); + hide(true); + }} + > + + An ontology is a collection of classes and properties that together + describe a concept. Great for data models. + + + + + + +
+
+ + + + +
+ ); +}; + +const H1 = styled.h1` + margin: 0; +`; + +const Explanation = styled.p` + color: ${p => p.theme.colors.textLight}; + max-width: 60ch; +`; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx new file mode 100644 index 000000000..678c42382 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx @@ -0,0 +1,130 @@ +import { useResource, Core, dataBrowser, core, useStore } from '@tomic/react'; +import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; +import styled from 'styled-components'; +import { stringToSlug } from '../../../../helpers/stringToSlug'; +import { BetaBadge } from '../../../BetaBadge'; +import { Button } from '../../../Button'; +import { + useDialog, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '../../../Dialog'; +import { useCreateAndNavigate } from '../../../NewInstanceButton'; +import Field from '../../Field'; +import { InputWrapper, InputStyled } from '../../InputStyles'; +import type { CustomResourceDialogProps } from '../useNewResourceUI'; + +const instanceOpts = { + newResource: true, +}; + +export const NewTableDialog: FC = ({ + parent, + onClose, +}) => { + const store = useStore(); + const [instanceSubject] = useState(() => store.createSubject('class')); + const instanceResource = useResource( + instanceSubject, + instanceOpts, + ); + + const [name, setName] = useState(''); + + const createResourceAndNavigate = useCreateAndNavigate(); + + const onCancel = useCallback(() => { + instanceResource.destroy(store); + onClose(); + }, [onClose, instanceResource, store]); + + const onSuccess = useCallback(async () => { + await instanceResource.set( + core.properties.shortname, + stringToSlug(name), + store, + ); + await instanceResource.set( + core.properties.description, + `Represents a row in the ${name} table`, + store, + ); + await instanceResource.set( + core.properties.isA, + [core.classes.class], + store, + ); + await instanceResource.set(core.properties.parent, parent, store); + await instanceResource.set( + core.properties.recommends, + [core.properties.name], + store, + ); + await instanceResource.save(store); + + createResourceAndNavigate( + dataBrowser.classes.table, + { + [core.properties.name]: name, + [core.properties.classtype]: instanceResource.getSubject(), + }, + parent, + ); + + onClose(); + }, [name, instanceResource, store, onClose, parent]); + + const [dialogProps, show, hide] = useDialog({ onCancel, onSuccess }); + + useEffect(() => { + show(); + }, []); + + return ( + + +

New Table

+ +
+ +
{ + e.preventDefault(); + hide(true); + }} + > + + + setName(e.target.value)} + /> + + +
+
+ + + + +
+ ); +}; + +const WiderDialogContent = styled(DialogContent)` + width: min(80vw, 20rem); +`; + +const RelativeDialogTitle = styled(DialogTitle)` + display: flex; + align-items: flex-start; + gap: 1ch; +`; diff --git a/browser/data-browser/src/components/forms/NewForm/NewFormPage.tsx b/browser/data-browser/src/components/forms/NewForm/NewFormPage.tsx index fbf6c823c..15db7fc0f 100644 --- a/browser/data-browser/src/components/forms/NewForm/NewFormPage.tsx +++ b/browser/data-browser/src/components/forms/NewForm/NewFormPage.tsx @@ -4,6 +4,7 @@ import { ResourceForm } from '../ResourceForm'; import { NewFormTitle } from './NewFormTitle'; import { SubjectField } from './SubjectField'; import { useNewForm } from './useNewForm'; +import { Column } from '../../Row'; export interface NewFormProps { classSubject: string; @@ -28,7 +29,7 @@ export const NewFormFullPage = ({ if (!initialized) return <>Initializing Resource; return ( - <> + - + ); }; diff --git a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx index 4287c1694..d966fce5f 100644 --- a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx +++ b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx @@ -4,6 +4,9 @@ import { FaInfo } from 'react-icons/fa'; import { AtomicLink } from '../../AtomicLink'; import { Button } from '../../Button'; import Markdown from '../../datatypes/Markdown'; +import { Column, Row } from '../../Row'; +import styled from 'styled-components'; +import { IconButton, IconButtonVariant } from '../../IconButton/IconButton'; export enum NewFormTitleVariant { FullPage, @@ -34,27 +37,32 @@ export const NewFormTitle: React.FC = ({ const [klassDescription] = useString(klass, properties.description); const [showDetails, setShowDetails] = useState(false); - const HeadingComp = variantHeaderMapping.get(variant!) ?? 'h2'; + const headingType = variantHeaderMapping.get(variant!) ?? 'h2'; return ( - <> - - new{' '} - {classSubject ? ( - {klassTitle} - ) : ( - 'Resource' - )} - - + + {showDetails && klassDescription && } - + ); }; + +const Heading = styled.h1` + margin: 0; +`; diff --git a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx new file mode 100644 index 000000000..d043d42ad --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx @@ -0,0 +1,70 @@ +import { core, dataBrowser } from '@tomic/react'; +import { + FC, + PropsWithChildren, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; +import { NewTableDialog } from './CustomForms/NewTableDialog'; +import { NewOntologyDialog } from './CustomForms/NewOntologyDialog'; +import { NewBookmarkDialog } from './CustomForms/NewBookmarkDialog'; + +export interface CustomResourceDialogProps { + parent: string; + onClose: () => void; +} + +export function useNewResourceUI() { + const { showNewResourceUI } = useContext(NewResourceUIContext); + + return showNewResourceUI; +} + +const dialogs = new Map>([ + [core.classes.ontology, NewOntologyDialog], + [dataBrowser.classes.table, NewTableDialog], + [dataBrowser.classes.bookmark, NewBookmarkDialog], +]); + +interface NewResourceUIContext { + showNewResourceUI: (classType: string, parent: string) => void; +} + +const NewResourceUIContext = createContext({ + showNewResourceUI: () => undefined, +}); + +export function NewResourceUIProvider({ children }: PropsWithChildren) { + const [Dialog, setDialog] = useState(undefined); + + const showNewResourceUI = useCallback((classType: string, parent: string) => { + if (!dialogs.has(classType)) { + // TODO: Default behaviour + return; + } + + const onClose = () => { + setDialog(undefined); + }; + + const Comp = dialogs.get(classType)!; + setDialog(); + }, []); + + const context = useMemo( + () => ({ + showNewResourceUI, + }), + [showNewResourceUI], + ); + + return ( + + {children} + {Dialog} + + ); +} diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx index 41936222d..ff49eacc7 100644 --- a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx @@ -4,7 +4,7 @@ import { removeCachedSearchResults, useResource, useStore } from '@tomic/react'; import { DropdownPortalContext } from '../../Dropdown/dropdownContext'; import * as RadixPopover from '@radix-ui/react-popover'; import { SearchBoxWindow } from './SearchBoxWindow'; -import { FaTimes } from 'react-icons/fa'; +import { FaSearch, FaTimes } from 'react-icons/fa'; import { ErrorChip } from '../ErrorChip'; import { useValidation } from '../formValidation/useValidation'; @@ -138,7 +138,10 @@ export function SearchBox({ : selectedResource.title} ) : ( - {placeholderText} + <> + + {placeholderText} + )} {value && ( @@ -186,6 +189,7 @@ const TriggerButton = styled.button<{ $empty: boolean }>` border: none; text-align: start; height: 2rem; + gap: 0.5rem; width: 100%; overflow: hidden; cursor: text; diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx index 4b4e1ca2c..0f710a1ee 100644 --- a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx @@ -8,6 +8,7 @@ import { useRef, useState, } from 'react'; +import { FaSearch } from 'react-icons/fa'; import { styled, css } from 'styled-components'; import { ResourceResultLine, ResultLine } from './ResultLine'; import { fadeIn } from '../../../helpers/commonAnimations'; @@ -173,16 +174,19 @@ export function SearchBoxWindow({ return ( - ) => - onChange(e.target.value) - } - onKeyDown={handleKeyDown} - onPaste={handlePaste} - /> + + + ) => + onChange(e.target.value) + } + onKeyDown={handleKeyDown} + onPaste={handlePaste} + /> + {!searchValue && Start Searching} @@ -215,18 +219,34 @@ export function SearchBoxWindow({ ); } -const Input = styled.input` +const SearchInputWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; border: solid 1px ${p => p.theme.colors.bg2}; - padding: 0.5rem; height: var(--radix-popover-trigger-height); + padding-inline-start: 0.5rem; width: 100%; - &:focus-visible { + & svg { + color: ${p => p.theme.colors.textLight}; + } + &:focus-within { border-color: ${p => p.theme.colors.main}; outline: none; } `; +const Input = styled.input` + padding: 0.5rem; + height: 100%; + flex: 1; + border: none; + &:focus-visible { + outline: none; + } +`; + const ResultBox = styled.div` flex: 1; border: solid 1px ${p => p.theme.colors.bg2}; @@ -251,7 +271,7 @@ const Wrapper = styled.div<{ $above: boolean }>` bottom: 0; flex-direction: column-reverse; - ${Input} { + ${SearchInputWrapper}, ${Input} { border-bottom-left-radius: ${theme.radius}; border-bottom-right-radius: ${theme.radius}; } @@ -266,7 +286,7 @@ const Wrapper = styled.div<{ $above: boolean }>` top: calc(var(--radix-popover-trigger-height) * -1); flex-direction: column; - ${Input} { + ${SearchInputWrapper}, ${Input} { border-top-left-radius: ${theme.radius}; border-top-right-radius: ${theme.radius}; } diff --git a/browser/data-browser/src/routes/NewResource/BaseButtons.tsx b/browser/data-browser/src/routes/NewResource/BaseButtons.tsx new file mode 100644 index 000000000..9d6c90ff2 --- /dev/null +++ b/browser/data-browser/src/routes/NewResource/BaseButtons.tsx @@ -0,0 +1,26 @@ +import { core, dataBrowser } from '@tomic/react'; +import { ButtonSection } from './ButtonSection'; +import { ClassButton } from './ClassButton'; + +interface BaseButtonsProps { + parent: string; +} + +const buttons = [ + dataBrowser.classes.table, + dataBrowser.classes.folder, + dataBrowser.classes.document, + dataBrowser.classes.chatroom, + dataBrowser.classes.bookmark, + core.classes.ontology, +]; + +export function BaseButtons({ parent }: BaseButtonsProps): JSX.Element { + return ( + + {buttons.map(classType => ( + + ))} + + ); +} diff --git a/browser/data-browser/src/routes/NewResource/ButtonSection.tsx b/browser/data-browser/src/routes/NewResource/ButtonSection.tsx new file mode 100644 index 000000000..9a0a92a91 --- /dev/null +++ b/browser/data-browser/src/routes/NewResource/ButtonSection.tsx @@ -0,0 +1,45 @@ +import { PropsWithChildren } from 'react'; +import { Row } from '../../components/Row'; +import styled from 'styled-components'; + +interface ButtonSectionProps { + title: string; +} + +export function ButtonSection({ + title, + children, +}: PropsWithChildren): JSX.Element { + return ( + <> + {title} + {children} + + ); +} + +const Heading = styled.h2` + display: flex; + align-items: center; + font-size: 1rem; + gap: 1ch; + width: 100%; + color: ${({ theme }) => theme.colors.textLight}; + font-weight: normal; + margin: 0; + font-family: ${({ theme }) => theme.fontFamily}; + + /* &::before, + &::after { + content: ''; + border-bottom: 1px solid ${({ theme }) => theme.colors.bg2}; + flex: 1; + } */ + + /* &::before { + width: 1rem; + } + + &::after { + } */ +`; diff --git a/browser/data-browser/src/routes/NewResource/ClassButton.tsx b/browser/data-browser/src/routes/NewResource/ClassButton.tsx new file mode 100644 index 000000000..c400399a5 --- /dev/null +++ b/browser/data-browser/src/routes/NewResource/ClassButton.tsx @@ -0,0 +1,27 @@ +import { useResource, useTitle } from '@tomic/react'; +import { getIconForClass } from '../../views/FolderPage/iconMap'; +import NewIntanceButton from '../../components/NewInstanceButton'; + +interface ClassButtonProps { + classType: string; + parent: string; +} + +export function ClassButton({ + classType, + parent, +}: ClassButtonProps): JSX.Element { + const classResource = useResource(classType); + const [label] = useTitle(classResource); + + return ( + + ); +} diff --git a/browser/data-browser/src/routes/NewResource/NewRoute.tsx b/browser/data-browser/src/routes/NewResource/NewRoute.tsx new file mode 100644 index 000000000..248139a54 --- /dev/null +++ b/browser/data-browser/src/routes/NewResource/NewRoute.tsx @@ -0,0 +1,100 @@ +import { useResource, urls } from '@tomic/react'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; + +import { + constructOpenURL, + newURL, + useQueryString, +} from '../../helpers/navigation'; +import { ContainerNarrow } from '../../components/Containers'; +import { ResourceSelector } from '../../components/forms/ResourceSelector'; +import { useSettings } from '../../helpers/AppSettings'; +import { ResourceInline } from '../../views/ResourceInline'; +import { styled } from 'styled-components'; +import { FileDropzoneInput } from '../../components/forms/FileDropzone/FileDropzoneInput'; +import toast from 'react-hot-toast'; +import { NewFormFullPage } from '../../components/forms/NewForm/NewFormPage'; +import { Main } from '../../components/Main'; +import { BaseButtons } from './BaseButtons'; +import { OntologySections } from './OntologySections'; + +/** Start page for instantiating a new Resource from some Class */ +function NewRoute(): JSX.Element { + const [classSubject] = useQueryString('classSubject'); + + return ( + + {classSubject ? ( + + ) : ( + + )} + + ); +} + +function NewResourceSelector() { + const [parentSubject] = useQueryString('parentSubject'); + const { drive } = useSettings(); + const calculatedParent = parentSubject || drive; + const parentResource = useResource(calculatedParent); + + const navigate = useNavigate(); + + function handleClassSet(subject: string | undefined) { + if (!subject) { + return; + } + + navigate(newURL(subject, calculatedParent)); + } + + const onUploadComplete = useCallback( + (files: string[]) => { + toast.success(`Uploaded ${files.length} files.`); + + if (parentSubject) { + navigate(constructOpenURL(parentSubject)); + } + }, + [parentSubject, navigate], + ); + + return ( +
+ +

+ Create new resource{' '} + {parentSubject && ( + <> + {`under `} + + + )} +

+
+ +
+ + + +
+
+ ); +} + +const StyledForm = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.margin * 2}rem; +`; + +export default NewRoute; diff --git a/browser/data-browser/src/routes/NewResource/OntologySections.tsx b/browser/data-browser/src/routes/NewResource/OntologySections.tsx new file mode 100644 index 000000000..2b538b0f1 --- /dev/null +++ b/browser/data-browser/src/routes/NewResource/OntologySections.tsx @@ -0,0 +1,54 @@ +import { ButtonSection } from './ButtonSection'; +import { Core, core, useResource, useServerSearch } from '@tomic/react'; +import { ClassButton } from './ClassButton'; +import { FC } from 'react'; +import { useSettings } from '../../helpers/AppSettings'; + +interface OntologySectionsProps { + parent: string; +} + +export function OntologySections({ + parent, +}: OntologySectionsProps): JSX.Element { + const { drive } = useSettings(); + + const { results } = useServerSearch('', { + filters: { + [core.properties.isA]: core.classes.ontology, + }, + parents: [drive], + allowEmptyQuery: true, + limit: 100, + }); + + return ( + <> + {results.map(subject => ( + + ))} + + ); +} + +interface OntologySectionProps { + subject: string; + parent: string; +} + +const OntologySection: FC = ({ subject, parent }) => { + const ontology = useResource(subject); + const classes = ontology.props.classes ?? []; + + if (classes.length === 0) { + return null; + } + + return ( + + {classes.map(classType => ( + + ))} + + ); +}; diff --git a/browser/data-browser/src/routes/NewRoute.tsx b/browser/data-browser/src/routes/NewRoute.tsx deleted file mode 100644 index 07ebe0874..000000000 --- a/browser/data-browser/src/routes/NewRoute.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useResource, useString, useTitle, urls } from '@tomic/react'; -import { useCallback, useState } from 'react'; -import { useNavigate } from 'react-router'; - -import { - constructOpenURL, - newURL, - useQueryString, -} from '../helpers/navigation'; -import { ContainerNarrow } from '../components/Containers'; -import NewIntanceButton from '../components/NewInstanceButton'; -import { ResourceSelector } from '../components/forms/ResourceSelector'; -import { Button } from '../components/Button'; -import { useSettings } from '../helpers/AppSettings'; -import { Row } from '../components/Row'; -import { ResourceInline } from '../views/ResourceInline'; -import { styled } from 'styled-components'; -import { FileDropzoneInput } from '../components/forms/FileDropzone/FileDropzoneInput'; -import toast from 'react-hot-toast'; -import { getIconForClass } from '../views/FolderPage/iconMap'; -import { NewFormFullPage } from '../components/forms/NewForm/NewFormPage'; -import { Main } from '../components/Main'; - -/** Start page for instantiating a new Resource from some Class */ -function NewRoute(): JSX.Element { - const [classSubject] = useQueryString('classSubject'); - - return ( - - {classSubject ? ( - - ) : ( - - )} - - ); -} - -function NewResourceSelector() { - const [parentSubject] = useQueryString('parentSubject'); - const { drive } = useSettings(); - const calculatedParent = parentSubject || drive; - const parentResource = useResource(calculatedParent); - const [error, setError] = useState(undefined); - const [classInputValue, setClassInputValue] = useState(); - const classFull = useResource(classInputValue); - const [className] = useString(classFull, urls.properties.shortname); - const navigate = useNavigate(); - - const buttons = [ - urls.classes.table, - urls.classes.folder, - urls.classes.document, - urls.classes.chatRoom, - urls.classes.bookmark, - urls.classes.ontology, - ]; - - function handleClassSet(e) { - if (!classInputValue) { - setError(new Error('Please select a class')); - - return; - } - - e.preventDefault(); - navigate(newURL(classInputValue, calculatedParent)); - } - - const onUploadComplete = useCallback( - (files: string[]) => { - toast.success(`Uploaded ${files.length} files.`); - - if (parentSubject) { - navigate(constructOpenURL(parentSubject)); - } - }, - [parentSubject, navigate], - ); - - return ( -
- -

- Create new resource{' '} - {parentSubject && ( - <> - {`under `} - - - )} -

- - {classInputValue && ( - - )} - {!classInputValue && ( - <> - {buttons.map(classType => ( - - ))} - - )} - -
- -
- -
-
- ); -} - -const StyledForm = styled.form` - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.margin}rem; -`; - -export default NewRoute; - -interface WrappedButtonProps { - classType: string; - parent: string; -} - -function WrappedButton({ classType, parent }: WrappedButtonProps): JSX.Element { - const classResource = useResource(classType); - const [label] = useTitle(classResource); - - return ( - - ); -} diff --git a/browser/data-browser/src/routes/Routes.tsx b/browser/data-browser/src/routes/Routes.tsx index 59fef27c1..69959ed2c 100644 --- a/browser/data-browser/src/routes/Routes.tsx +++ b/browser/data-browser/src/routes/Routes.tsx @@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom'; import Show from './ShowRoute'; import { Search } from './SearchRoute'; -import NewRoute from './NewRoute'; +import NewRoute from './NewResource/NewRoute'; import { SettingsTheme } from './SettingsTheme'; import { Edit } from './EditRoute'; import Data from './DataRoute'; diff --git a/browser/data-browser/src/views/Article/ArticlePage.tsx b/browser/data-browser/src/views/Article/ArticlePage.tsx index dbefaa2e7..b10293b86 100644 --- a/browser/data-browser/src/views/Article/ArticlePage.tsx +++ b/browser/data-browser/src/views/Article/ArticlePage.tsx @@ -26,18 +26,18 @@ export function ArticlePage({ resource }: ResourcePageProps): JSX.Element { const [canEdit] = useCanWrite(resource); const children = useChildren(resource); - const createAndNavigate = useCreateAndNavigate( - classes.article, - resource.getSubject(), - ); + const createAndNavigate = useCreateAndNavigate(); const createNewArticle = useCallback(() => { - createAndNavigate('article', { - [properties.isA]: [classes.article], - [properties.name]: 'New Article', - [properties.publishedAt]: getTimestampNow(), - [properties.description]: '', - }); + createAndNavigate( + classes.article, + { + [properties.name]: 'New Article', + [properties.publishedAt]: getTimestampNow(), + [properties.description]: '', + }, + resource.getSubject(), + ); }, [createAndNavigate]); return ( diff --git a/browser/react/src/useServerSearch.tsx b/browser/react/src/useServerSearch.tsx index dc9161140..8de1156a2 100644 --- a/browser/react/src/useServerSearch.tsx +++ b/browser/react/src/useServerSearch.tsx @@ -26,6 +26,7 @@ interface SearchOptsHook extends SearchOpts { * respresents milliseconds. */ debounce?: number; + allowEmptyQuery?: boolean; } const noResultsResult = { @@ -88,7 +89,7 @@ export function useServerSearch( [results, resource.loading, resource.error], ); - if (!query) { + if (!query && !opts.allowEmptyQuery) { return noResultsResult; } From 8d5d4e0be041796bebda139279a857b0f81a0529 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 8 Jan 2024 16:37:57 +0100 Subject: [PATCH 3/6] #747 New Resource Refactor --- browser/data-browser/src/App.tsx | 4 +- .../NewInstanceButton/NewBookmarkButton.tsx | 102 ----------------- .../NewInstanceButton/NewInstanceButton.tsx | 53 +++++++++ .../NewInstanceButtonDefault.tsx | 35 ------ .../NewInstanceButtonProps.ts | 16 --- .../NewInstanceButton/NewOntologyButton.tsx | 37 ------ .../NewInstanceButton/NewTableButton.tsx | 38 ------- .../src/components/NewInstanceButton/index.ts | 1 + .../components/NewInstanceButton/index.tsx | 32 ------ .../useDefaultNewInstanceHandler.tsx | 107 ------------------ .../src/components/SideBar/DriveSwitcher.tsx | 14 +-- .../BasicInstanceHandlers.ts | 83 ++++++++++++++ .../CustomForms/NewBookmarkDialog.tsx | 12 +- .../CustomForms/NewOntologyDialog.tsx | 16 +-- .../CustomForms/NewTableDialog.tsx | 16 +-- .../CustomCreateActions/CustomForms/index.ts | 11 ++ .../NewForm/CustomCreateActions/index.ts | 7 ++ .../forms/NewForm/useNewResourceUI.tsx | 84 ++++++++++---- .../data-browser/src/helpers/AppSettings.tsx | 2 +- .../useCreateAndNavigate.ts | 15 ++- .../src/routes/NewResource/ClassButton.tsx | 4 +- .../src/routes/NewResource/NewRoute.tsx | 10 +- .../src/routes/SettingsServer/DrivesCard.tsx | 4 +- .../src/views/Article/ArticlePage.tsx | 2 +- browser/data-browser/src/views/ClassPage.tsx | 2 +- .../data-browser/src/views/CollectionPage.tsx | 2 +- 26 files changed, 272 insertions(+), 437 deletions(-) delete mode 100644 browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx create mode 100644 browser/data-browser/src/components/NewInstanceButton/NewInstanceButton.tsx delete mode 100644 browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonDefault.tsx delete mode 100644 browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonProps.ts delete mode 100644 browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx delete mode 100644 browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx create mode 100644 browser/data-browser/src/components/NewInstanceButton/index.ts delete mode 100644 browser/data-browser/src/components/NewInstanceButton/index.tsx delete mode 100644 browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts rename browser/data-browser/src/components/forms/NewForm/{ => CustomCreateActions}/CustomForms/NewBookmarkDialog.tsx (84%) rename browser/data-browser/src/components/forms/NewForm/{ => CustomCreateActions}/CustomForms/NewOntologyDialog.tsx (84%) rename browser/data-browser/src/components/forms/NewForm/{ => CustomCreateActions}/CustomForms/NewTableDialog.tsx (86%) create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomCreateActions/index.ts rename browser/data-browser/src/{components/NewInstanceButton => hooks}/useCreateAndNavigate.ts (81%) diff --git a/browser/data-browser/src/App.tsx b/browser/data-browser/src/App.tsx index f6b09701f..39111acae 100644 --- a/browser/data-browser/src/App.tsx +++ b/browser/data-browser/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; import { StoreContext, Store } from '@tomic/react'; +import { StyleSheetManager } from 'styled-components'; import { GlobalStyle, ThemeWrapper } from './styling'; import { AppRoutes } from './routes/Routes'; @@ -22,7 +23,7 @@ import { PopoverContainer } from './components/Popover'; import { SkipNav } from './components/SkipNav'; import { ControlLockProvider } from './hooks/useControlLock'; import { FormValidationContextProvider } from './components/forms/formValidation/FormValidationContextProvider'; -import { StyleSheetManager } from 'styled-components'; +import { registerCustomCreateActions } from './components/forms/NewForm/CustomCreateActions'; function fixDevUrl(url: string) { if (isDev()) { @@ -60,6 +61,7 @@ const ErrBoundary = window.bugsnagApiKey // Fetch all the Properties and Classes - this helps speed up the app. store.preloadPropsAndClasses(); +registerCustomCreateActions(); // Register global event handlers. registerHandlers(store); diff --git a/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx deleted file mode 100644 index c40293c7c..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/NewBookmarkButton.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { dataBrowser, properties, useResource, useTitle } from '@tomic/react'; -import { 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'; - -function normalizeWebAddress(url: string) { - if (/^[http://|https://]/i.test(url)) { - return url; - } - - return `https://${url}`; -} - -export function NewBookmarkButton({ - klass, - subtle, - icon, - IconComponent, - parent, - children, - label, -}: NewInstanceButtonProps): JSX.Element { - const resource = useResource(klass); - const [title] = useTitle(resource); - - const [url, setUrl] = useState(''); - - const [dialogProps, show, hide] = useDialog(); - - const createResourceAndNavigate = useCreateAndNavigate(); - - const onDone = useCallback( - (e: FormEvent) => { - e.preventDefault(); - - const normalizedUrl = normalizeWebAddress(url); - - createResourceAndNavigate( - dataBrowser.classes.bookmark, - { - [properties.name]: 'New Bookmark', - [properties.bookmark.url]: normalizedUrl, - }, - parent, - ); - }, - [url], - ); - - return ( - <> - - {children} - - - -

New Bookmark

-
- -
- - - setUrl(e.target.value)} - /> - - -
-
- - - - -
- - ); -} diff --git a/browser/data-browser/src/components/NewInstanceButton/NewInstanceButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewInstanceButton.tsx new file mode 100644 index 000000000..9f2249e31 --- /dev/null +++ b/browser/data-browser/src/components/NewInstanceButton/NewInstanceButton.tsx @@ -0,0 +1,53 @@ +import { useResource } from '@tomic/react'; + +import { useSettings } from '../../helpers/AppSettings'; +import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; +import { Base } from './Base'; +import { IconType } from 'react-icons'; + +interface NewInstanceButtonProps { + /** URL of the Class to be instantiated */ + klass: string; + subtle?: boolean; + icon?: boolean; + IconComponent?: IconType; + /** subject of the parent Resource, which will be passed to the form */ + parent?: string; + /** Give explicit label. If missing, uses the Shortname of the Class */ + label?: string; + className?: string; +} + +/** A button for creating a new instance of some thing */ +export function NewInstanceButton({ + klass, + subtle, + icon, + IconComponent, + parent, + children, + label, + className, +}: React.PropsWithChildren): JSX.Element { + const { drive } = useSettings(); + const classResource = useResource(klass); + const showNewResourceUI = useNewResourceUI(); + + const onClick = () => { + showNewResourceUI(klass, parent ?? drive); + }; + + return ( + + {children} + + ); +} diff --git a/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonDefault.tsx b/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonDefault.tsx deleted file mode 100644 index e59e4d334..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonDefault.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useResource, useTitle } from '@tomic/react'; -import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { Base } from './Base'; -import { useDefaultNewInstanceHandler } from './useDefaultNewInstanceHandler'; - -/** Default handler for the new Instance button. DO NOT USE DIRECTLY. */ -export function NewInstanceButtonDefault({ - klass, - subtle, - icon, - IconComponent, - parent, - children, - label, - className, -}: NewInstanceButtonProps): JSX.Element { - const classResource = useResource(klass); - const [title] = useTitle(classResource); - - const onClick = useDefaultNewInstanceHandler(klass, parent); - - return ( - - {children} - - ); -} diff --git a/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonProps.ts b/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonProps.ts deleted file mode 100644 index 36a7f5117..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/NewInstanceButtonProps.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IconType } from 'react-icons'; - -interface Props { - /** URL of the Class to be instantiated */ - klass: string; - subtle?: boolean; - icon?: boolean; - IconComponent?: IconType; - /** subject of the parent Resource, which will be passed to the form */ - parent?: string; - /** Give explicit label. If missing, uses the Shortname of the Class */ - label?: string; - className?: string; -} - -export type NewInstanceButtonProps = React.PropsWithChildren; diff --git a/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx deleted file mode 100644 index 3cffdf9ab..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/NewOntologyButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { core, useResource } from '@tomic/react'; -import { Base } from './Base'; -import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; -import { useSettings } from '../../helpers/AppSettings'; - -export function NewOntologyButton({ - klass, - subtle, - icon, - IconComponent, - parent, - children, - label, -}: NewInstanceButtonProps): JSX.Element { - const ontology = useResource(klass); - const { drive } = useSettings(); - - const showNewResourceUI = useNewResourceUI(); - - const show = () => { - showNewResourceUI(core.classes.ontology, parent ?? drive); - }; - - return ( - - {children} - - ); -} diff --git a/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx b/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx deleted file mode 100644 index d70b63759..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/NewTableButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { dataBrowser, useResource } from '@tomic/react'; -import { Base } from './Base'; -import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; -import { useSettings } from '../../helpers/AppSettings'; - -export function NewTableButton({ - klass, - subtle, - icon, - IconComponent, - parent, - children, - label, -}: NewInstanceButtonProps): JSX.Element { - const classResource = useResource(klass); - const { drive } = useSettings(); - const showNewResourceUI = useNewResourceUI(); - - const show = () => { - showNewResourceUI(dataBrowser.classes.table, parent ?? drive); - }; - - return ( - <> - - {children} - - - ); -} diff --git a/browser/data-browser/src/components/NewInstanceButton/index.ts b/browser/data-browser/src/components/NewInstanceButton/index.ts new file mode 100644 index 000000000..7f43e3c87 --- /dev/null +++ b/browser/data-browser/src/components/NewInstanceButton/index.ts @@ -0,0 +1 @@ +export * from './NewInstanceButton'; diff --git a/browser/data-browser/src/components/NewInstanceButton/index.tsx b/browser/data-browser/src/components/NewInstanceButton/index.tsx deleted file mode 100644 index e31cf77e0..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { classes } from '@tomic/react'; - -import { NewBookmarkButton } from './NewBookmarkButton'; -import { NewInstanceButtonProps } from './NewInstanceButtonProps'; -import { NewInstanceButtonDefault } from './NewInstanceButtonDefault'; -import { useSettings } from '../../helpers/AppSettings'; -import { NewTableButton } from './NewTableButton'; -import { NewOntologyButton } from './NewOntologyButton'; - -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.table, NewTableButton], - [classes.ontology, NewOntologyButton], -]); - -/** A button for creating a new instance of some thing */ -export default function NewInstanceButton( - props: NewInstanceButtonProps, -): JSX.Element { - const { klass, parent } = props; - const { drive } = useSettings(); - - const Comp = classMap.get(klass) ?? NewInstanceButtonDefault; - - return ; -} - -export { useDefaultNewInstanceHandler } from './useDefaultNewInstanceHandler'; -export { useCreateAndNavigate } from './useCreateAndNavigate'; diff --git a/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx b/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx deleted file mode 100644 index afc770dd0..000000000 --- a/browser/data-browser/src/components/NewInstanceButton/useDefaultNewInstanceHandler.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { - classes, - core, - dataBrowser, - server, - useResource, - useStore, - useString, -} from '@tomic/react'; -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -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(); - const navigate = useNavigate(); - - const classResource = useResource(klass); - const [shortname] = useString(classResource, core.properties.shortname); - - const createResourceAndNavigate = useCreateAndNavigate(); - - const onClick = useCallback(async () => { - try { - switch (klass) { - case dataBrowser.classes.chatroom: { - createResourceAndNavigate( - dataBrowser.classes.chatroom, - { - [core.properties.name]: 'Untitled ChatRoom', - }, - parent, - ); - break; - } - - case dataBrowser.classes.document: { - createResourceAndNavigate( - dataBrowser.classes.document, - { - [core.properties.name]: 'Untitled Document', - }, - parent, - ); - break; - } - - case dataBrowser.classes.folder: { - createResourceAndNavigate( - dataBrowser.classes.folder, - { - [core.properties.name]: 'Untitled Folder', - [dataBrowser.properties.displayStyle]: classes.displayStyles.list, - }, - parent, - ); - break; - } - - case server.classes.drive: { - const agent = store.getAgent(); - - if (!agent || agent.subject === undefined) { - throw new Error( - 'No agent set in the Store, required when creating a Drive', - ); - } - - const newResource = await createResourceAndNavigate( - server.classes.drive, - { - [core.properties.write]: [agent.subject], - [core.properties.read]: [agent.subject], - }, - ); - - const agentResource = await store.getResourceAsync(agent.subject); - agentResource.pushPropVal(server.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.notifyError(e); - } - }, [klass, store, parent, createResourceAndNavigate]); - - return onClick; -} diff --git a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx index 8d24e72ce..9a640dd5c 100644 --- a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx +++ b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx @@ -1,4 +1,4 @@ -import { classes, Resource, urls, useResources } from '@tomic/react'; +import { Resource, core, server, useResources } from '@tomic/react'; import { useMemo } from 'react'; import { FaCog, @@ -15,13 +15,13 @@ import { useSavedDrives } from '../../hooks/useSavedDrives'; import { paths } from '../../routes/paths'; import { DIVIDER, DropdownMenu } from '../Dropdown'; import { buildDefaultTrigger } from '../Dropdown/DefaultTrigger'; -import { useDefaultNewInstanceHandler } from '../NewInstanceButton'; +import { useNewResourceUI } from '../forms/NewForm/useNewResourceUI'; const Trigger = buildDefaultTrigger(, 'Open Drive Settings'); function getTitle(resource: Resource): string { return ( - (resource.get(urls.properties.name) as string) ?? resource.getSubject() + (resource.get(core.properties.name) as string) ?? resource.getSubject() ); } @@ -44,10 +44,7 @@ export function DriveSwitcher() { navigate(constructOpenURL(subject)); }; - const createNewDrive = useDefaultNewInstanceHandler( - classes.drive, - agent?.subject, - ); + const createNewResource = useNewResourceUI(); const items = useMemo( () => [ @@ -89,7 +86,8 @@ export function DriveSwitcher() { label: 'New Drive', icon: , helper: 'Create a new drive', - onClick: createNewDrive, + onClick: () => + createNewResource(server.classes.drive, agent?.subject ?? ''), }, ], [savedDrivesMap, drive, historyMap], diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts new file mode 100644 index 000000000..775da1466 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts @@ -0,0 +1,83 @@ +import { dataBrowser, core, classes, server } from '@tomic/react'; +import { registerBasicInstanceHandler } from '../useNewResourceUI'; + +export const registerBasicInstanceHandlers = () => { + registerBasicInstanceHandler( + dataBrowser.classes.folder, + async (parent, createAndNavigate) => { + await createAndNavigate( + dataBrowser.classes.folder, + { + [core.properties.name]: 'Untitled Folder', + [dataBrowser.properties.displayStyle]: classes.displayStyles.list, + }, + parent, + ); + }, + ); + + registerBasicInstanceHandler( + dataBrowser.classes.chatroom, + async (parent, createAndNavigate) => { + await createAndNavigate( + dataBrowser.classes.chatroom, + { + [core.properties.name]: 'Untitled ChatRoom', + }, + parent, + ); + }, + ); + + registerBasicInstanceHandler( + dataBrowser.classes.folder, + async (parent, createAndNavigate) => { + await createAndNavigate( + dataBrowser.classes.folder, + { + [core.properties.name]: 'Untitled Folder', + [dataBrowser.properties.displayStyle]: classes.displayStyles.list, + }, + parent, + ); + }, + ); + + registerBasicInstanceHandler( + dataBrowser.classes.document, + async (parent, createAndNavigate) => { + createAndNavigate( + dataBrowser.classes.document, + { + [core.properties.name]: 'Untitled Document', + }, + parent, + ); + }, + ); + + registerBasicInstanceHandler( + server.classes.drive, + async (_parent, createAndNavigate, { store, settings }) => { + const agent = store.getAgent(); + + if (!agent || agent.subject === undefined) { + throw new Error( + 'No agent set in the Store, required when creating a Drive', + ); + } + + const newResource = await createAndNavigate(server.classes.drive, { + [core.properties.write]: [agent.subject], + [core.properties.read]: [agent.subject], + }); + + const agentResource = await store.getResourceAsync(agent.subject); + agentResource.pushPropVal(server.properties.drives, [ + newResource.getSubject(), + ]); + agentResource.save(store); + settings.setDrive(newResource.getSubject()); + }, + ); +}; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx similarity index 84% rename from browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx rename to browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx index 0c9525073..fe2203f8a 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewBookmarkDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx @@ -1,17 +1,17 @@ import { core, dataBrowser } from '@tomic/react'; import { useState, useCallback, FormEvent, useEffect, FC } from 'react'; -import { Button } from '../../../Button'; +import { Button } from '../../../../Button'; import { useDialog, Dialog, DialogTitle, DialogContent, DialogActions, -} from '../../../Dialog'; -import { useCreateAndNavigate } from '../../../NewInstanceButton'; -import Field from '../../Field'; -import { InputWrapper, InputStyled } from '../../InputStyles'; -import { CustomResourceDialogProps } from '../useNewResourceUI'; +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; function normalizeWebAddress(url: string) { if (/^[http://|https://]/i.test(url)) { diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx similarity index 84% rename from browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx rename to browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx index d771eb3d8..a810813be 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewOntologyDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx @@ -1,18 +1,18 @@ import { validateDatatype, Datatype, core } from '@tomic/react'; import { useState, useCallback, FormEvent, FC, useEffect } from 'react'; -import styled from 'styled-components'; -import { stringToSlug } from '../../../../helpers/stringToSlug'; -import { Button } from '../../../Button'; +import { styled } from 'styled-components'; +import { stringToSlug } from '../../../../../helpers/stringToSlug'; +import { Button } from '../../../../Button'; import { useDialog, Dialog, DialogContent, DialogActions, -} from '../../../Dialog'; -import { useCreateAndNavigate } from '../../../NewInstanceButton'; -import Field from '../../Field'; -import { InputWrapper, InputStyled } from '../../InputStyles'; -import { CustomResourceDialogProps } from '../useNewResourceUI'; +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; export const NewOntologyDialog: FC = ({ parent, diff --git a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx similarity index 86% rename from browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx rename to browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx index 678c42382..f25619896 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomForms/NewTableDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx @@ -1,20 +1,20 @@ import { useResource, Core, dataBrowser, core, useStore } from '@tomic/react'; import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; import styled from 'styled-components'; -import { stringToSlug } from '../../../../helpers/stringToSlug'; -import { BetaBadge } from '../../../BetaBadge'; -import { Button } from '../../../Button'; +import { stringToSlug } from '../../../../../helpers/stringToSlug'; +import { BetaBadge } from '../../../../BetaBadge'; +import { Button } from '../../../../Button'; import { useDialog, Dialog, DialogActions, DialogContent, DialogTitle, -} from '../../../Dialog'; -import { useCreateAndNavigate } from '../../../NewInstanceButton'; -import Field from '../../Field'; -import { InputWrapper, InputStyled } from '../../InputStyles'; -import type { CustomResourceDialogProps } from '../useNewResourceUI'; +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import type { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; const instanceOpts = { newResource: true, diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts new file mode 100644 index 000000000..b4c47412e --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts @@ -0,0 +1,11 @@ +import { dataBrowser, core } from '@tomic/react'; +import { registerNewResourceDialog } from '../../useNewResourceUI'; +import { NewBookmarkDialog } from './NewBookmarkDialog'; +import { NewOntologyDialog } from './NewOntologyDialog'; +import { NewTableDialog } from './NewTableDialog'; + +export const registerCustomForms = () => { + registerNewResourceDialog(dataBrowser.classes.bookmark, NewBookmarkDialog); + registerNewResourceDialog(core.classes.ontology, NewOntologyDialog); + registerNewResourceDialog(dataBrowser.classes.table, NewTableDialog); +}; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/index.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/index.ts new file mode 100644 index 000000000..80d382ed0 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/index.ts @@ -0,0 +1,7 @@ +import { registerBasicInstanceHandlers } from './BasicInstanceHandlers'; +import { registerCustomForms } from './CustomForms'; + +export const registerCustomCreateActions = () => { + registerCustomForms(); + registerBasicInstanceHandlers(); +}; diff --git a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx index d043d42ad..1fca9f89e 100644 --- a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx +++ b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx @@ -1,4 +1,4 @@ -import { core, dataBrowser } from '@tomic/react'; +import { Core, Store, useStore } from '@tomic/react'; import { FC, PropsWithChildren, @@ -8,50 +8,94 @@ import { useMemo, useState, } from 'react'; -import { NewTableDialog } from './CustomForms/NewTableDialog'; -import { NewOntologyDialog } from './CustomForms/NewOntologyDialog'; -import { NewBookmarkDialog } from './CustomForms/NewBookmarkDialog'; +import { + useCreateAndNavigate, + CreateAndNavigate, +} from '../../../hooks/useCreateAndNavigate'; +import { AppSettings, useSettings } from '../../../helpers/AppSettings'; +import { newURL } from '../../../helpers/navigation'; +import { useNavigateWithTransition } from '../../../hooks/useNavigateWithTransition'; export interface CustomResourceDialogProps { parent: string; onClose: () => void; } +export type BasicInstanceHandler = ( + parent: string, + createAndNavigate: CreateAndNavigate, + context: { + store: Store; + settings: AppSettings; + }, +) => Promise; + +interface NewResourceUIContext { + showNewResourceUI: (classType: string, parent: string) => void; +} + +const dialogs = new Map>(); +const basicNewInstanceHandlers = new Map(); + export function useNewResourceUI() { const { showNewResourceUI } = useContext(NewResourceUIContext); return showNewResourceUI; } -const dialogs = new Map>([ - [core.classes.ontology, NewOntologyDialog], - [dataBrowser.classes.table, NewTableDialog], - [dataBrowser.classes.bookmark, NewBookmarkDialog], -]); +export const registerNewResourceDialog = ( + classSubject: string, + component: FC, +) => { + dialogs.set(classSubject, component); +}; -interface NewResourceUIContext { - showNewResourceUI: (classType: string, parent: string) => void; -} +export const registerBasicInstanceHandler = ( + classSubject: string, + handler: BasicInstanceHandler, +) => { + basicNewInstanceHandlers.set(classSubject, handler); +}; const NewResourceUIContext = createContext({ showNewResourceUI: () => undefined, }); export function NewResourceUIProvider({ children }: PropsWithChildren) { + const store = useStore(); + const settings = useSettings(); + const createAndNavigate = useCreateAndNavigate(); const [Dialog, setDialog] = useState(undefined); + const navigate = useNavigateWithTransition(); + + const showNewResourceUI = useCallback(async (isA: string, parent: string) => { + // Show a dialog if one is registered for the given class + if (dialogs.has(isA)) { + const onClose = () => { + setDialog(undefined); + }; + + const Comp = dialogs.get(isA)!; + setDialog(); - const showNewResourceUI = useCallback((classType: string, parent: string) => { - if (!dialogs.has(classType)) { - // TODO: Default behaviour return; } - const onClose = () => { - setDialog(undefined); - }; + // If a basicInstanceHandler is registered for the class, create a resource of the given class with some default values. + if (basicNewInstanceHandlers.has(isA)) { + basicNewInstanceHandlers.get(isA)?.(parent, createAndNavigate, { + store, + settings, + }); + + return; + } - const Comp = dialogs.get(classType)!; - setDialog(); + // Default behaviour. Navigate to a new resource form for the given class. + const classResource = await store.getResourceAsync(isA); + navigate( + newURL(isA, parent, store.createSubject(classResource.props.shortname)), + ); }, []); const context = useMemo( diff --git a/browser/data-browser/src/helpers/AppSettings.tsx b/browser/data-browser/src/helpers/AppSettings.tsx index 45de1d95b..8606e75e3 100644 --- a/browser/data-browser/src/helpers/AppSettings.tsx +++ b/browser/data-browser/src/helpers/AppSettings.tsx @@ -115,7 +115,7 @@ export const AppSettingsContextProvider = ( }; /** A bunch of getters and setters for client-side app settings */ -interface AppSettings { +export interface AppSettings { /** Whether the App should render in dark mode. Checks user preferences. */ darkMode: boolean; /** 'always', 'never' or 'auto' */ diff --git a/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts b/browser/data-browser/src/hooks/useCreateAndNavigate.ts similarity index 81% rename from browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts rename to browser/data-browser/src/hooks/useCreateAndNavigate.ts index 97dd74575..898ddab86 100644 --- a/browser/data-browser/src/components/NewInstanceButton/useCreateAndNavigate.ts +++ b/browser/data-browser/src/hooks/useCreateAndNavigate.ts @@ -2,18 +2,23 @@ import { Core, JSONValue, Resource, core, useStore } from '@tomic/react'; import { useCallback } from 'react'; import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; -import { constructOpenURL } from '../../helpers/navigation'; -import { getNamePartFromProps } from '../../helpers/getNamePartFromProps'; +import { constructOpenURL } from '../helpers/navigation'; +import { getNamePartFromProps } from '../helpers/getNamePartFromProps'; + +export type CreateAndNavigate = ( + isA: string, + propVals: Record, + parent?: string, + extraParams?: Record, +) => Promise; /** * 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. - * @param parent The parent resource of the new resource. * @returns A createAndNavigate function. */ -export function useCreateAndNavigate() { +export function useCreateAndNavigate(): CreateAndNavigate { const store = useStore(); const navigate = useNavigate(); diff --git a/browser/data-browser/src/routes/NewResource/ClassButton.tsx b/browser/data-browser/src/routes/NewResource/ClassButton.tsx index c400399a5..e7baffa72 100644 --- a/browser/data-browser/src/routes/NewResource/ClassButton.tsx +++ b/browser/data-browser/src/routes/NewResource/ClassButton.tsx @@ -1,6 +1,6 @@ import { useResource, useTitle } from '@tomic/react'; import { getIconForClass } from '../../views/FolderPage/iconMap'; -import NewIntanceButton from '../../components/NewInstanceButton'; +import { NewInstanceButton } from '../../components/NewInstanceButton'; interface ClassButtonProps { classType: string; @@ -15,7 +15,7 @@ export function ClassButton({ const [label] = useTitle(classResource); return ( - Date: Mon, 8 Jan 2024 17:14:10 +0100 Subject: [PATCH 4/6] #747 Fix test and add changelog entry --- browser/CHANGELOG.md | 7 ++++ browser/e2e/tests/e2e.spec.ts | 68 +++++++++++++++++---------------- browser/e2e/tests/test-utils.ts | 1 - 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 8c6b40f10..8b48034df 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -2,6 +2,13 @@ This changelog covers all three packages, as they are (for now) updated as a whole +## UNRELEASED + +### Atomic Browser + +- [#747](https://github.com/atomicdata-dev/atomic-server/issues/747) Show ontology classes on new resource page. +- Fix server not rebuilding client when files changed. + ## v0.36.2 ### Atomic Browser diff --git a/browser/e2e/tests/e2e.spec.ts b/browser/e2e/tests/e2e.spec.ts index 6cdb4d8ce..a6796685e 100644 --- a/browser/e2e/tests/e2e.spec.ts +++ b/browser/e2e/tests/e2e.spec.ts @@ -96,39 +96,43 @@ test.describe('data-browser', async () => { await editProfileAndCommit(page); }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars test('collections & data view', async ({ page }) => { - await openAtomic(page); - // collections, pagination, sorting - await openSubject(page, 'https://atomicdata.dev/properties'); - await page.click( - '[data-test="sort-https://atomicdata.dev/properties/description"]', - ); - // These values can change as new Properties are added to atomicdata.dev - const firstPageText = 'text=A base64 serialized JSON object'; - const secondPageText = 'text=include-nested'; - await expect(page.locator(firstPageText)).toBeVisible(); - await page.click('[data-test="next-page"]'); - await expect(page.locator(firstPageText)).not.toBeVisible(); - await expect(page.locator(secondPageText)).toBeVisible(); - - // context menu, keyboard & data view - await page.click(contextMenu); - await page.keyboard.press('Enter'); - await expect(page.locator('text=JSON-AD')).toBeVisible(); - await page.click('[data-test="fetch-json-ad"]'); - await expect( - page.locator( - 'text="https://atomicdata.dev/properties/collection/members": [', - ), - ).toBeVisible(); - await page.click('[data-test="fetch-json"]'); - await expect(page.locator('text= "members": [')).toBeVisible(); - await page.click('[data-test="fetch-json-ld"]'); - await expect(page.locator('text="current-page": {')).toBeVisible(); - await page.click('[data-test="fetch-turtle"]'); - await expect(page.locator('text= { diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts index e67821516..bbb17b965 100644 --- a/browser/e2e/tests/test-utils.ts +++ b/browser/e2e/tests/test-utils.ts @@ -237,7 +237,6 @@ export async function newResource(klass: string, page: Page) { if (klass.startsWith('https://')) { await fillSearchBox(page, 'Search for a class or enter a URL', klass); await page.keyboard.press('Enter'); - await page.click(`button:has-text("new ")`); } else { await page.locator(`button:has-text("${klass}")`).click(); } From ffab9518507442e9151c9fa66c38697e7380f56d Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 8 Jan 2024 18:09:13 +0100 Subject: [PATCH 5/6] Documentation, add Collections new form #747 --- .../BasicInstanceHandlers.ts | 18 +--- .../CustomForms/NewCollectionDialog.tsx | 101 ++++++++++++++++++ .../CustomCreateActions/CustomForms/index.ts | 7 +- .../components/forms/NewForm/NewFormTitle.tsx | 1 - .../forms/NewForm/useNewResourceUI.tsx | 11 ++ .../src/hooks/useCreateAndNavigate.ts | 2 +- 6 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts index 775da1466..290153d1a 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts @@ -1,6 +1,10 @@ import { dataBrowser, core, classes, server } from '@tomic/react'; import { registerBasicInstanceHandler } from '../useNewResourceUI'; +/** + * These handlers do not show any UI / inputs when creating new instances. + * This is where they can have hardcoded default values or custom logic. + */ export const registerBasicInstanceHandlers = () => { registerBasicInstanceHandler( dataBrowser.classes.folder, @@ -29,20 +33,6 @@ export const registerBasicInstanceHandlers = () => { }, ); - registerBasicInstanceHandler( - dataBrowser.classes.folder, - async (parent, createAndNavigate) => { - await createAndNavigate( - dataBrowser.classes.folder, - { - [core.properties.name]: 'Untitled Folder', - [dataBrowser.properties.displayStyle]: classes.displayStyles.list, - }, - parent, - ); - }, - ); - registerBasicInstanceHandler( dataBrowser.classes.document, async (parent, createAndNavigate) => { diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx new file mode 100644 index 000000000..681e9c624 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx @@ -0,0 +1,101 @@ +import { JSONValue, collections, core } from '@tomic/react'; +import { useState, useCallback, FormEvent, useEffect, FC } from 'react'; +import { Button } from '../../../../Button'; +import { + useDialog, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; +import { ResourceSelector } from '../../../ResourceSelector'; + +export const NewCollectionDialog: FC = ({ + parent, + onClose, +}) => { + const [name, setName] = useState('New Collection'); + const [valueFilter, setValue] = useState(); + const [propertyFilter, setProperty] = useState(); + + const [dialogProps, show, hide] = useDialog({ onCancel: onClose }); + + const createResourceAndNavigate = useCreateAndNavigate(); + + const onDone = useCallback( + (e: FormEvent) => { + e.preventDefault(); + + createResourceAndNavigate( + collections.classes.collection, + { + [core.properties.name]: name, + [collections.properties.value]: valueFilter, + [collections.properties.property]: propertyFilter, + [collections.properties.pageSize]: 30, + [collections.properties.currentPage]: 0, + }, + parent, + ); + + onClose(); + }, + [valueFilter, onClose, propertyFilter], + ); + + useEffect(() => { + show(); + }, []); + + return ( + + +

New Collection

+
+ +
+ + + setName(e.target.value)} + /> + + + +
+ +
+
+ + + setValue(e.target.value)} + /> + + +
+
+ + + + +
+ ); +}; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts index b4c47412e..20adb421c 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts @@ -1,11 +1,16 @@ -import { dataBrowser, core } from '@tomic/react'; +import { dataBrowser, core, collections } from '@tomic/react'; import { registerNewResourceDialog } from '../../useNewResourceUI'; import { NewBookmarkDialog } from './NewBookmarkDialog'; import { NewOntologyDialog } from './NewOntologyDialog'; import { NewTableDialog } from './NewTableDialog'; +import { NewCollectionDialog } from './NewCollectionDialog'; export const registerCustomForms = () => { registerNewResourceDialog(dataBrowser.classes.bookmark, NewBookmarkDialog); registerNewResourceDialog(core.classes.ontology, NewOntologyDialog); registerNewResourceDialog(dataBrowser.classes.table, NewTableDialog); + registerNewResourceDialog( + collections.classes.collection, + NewCollectionDialog, + ); }; diff --git a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx index d966fce5f..613d1b236 100644 --- a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx +++ b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx @@ -2,7 +2,6 @@ import { properties, useResource, useString, useTitle } from '@tomic/react'; import { useState } from 'react'; import { FaInfo } from 'react-icons/fa'; import { AtomicLink } from '../../AtomicLink'; -import { Button } from '../../Button'; import Markdown from '../../datatypes/Markdown'; import { Column, Row } from '../../Row'; import styled from 'styled-components'; diff --git a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx index 1fca9f89e..38266aa84 100644 --- a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx +++ b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx @@ -21,6 +21,7 @@ export interface CustomResourceDialogProps { onClose: () => void; } +/** When creating a new resource, the matched handler is called */ export type BasicInstanceHandler = ( parent: string, createAndNavigate: CreateAndNavigate, @@ -37,12 +38,18 @@ interface NewResourceUIContext { const dialogs = new Map>(); const basicNewInstanceHandlers = new Map(); +/** + * Returns a function that when called, renders UI to create a new Resource of the given class. + * + * Use {@link registerNewResourceDialog} to register a custom dialog for a given class. + */ export function useNewResourceUI() { const { showNewResourceUI } = useContext(NewResourceUIContext); return showNewResourceUI; } +/** Call this when adding a new custom New Resource Form / Dialog. */ export const registerNewResourceDialog = ( classSubject: string, component: FC, @@ -50,6 +57,9 @@ export const registerNewResourceDialog = ( dialogs.set(classSubject, component); }; +/** Call this when adding a new custom action for a New Resource that does _not_ require inputs. + * For example, creating a new Folder does not require any inputs, so it can be handled without any UI. + */ export const registerBasicInstanceHandler = ( classSubject: string, handler: BasicInstanceHandler, @@ -61,6 +71,7 @@ const NewResourceUIContext = createContext({ showNewResourceUI: () => undefined, }); +/** Renders the Dialog used when creating new resources. */ export function NewResourceUIProvider({ children }: PropsWithChildren) { const store = useStore(); const settings = useSettings(); diff --git a/browser/data-browser/src/hooks/useCreateAndNavigate.ts b/browser/data-browser/src/hooks/useCreateAndNavigate.ts index 898ddab86..b10a77e49 100644 --- a/browser/data-browser/src/hooks/useCreateAndNavigate.ts +++ b/browser/data-browser/src/hooks/useCreateAndNavigate.ts @@ -16,7 +16,7 @@ export type CreateAndNavigate = ( * Hook that builds a function that will create a new resource with the given * properties and then navigate to it. * - * @returns A createAndNavigate function. + * @returns A {@link CreateAndNavigate} function. */ export function useCreateAndNavigate(): CreateAndNavigate { const store = useStore(); From d84f5c6a40d75d12bef4068eff1980ee9a239260 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 9 Jan 2024 14:06:23 +0100 Subject: [PATCH 6/6] Lint fixes --- browser/data-browser/src/components/ProgressBar.tsx | 2 +- .../CustomCreateActions/CustomForms/NewCollectionDialog.tsx | 2 +- .../NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx | 2 +- .../data-browser/src/components/forms/NewForm/NewFormTitle.tsx | 2 +- browser/data-browser/src/routes/NewResource/ButtonSection.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/browser/data-browser/src/components/ProgressBar.tsx b/browser/data-browser/src/components/ProgressBar.tsx index c25f6a1bc..a3446c91d 100644 --- a/browser/data-browser/src/components/ProgressBar.tsx +++ b/browser/data-browser/src/components/ProgressBar.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; interface ProgressBarProps { value: number; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx index 681e9c624..39b5d5841 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewCollectionDialog.tsx @@ -1,4 +1,4 @@ -import { JSONValue, collections, core } from '@tomic/react'; +import { collections, core } from '@tomic/react'; import { useState, useCallback, FormEvent, useEffect, FC } from 'react'; import { Button } from '../../../../Button'; import { diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx index f25619896..2b961f893 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx @@ -1,6 +1,6 @@ import { useResource, Core, dataBrowser, core, useStore } from '@tomic/react'; import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; -import styled from 'styled-components'; +import { styled } from 'styled-components'; import { stringToSlug } from '../../../../../helpers/stringToSlug'; import { BetaBadge } from '../../../../BetaBadge'; import { Button } from '../../../../Button'; diff --git a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx index 613d1b236..717917586 100644 --- a/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx +++ b/browser/data-browser/src/components/forms/NewForm/NewFormTitle.tsx @@ -4,7 +4,7 @@ import { FaInfo } from 'react-icons/fa'; import { AtomicLink } from '../../AtomicLink'; import Markdown from '../../datatypes/Markdown'; import { Column, Row } from '../../Row'; -import styled from 'styled-components'; +import { styled } from 'styled-components'; import { IconButton, IconButtonVariant } from '../../IconButton/IconButton'; export enum NewFormTitleVariant { diff --git a/browser/data-browser/src/routes/NewResource/ButtonSection.tsx b/browser/data-browser/src/routes/NewResource/ButtonSection.tsx index 9a0a92a91..a8d503908 100644 --- a/browser/data-browser/src/routes/NewResource/ButtonSection.tsx +++ b/browser/data-browser/src/routes/NewResource/ButtonSection.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from 'react'; import { Row } from '../../components/Row'; -import styled from 'styled-components'; +import { styled } from 'styled-components'; interface ButtonSectionProps { title: string;