diff --git a/Earthfile b/Earthfile index 5cd8d51eb..41256c3b0 100644 --- a/Earthfile +++ b/Earthfile @@ -91,7 +91,7 @@ docker-musl: SAVE IMAGE --push ${tags} setup-playwright: - FROM mcr.microsoft.com/playwright:v1.38.0-jammy + FROM mcr.microsoft.com/playwright:v1.43.1-jammy RUN curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm RUN apt update && apt install -y zip RUN pnpx playwright install --with-deps diff --git a/browser/.eslintrc.cjs b/browser/.eslintrc.cjs index 5221e0d4d..85eda6450 100644 --- a/browser/.eslintrc.cjs +++ b/browser/.eslintrc.cjs @@ -34,7 +34,8 @@ module.exports = { 'lib/tsconfig.json', 'cli/tsconfig.json', 'react/tsconfig.json', - 'data-browser/tsconfig.json' + 'data-browser/tsconfig.json', + 'e2e/tsconfig.json', ], }, plugins: ['react', '@typescript-eslint', 'prettier', 'react-hooks', 'jsx-a11y'], diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 1afc04b4f..3237aaa78 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog -This changelog covers all three packages, as they are (for now) updated as a whole +This changelog covers all five packages, as they are (for now) updated as a whole ## Unreleased ### Atomic Browser +- [#845](https://github.com/atomicdata-dev/atomic-server/issues/845) Add option to create instances and tables from the ontology view. +- [#845](https://github.com/atomicdata-dev/atomic-server/issues/845) Add default Ontology option to drives. - [#841](https://github.com/atomicdata-dev/atomic-server/issues/841) Add better inputs for `Timestamp` and `Date` datatypes. - [#842](https://github.com/atomicdata-dev/atomic-server/issues/842) Add media picker for properties with classtype file. - [#850](https://github.com/atomicdata-dev/atomic-server/issues/850) Add drag & drop sorting to ResourceArray inputs. @@ -24,6 +26,7 @@ This changelog covers all three packages, as they are (for now) updated as a who - Added `collection.totalPages`. - BREAKING CHANGE: Renamed `resource.getCommitsCollection` to `resource.getCommitsCollectionSubject`. - BREAKING CHANGE: `resource.getChildrenCollection()` now returns a `Promise` instead of a subject. +- BREAKING CHANGE: `resource.createSubject()` no longer accepts a class name as an argument and defaults to a fully random subject. - BREAKING CHANGE: Resource now keeps a reference to store internally, therefore all methods that required you to pass a store have been changed to not require a store. These methods are: - `resource.canWrite()` diff --git a/browser/cli/package.json b/browser/cli/package.json index 7e124ba4a..740427d7a 100644 --- a/browser/cli/package.json +++ b/browser/cli/package.json @@ -23,7 +23,7 @@ "watch": "tsc --build --watch", "start": "pnpm watch", "tsc": "tsc --build", - "typecheck": "tsc --noEmit" + "typecheck": "pnpm exec tsc --noEmit" }, "bin": { "ad-generate": "./bin/src/index.js" diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index e98bca8cb..0c567c487 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -50,7 +50,7 @@ "@types/react-window": "^1.8.7", "@vitejs/plugin-react-swc": "^3.5.0", "csstype": "^3.1.0", - "gh-pages": "^3.1.0", + "gh-pages": "^5.0.0", "lint-staged": "^10.5.4", "types-wm": "^1.1.0", "vite-plugin-pwa": "^0.17.0", @@ -78,6 +78,6 @@ "preview": "vite preview", "start": "vite", "test": "jest", - "typecheck": "tsc --noEmit" + "typecheck": "pnpm exec tsc --noEmit" } } diff --git a/browser/data-browser/src/components/Button.tsx b/browser/data-browser/src/components/Button.tsx index 0fd53380b..cb8a99226 100644 --- a/browser/data-browser/src/components/Button.tsx +++ b/browser/data-browser/src/components/Button.tsx @@ -79,6 +79,7 @@ export const ButtonClean = styled.button` appearance: none; background-color: initial; -webkit-tap-highlight-color: transparent; /* Remove the tap / click effect on touch devices */ + user-select: none; `; /** Base button style. You're likely to want to use ButtonMargin in most places */ diff --git a/browser/data-browser/src/components/Details/index.tsx b/browser/data-browser/src/components/Details.tsx similarity index 98% rename from browser/data-browser/src/components/Details/index.tsx rename to browser/data-browser/src/components/Details.tsx index e5dcb1428..bacc9ab0c 100644 --- a/browser/data-browser/src/components/Details/index.tsx +++ b/browser/data-browser/src/components/Details.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; import { styled } from 'styled-components'; import { FaCaretRight } from 'react-icons/fa'; -import { Collapse } from '../Collapse'; +import { Collapse } from './Collapse'; export interface DetailsProps { open?: boolean; diff --git a/browser/data-browser/src/components/Dialog/waitForActiveDocument.ts b/browser/data-browser/src/components/Dialog/waitForActiveDocument.ts new file mode 100644 index 000000000..a2e3f84f9 --- /dev/null +++ b/browser/data-browser/src/components/Dialog/waitForActiveDocument.ts @@ -0,0 +1,17 @@ +/** + * Waits for when the document becomes active again after it has been inert. + * (Useful for waiting for a dialog to close before navigating to a new page) + */ +export function waitForActiveDocument(callback: () => void) { + const observer = new MutationObserver(() => { + if (!document.body.hasAttribute('inert')) { + callback(); + observer.disconnect(); + } + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ['inert'], + }); +} diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx index 76acb9a11..6eaf4c948 100644 --- a/browser/data-browser/src/components/Dropdown/index.tsx +++ b/browser/data-browser/src/components/Dropdown/index.tsx @@ -35,11 +35,11 @@ export type MenuItemMinimial = { shortcut?: string; }; -export type Item = typeof DIVIDER | MenuItemMinimial; +export type DropdownItem = typeof DIVIDER | MenuItemMinimial; interface DropdownMenuProps { /** The list of menu items */ - items: Item[]; + items: DropdownItem[]; trigger: DropdownTriggerRenderFunction; /** Enables the keyboard shortcut */ isMainMenu?: boolean; @@ -51,7 +51,7 @@ export const isItem = ( ): item is MenuItemMinimial => typeof item !== 'string' && typeof item?.label === 'string'; -const shouldSkip = (item?: Item) => !isItem(item) || item.disabled; +const shouldSkip = (item?: DropdownItem) => !isItem(item) || item.disabled; const getAdditionalOffest = (increment: number) => increment === 0 ? 1 : Math.sign(increment); @@ -62,7 +62,7 @@ const getAdditionalOffest = (increment: number) => * Returns 0 when no suitable index is found. */ const createIndexOffset = - (items: Item[]) => (startingPoint: number, offset: number) => { + (items: DropdownItem[]) => (startingPoint: number, offset: number) => { const findNextAvailable = ( scopedStartingPoint: number, scopedOffset: number, @@ -84,8 +84,8 @@ const createIndexOffset = return findNextAvailable(startingPoint, offset); }; -function normalizeItems(items: Item[]) { - return items.reduce((acc: Item[], current, i) => { +function normalizeItems(items: DropdownItem[]) { + return items.reduce((acc: DropdownItem[], current, i) => { // If the item is a divider at the start or end of the list, remove it. if ((i === 0 || i === items.length - 1) && !isItem(current)) { return acc; @@ -136,7 +136,6 @@ export function DropdownMenu({ useClickAwayListener([triggerRef, dropdownRef], handleClose, isActive, [ 'click', - 'mouseout', ]); const normalizedItems = useMemo(() => normalizeItems(items), [items]); @@ -412,6 +411,9 @@ const MenuItemStyled = styled(Button)` p.selected ? p.theme.colors.bg1 : p.theme.colors.bg}; text-decoration: ${p => (p.selected ? 'underline' : 'none')}; + & svg { + color: ${p => p.theme.colors.textLight}; + } &:hover { background-color: ${p => p.theme.colors.bg1}; } @@ -419,12 +421,17 @@ const MenuItemStyled = styled(Button)` background-color: ${p => p.theme.colors.bg2}; } &:disabled { - color: ${p => p.theme.colors.textLight}; + color: ${p => p.theme.colors.textLight2}; cursor: default; + background-color: ${p => p.theme.colors.bg}; + &:hover { cursor: 'default'; } - background-color: ${p => p.theme.colors.bg}; + + & svg { + color: ${p => p.theme.colors.textLight2}; + } } svg { diff --git a/browser/data-browser/src/components/InviteForm.tsx b/browser/data-browser/src/components/InviteForm.tsx index f34bdde9b..076e6a334 100644 --- a/browser/data-browser/src/components/InviteForm.tsx +++ b/browser/data-browser/src/components/InviteForm.tsx @@ -26,7 +26,7 @@ interface InviteFormProps { */ export function InviteForm({ target }: InviteFormProps) { const store = useStore(); - const [subject] = useState(() => store.createSubject('invite')); + const [subject] = useState(() => store.createSubject()); const invite = useResource(subject, { newResource: true, }); @@ -59,7 +59,7 @@ export function InviteForm({ target }: InviteFormProps) { return ( @@ -69,11 +69,11 @@ export function InviteForm({ target }: InviteFormProps) { resource={invite} /> - + {err && (

{err.message} @@ -84,7 +84,7 @@ export function InviteForm({ target }: InviteFormProps) { } else return ( -

Invite created and copied to clipboard! Send it to your buddy:

+

Invite created and copied to clipboard! 🚀

); diff --git a/browser/data-browser/src/components/ParentPicker/ParentPicker.tsx b/browser/data-browser/src/components/ParentPicker/ParentPicker.tsx new file mode 100644 index 000000000..39c8010f8 --- /dev/null +++ b/browser/data-browser/src/components/ParentPicker/ParentPicker.tsx @@ -0,0 +1,51 @@ +import { styled } from 'styled-components'; +import { Column } from '../Row'; +import { ParentPickerItem } from './ParentPickerItem'; +import { InputStyled, InputWrapper } from '../forms/InputStyles'; +import { useSettings } from '../../helpers/AppSettings'; +import { FaFolderOpen } from 'react-icons/fa6'; + +export interface ParentPickerProps { + root?: string; + value: string | undefined; + onChange: (subject: string) => void; +} + +export function ParentPicker({ + root, + value, + onChange, +}: ParentPickerProps): React.JSX.Element { + const { drive } = useSettings(); + + return ( + + + + onChange(e.target.value)} + /> + + + + + + ); +} + +const PickerWrapper = styled.section` + background-color: ${p => p.theme.colors.bg}; + border-radius: ${p => p.theme.radius}; + border: 1px solid ${p => p.theme.colors.bg2}; + padding: ${p => p.theme.margin}rem; + + height: 20.5rem; + overflow-y: auto; +`; diff --git a/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx b/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx new file mode 100644 index 000000000..efbc4a85c --- /dev/null +++ b/browser/data-browser/src/components/ParentPicker/ParentPickerDialog.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + useDialog, +} from '../Dialog'; +import { ParentPicker } from './ParentPicker'; +import { Button } from '../Button'; +import { waitForActiveDocument } from '../Dialog/waitForActiveDocument'; + +interface ParentPickerDialogProps { + root?: string; + open: boolean; + onSelect: (subject: string) => void; + onCancel?: () => void; + onOpenChange?: (open: boolean) => void; + title?: string; +} + +export function ParentPickerDialog({ + open, + root, + title, + onSelect, + onCancel, + onOpenChange, +}: ParentPickerDialogProps): React.JSX.Element { + const [selected, setSelected] = useState(); + + const [dialogProps, show, close, isOpen] = useDialog({ + onCancel, + bindShow: onOpenChange, + }); + + const select = () => { + if (!selected) return; + + waitForActiveDocument(() => { + onSelect(selected); + }); + close(true); + }; + + useEffect(() => { + if (open) { + show(); + } else { + close(); + setSelected(undefined); + } + }, [open]); + + return ( + + {isOpen && ( + <> + +

{title ?? 'Select a location'}

+
+ + + + + + + + + )} +
+ ); +} diff --git a/browser/data-browser/src/components/ParentPicker/ParentPickerItem.tsx b/browser/data-browser/src/components/ParentPicker/ParentPickerItem.tsx new file mode 100644 index 000000000..1ef371fbf --- /dev/null +++ b/browser/data-browser/src/components/ParentPicker/ParentPickerItem.tsx @@ -0,0 +1,148 @@ +import { + core, + dataBrowser, + Resource, + server, + useArray, + useCollection, + useResource, + useStore, +} from '@tomic/react'; +import { Details } from '../Details'; +import { useEffect, useState } from 'react'; +import { getIconForClass } from '../../views/FolderPage/iconMap'; +import { styled } from 'styled-components'; + +const shouldBeRendered = (resource: Resource) => + resource.hasClasses(dataBrowser.classes.folder) || + resource.hasClasses(server.classes.drive); + +interface ParentPickerItemProps { + subject: string; + selectedValue: string | undefined; + inialOpen?: boolean; + onClick: (subject: string) => void; +} + +export const ParentPickerItem: React.FC = ({ + subject, + ...props +}) => { + const resource = useResource(subject); + + if ( + !resource.hasClasses(dataBrowser.classes.folder) && + !resource.hasClasses(server.classes.drive) + ) { + return null; + } + + return ; +}; + +const InnerItem = ({ + subject, + selectedValue, + inialOpen, + onClick, +}: ParentPickerItemProps) => { + const store = useStore(); + const { collection } = useCollection({ + property: core.properties.parent, + value: subject, + }); + + const [children, setChildren] = useState([]); + + useEffect(() => { + collection.getAllMembers().then(async (members: string[]) => { + const resources = await Promise.all( + members.map(s => store.getResource(s)), + ); + const filtered = resources.filter(shouldBeRendered); + + setChildren(filtered.map(r => r.subject)); + }); + }, [collection]); + + if (children.length === 0) { + return ( + + ); + } + + return ( + <Details + initialState={inialOpen} + open={inialOpen} + title={ + <Title + subject={subject} + selected={selectedValue === subject} + onClick={onClick} + /> + } + > + {children.map(child => ( + <ParentPickerItem + key={child} + subject={child} + selectedValue={selectedValue} + onClick={onClick} + /> + ))} + </Details> + ); +}; + +interface TitleProps extends Omit<ParentPickerItemProps, 'selectedValue'> { + indented?: boolean; + selected?: boolean; +} + +const Title = ({ + subject, + indented, + selected, + onClick, +}: TitleProps): React.JSX.Element => { + const resource = useResource(subject); + const [isA] = useArray(resource, core.properties.isA); + + const Icon = getIconForClass(isA[0]); + + return ( + <FolderButton + selected={selected} + indented={indented} + onClick={() => onClick(subject)} + > + <Icon /> + {resource.title} + </FolderButton> + ); +}; + +const FolderButton = styled.button<{ indented?: boolean; selected?: boolean }>` + display: flex; + align-items: center; + gap: 1ch; + background-color: ${p => (p.selected ? p.theme.colors.bg1 : 'transparent')}; + color: ${p => (p.selected ? p.theme.colors.main : p.theme.colors.textLight)}; + cursor: pointer; + border: none; + padding: 0.3rem 0.5rem; + margin-inline-start: ${p => (p.indented ? '2rem' : '0')}; + border-radius: ${p => p.theme.radius}; + user-select: none; + + &:hover { + background-color: ${p => p.theme.colors.bg1}; + color: ${p => (p.selected ? p.theme.colors.main : p.theme.colors.text)}; + } +`; diff --git a/browser/data-browser/src/components/ResourceContextMenu/index.tsx b/browser/data-browser/src/components/ResourceContextMenu/index.tsx index b80a5247e..383b5ff54 100644 --- a/browser/data-browser/src/components/ResourceContextMenu/index.tsx +++ b/browser/data-browser/src/components/ResourceContextMenu/index.tsx @@ -9,7 +9,7 @@ import { shareURL, importerURL, } from '../../helpers/navigation'; -import { DIVIDER, DropdownMenu, isItem, Item } from '../Dropdown'; +import { DIVIDER, DropdownMenu, isItem, DropdownItem } from '../Dropdown'; import toast from 'react-hot-toast'; import { paths } from '../../routes/paths'; import { shortcuts } from '../HotKeyWrapper'; @@ -105,7 +105,7 @@ function ResourceContextMenu({ return null; } - const items: Item[] = [ + const items: DropdownItem[] = [ ...(simple ? [] : [ diff --git a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx index 9a640dd5c..511918c1a 100644 --- a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx +++ b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx @@ -49,6 +49,7 @@ export function DriveSwitcher() { const items = useMemo( () => [ ...Array.from(savedDrivesMap.entries()) + .filter(([_, resource]) => !resource.error) .map(([subject, resource]) => ({ id: subject, label: getTitle(resource), @@ -59,8 +60,7 @@ export function DriveSwitcher() { navigate(constructOpenURL(subject)); }, icon: subject === drive ? <FaRegCheckCircle /> : <FaRegCircle />, - })) - .slice(0, 5), + })), DIVIDER, // Dedupe history from savedDrives bause not all savedDrives might be loaded yet. ...Array.from(dedupeAFromB(historyMap, savedDrivesMap)) @@ -88,6 +88,7 @@ export function DriveSwitcher() { helper: 'Create a new drive', onClick: () => createNewResource(server.classes.drive, agent?.subject ?? ''), + disabled: !agent, }, ], [savedDrivesMap, drive, historyMap], diff --git a/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx b/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx index 66466a732..fda809e0f 100644 --- a/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx +++ b/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx @@ -1,10 +1,10 @@ import { styled } from 'styled-components'; import { - Collection, + core, + removeCachedSearchResults, unknownSubject, - urls, - useCollection, - useMemberFromCollection, + useResource, + useStore, } from '@tomic/react'; import { SideBarItem } from '../SideBarItem'; import { Row } from '../../Row'; @@ -12,18 +12,41 @@ import { AtomicLink } from '../../AtomicLink'; import { getIconForClass } from '../../../views/FolderPage/iconMap'; import { ScrollArea } from '../../ScrollArea'; import { ErrorLook } from '../../ErrorLook'; +import { useCallback, useEffect, useState } from 'react'; +import { useSettings } from '../../../helpers/AppSettings'; export function OntologiesPanel(): JSX.Element | null { - const { collection } = useCollection({ - property: urls.properties.isA, - value: urls.classes.ontology, - }); + const store = useStore(); + const [ontologies, setOntologies] = useState<string[]>([]); + const { drive } = useSettings(); + + const search = useCallback(async () => { + removeCachedSearchResults(store); + + const result = await store.search('', { + filters: { + [core.properties.isA]: core.classes.ontology, + }, + parents: drive, + }); + + setOntologies(result); + }, [store, drive]); + + useEffect(() => { + search(); + + // If the drive was just created we need to wait for search to index the new ontology. So we search again after 5 seconds. + setTimeout(() => { + search(); + }, 5000); + }, [drive, search]); return ( <Wrapper> - <StyledScrollArea> - {[...Array(collection.totalMembers).keys()].map(index => ( - <Item key={index} collection={collection} index={index} /> + <StyledScrollArea key={drive}> + {ontologies.map(subject => ( + <Item key={subject} subject={subject} /> ))} </StyledScrollArea> </Wrapper> @@ -37,25 +60,24 @@ const Wrapper = styled.div` `; const StyledScrollArea = styled(ScrollArea)` - height: 10rem; + max-height: 10rem; overflow-x: hidden; `; interface ItemProps { - index: number; - collection: Collection; + subject: string; } -function Item({ index, collection }: ItemProps): JSX.Element { - const resource = useMemberFromCollection(collection, index); +function Item({ subject }: ItemProps): JSX.Element { + const resource = useResource(subject); - const Icon = getIconForClass(urls.classes.ontology); + const Icon = getIconForClass(core.classes.ontology); if (resource.loading) { return <div>loading</div>; } - if (resource.error || resource.getSubject() === unknownSubject) { + if (resource.error || resource.subject === unknownSubject) { return ( <SideBarItem> <ErrorLook>Invalid Resource</ErrorLook> @@ -64,7 +86,7 @@ function Item({ index, collection }: ItemProps): JSX.Element { } return ( - <StyledLink subject={resource.getSubject()} clean> + <StyledLink subject={subject} clean> <SideBarItem> <Row gap='1ch' center> <Icon /> diff --git a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx index 30ee17991..f21ae5a48 100644 --- a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx +++ b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx @@ -78,7 +78,7 @@ export function SideBarDrive({ navigate(constructOpenURL(drive)); }} > - <DriveTitle data-test='current-drive-title'> + <DriveTitle data-testid='current-drive-title'> {title || drive}{' '} </DriveTitle> </TitleButton> diff --git a/browser/data-browser/src/components/SideBar/index.tsx b/browser/data-browser/src/components/SideBar/index.tsx index 357ead724..6cbdf87d3 100644 --- a/browser/data-browser/src/components/SideBar/index.tsx +++ b/browser/data-browser/src/components/SideBar/index.tsx @@ -138,8 +138,6 @@ const SideBarStyled = styled.nav.attrs<SideBarStyledProps>(p => ({ flex-direction: column; overflow-y: auto; overflow-x: hidden; - - view-transition-name: sidebar; `; const MenuWrapper = styled.div` diff --git a/browser/data-browser/src/components/Table.tsx b/browser/data-browser/src/components/Table.tsx index e3919d917..d97f850be 100644 --- a/browser/data-browser/src/components/Table.tsx +++ b/browser/data-browser/src/components/Table.tsx @@ -199,12 +199,7 @@ function Cell({ resource, prop: propUrl }: CellProps): JSX.Element { return ( <CellStyled> <CellContainer> - <ValueForm - key={propUrl} - resource={resource} - propertyURL={propUrl} - noMargin - /> + <ValueForm key={propUrl} resource={resource} propertyURL={propUrl} /> </CellContainer> </CellStyled> ); diff --git a/browser/data-browser/src/components/TableEditor/Cell.tsx b/browser/data-browser/src/components/TableEditor/Cell.tsx index 3de69bf41..6f2239e33 100644 --- a/browser/data-browser/src/components/TableEditor/Cell.tsx +++ b/browser/data-browser/src/components/TableEditor/Cell.tsx @@ -219,22 +219,26 @@ export function IndexCell({ onExpand, ...props }: React.PropsWithChildren<IndexCellProps>): JSX.Element { + const { markings } = useTableEditorContext(); + + const marking = markings.get(props.rowIndex); + return ( - <StyledIndexCell role='rowheader' {...props}> + <StyledIndexCell role='rowheader' {...props} hasMarking={!!marking}> <IconButton title='Open resource' onClick={() => onExpand(props.rowIndex)} > <FaExpandAlt /> </IconButton> - <IndexNumber>{children}</IndexNumber> + {marking ? marking : <IndexNumber>{children}</IndexNumber>} </StyledIndexCell> ); } const IndexNumber = styled.span``; -const StyledIndexCell = styled(Cell)` +const StyledIndexCell = styled(Cell)<{ hasMarking: boolean }>` justify-content: flex-end !important; color: ${p => p.theme.colors.textLight}; @@ -246,9 +250,9 @@ const StyledIndexCell = styled(Cell)` display: none; } - &:hover button, - &:focus-within button { - display: block; + &:not([data-hasmarking='true']):hover button, + &:not([data-hasmarking='true']):focus-within button { + display: ${p => (p.hasMarking ? 'none' : 'block')}; } `; diff --git a/browser/data-browser/src/components/TableEditor/TableEditorContext.tsx b/browser/data-browser/src/components/TableEditor/TableEditorContext.tsx index c7ae538bc..7bae3e82f 100644 --- a/browser/data-browser/src/components/TableEditor/TableEditorContext.tsx +++ b/browser/data-browser/src/components/TableEditor/TableEditorContext.tsx @@ -5,6 +5,7 @@ import { useState, createContext, useContext, + ReactElement, } from 'react'; import { FixedSizeList } from 'react-window'; import { EventManager } from '../../helpers/EventManager'; @@ -63,6 +64,8 @@ export interface TableEditorContext { clearCell: () => void; clearRow: (index: number) => void; enterEditModeWithCharacter: (key: string) => void; + markings: Map<number, ReactElement>; + setMarkings: React.Dispatch<React.SetStateAction<Map<number, ReactElement>>>; registerEventListener<T extends TableEvent>( event: T, cb: TableEventHandlers[T], @@ -70,7 +73,7 @@ export interface TableEditorContext { emitInteractionsFired(interactions: KeyboardInteraction[]): void; } -const initial = { +const initial: TableEditorContext = { mouseDown: false, setMouseDown: emptySetState, tableRef: { current: null }, @@ -94,6 +97,8 @@ const initial = { clearCell: () => undefined, clearRow: (_: number) => undefined, enterEditModeWithCharacter: (_: string) => undefined, + markings: new Map<number, ReactElement>(), + setMarkings: emptySetState, registerEventListener: () => () => undefined, emitInteractionsFired: () => undefined, }; @@ -125,6 +130,10 @@ export function TableEditorContextProvider({ const [indicatorHidden, setIndicatorHidden] = useState(false); + const [markings, setMarkings] = useState<Map<number, ReactElement>>( + new Map(), + ); + const activeCellRef = useRef<HTMLDivElement | null>(null); const multiSelectCornerCellRef = useRef<HTMLDivElement | null>(null); @@ -196,6 +205,8 @@ export function TableEditorContextProvider({ clearRow, enterEditModeWithCharacter, emitInteractionsFired, + markings, + setMarkings, }), [ disabledKeyboardInteractions, @@ -210,6 +221,7 @@ export function TableEditorContextProvider({ cursorMode, emitInteractionsFired, mouseDown, + markings, ], ); diff --git a/browser/data-browser/src/components/Toaster.tsx b/browser/data-browser/src/components/Toaster.tsx index 88d06e4f1..f283d0144 100644 --- a/browser/data-browser/src/components/Toaster.tsx +++ b/browser/data-browser/src/components/Toaster.tsx @@ -2,7 +2,8 @@ import toast, { ToastBar, Toaster as ReactHotToast } from 'react-hot-toast'; import { FaCopy, FaTimes } from 'react-icons/fa'; import { useTheme } from 'styled-components'; import { zIndex } from '../styling'; -import { Button } from './Button'; +import { Row } from './Row'; +import { IconButton } from './IconButton/IconButton'; /** * Makes themed toast notifications available in the Context. Render this @@ -58,7 +59,7 @@ function ToastMessage({ icon, message, t }) { } return ( - <> + <Row gap='1ch' center> {icon} {text} {t.type !== 'loading' && ( @@ -68,16 +69,16 @@ function ToastMessage({ icon, message, t }) { flexDirection: 'column', }} > - <Button title='Clear' subtle onClick={() => toast.dismiss(t.id)}> + <IconButton title='Clear' onClick={() => toast.dismiss(t.id)}> <FaTimes /> - </Button> + </IconButton> {t.type !== 'success' && ( - <Button title='Copy' subtle onClick={handleCopy}> + <IconButton title='Copy' onClick={handleCopy}> <FaCopy /> - </Button> + </IconButton> )} </div> )} - </> + </Row> ); } diff --git a/browser/data-browser/src/components/datatypes/Markdown.tsx b/browser/data-browser/src/components/datatypes/Markdown.tsx index 354353338..a7f90edf2 100644 --- a/browser/data-browser/src/components/datatypes/Markdown.tsx +++ b/browser/data-browser/src/components/datatypes/Markdown.tsx @@ -45,8 +45,6 @@ Markdown.defaultProps = { }; const MarkdownWrapper = styled.div` - /* Corrects the margin added by <p> and other HTML elements */ - width: 100%; overflow-x: hidden; img { diff --git a/browser/data-browser/src/components/forms/Checkbox.tsx b/browser/data-browser/src/components/forms/Checkbox.tsx index 74df08815..6b9eada3a 100644 --- a/browser/data-browser/src/components/forms/Checkbox.tsx +++ b/browser/data-browser/src/components/forms/Checkbox.tsx @@ -40,11 +40,11 @@ const InputCheckBox = styled.input` position: relative; - :checked { + &:checked { border: none; } - :checked::before { + &:checked::before { content: ''; position: absolute; inset: 0; @@ -54,7 +54,7 @@ const InputCheckBox = styled.input` background-color: ${p => p.theme.colors.main}; } - :checked::after { + &:checked::after { --inset: 3px; --size: calc(100% - (var(--inset) * 2)); content: ''; 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 d7b2b7295..05a394465 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts @@ -1,4 +1,4 @@ -import { dataBrowser, core, classes, server } from '@tomic/react'; +import { dataBrowser, core, classes } from '@tomic/react'; import { registerBasicInstanceHandler } from '../useNewResourceUI'; /** @@ -15,7 +15,9 @@ export const registerBasicInstanceHandlers = () => { [core.properties.name]: 'Untitled Folder', [dataBrowser.properties.displayStyle]: classes.displayStyles.list, }, - parent, + { + parent, + }, ); }, ); @@ -28,7 +30,9 @@ export const registerBasicInstanceHandlers = () => { { [core.properties.name]: 'Untitled ChatRoom', }, - parent, + { + parent, + }, ); }, ); @@ -41,34 +45,10 @@ export const registerBasicInstanceHandlers = () => { { [core.properties.name]: 'Untitled Document', }, - parent, + { + 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], - }); - - // resources created with createAndNavigate have a parent by default which we don't want for drives. - newResource.remove(core.properties.parent); - - const agentResource = await store.getResource(agent.subject); - agentResource.push(server.properties.drives, [newResource.subject]); - agentResource.save(); - settings.setDrive(newResource.subject); - }, - ); }; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx new file mode 100644 index 000000000..35d8293b4 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewArticleDialog.tsx @@ -0,0 +1,110 @@ +import { validateDatatype, Datatype, core, dataBrowser } 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 Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; + +export const NewArticleDialog: FC<CustomResourceDialogProps> = ({ + parent, + onClose, +}) => { + const [name, setName] = useState(''); + const [valid, setValid] = useState(false); + + const createResourceAndNavigate = useCreateAndNavigate(); + + const onSuccess = useCallback(async () => { + const shortName = stringToSlug(name); + + const subject = `${parent}/${shortName}`; + + // TODO: make subject and stuff. + createResourceAndNavigate( + dataBrowser.classes.article, + { + [core.properties.name]: name, + [core.properties.description]: '', + }, + { + parent, + subject, + }, + ); + + onClose(); + }, [name, createResourceAndNavigate, onClose, parent]); + + const [dialogProps, show, hide] = useDialog({ onSuccess, onCancel: onClose }); + + const onNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setName(e.target.value); + const value = stringToSlug(e.target.value); + + try { + validateDatatype(value, Datatype.SLUG); + setValid(true); + } catch (_) { + setValid(false); + } + }; + + useEffect(() => { + show(); + }, []); + + return ( + <Dialog {...dialogProps}> + <H1>New Article</H1> + <DialogContent> + <form + onSubmit={(e: FormEvent) => { + e.preventDefault(); + hide(true); + }} + > + <Field required label='Title'> + <InputWrapper> + <InputStyled + placeholder='New Article' + value={name} + autoFocus={true} + onChange={onNameChange} + /> + </InputWrapper> + </Field> + <Explanation> + Title is used to construct the subject, keep in mind that the + subject cannot be changed later. + </Explanation> + </form> + </DialogContent> + <DialogActions> + <Button onClick={() => hide(false)} subtle> + Cancel + </Button> + <Button onClick={() => hide(true)} disabled={!valid}> + Create + </Button> + </DialogActions> + </Dialog> + ); +}; + +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/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx index fe2203f8a..f2b7b4811 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewBookmarkDialog.tsx @@ -43,7 +43,9 @@ export const NewBookmarkDialog: FC<CustomResourceDialogProps> = ({ [core.properties.name]: 'New Bookmark', [dataBrowser.properties.url]: normalizedUrl, }, - parent, + { + parent, + }, ); onClose(); 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 39b5d5841..349866c84 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 @@ -39,7 +39,9 @@ export const NewCollectionDialog: FC<CustomResourceDialogProps> = ({ [collections.properties.pageSize]: 30, [collections.properties.currentPage]: 0, }, - parent, + { + parent, + }, ); onClose(); diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx new file mode 100644 index 000000000..5fd049811 --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -0,0 +1,137 @@ +import { core, useStore, server, dataBrowser } from '@tomic/react'; +import { useState, useCallback, FormEvent, FC, useEffect, useId } from 'react'; +import { styled } from 'styled-components'; +import { stringToSlug } from '../../../../../helpers/stringToSlug'; +import { Button } from '../../../../Button'; +import { + useDialog, + Dialog, + DialogContent, + DialogActions, +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; +import { useSettings } from '../../../../../helpers/AppSettings'; + +export const NewDriveDialog: FC<CustomResourceDialogProps> = ({ + parent, + onClose, +}) => { + const store = useStore(); + const nameFieldId = useId(); + const { setDrive } = useSettings(); + const [name, setName] = useState(''); + + const createAndNavigate = useCreateAndNavigate(); + + const onSuccess = useCallback(async () => { + if (!name.trim()) return; + + const agent = store.getAgent(); + + if (!agent || agent.subject === undefined) { + throw new Error( + 'No agent set in the Store, required when creating a Drive', + ); + } + + const newDrive = await createAndNavigate( + server.classes.drive, + { + [core.properties.name]: name, + [core.properties.write]: [agent.subject], + [core.properties.read]: [agent.subject], + }, + { + noParent: true, + onCreated: async resource => { + // Add drive to the agents drive list. + const agentResource = await store.getResource(agent.subject!); + agentResource.push(server.properties.drives, [resource.subject]); + await agentResource.save(); + + // Create a default ontology. + const ontologyName = stringToSlug(name); + const ontology = await store.newResource({ + subject: await store.buildUniqueSubjectFromParts( + ['defaultOntology'], + resource.subject, + ), + isA: core.classes.ontology, + parent: resource.subject, + propVals: { + [core.properties.shortname]: ontologyName, + [core.properties + .description]: `Default ontology for the ${name} drive`, + [core.properties.classes]: [], + [core.properties.properties]: [], + [core.properties.instances]: [], + }, + }); + + await ontology.save(); + + await resource.set( + server.properties.defaultOntology, + ontology.subject, + ); + await resource.set(dataBrowser.properties.subResources, [ + ontology.subject, + ]); + await resource.save(); + }, + }, + ); + + // Change current drive to new drive + setDrive(newDrive.subject); + + onClose(); + }, [name, createAndNavigate, onClose, parent, setDrive, store]); + + const [dialogProps, show, hide] = useDialog({ onSuccess, onCancel: onClose }); + + useEffect(() => { + show(); + }, []); + + return ( + <Dialog {...dialogProps}> + <H1>New Drive</H1> + <DialogContent> + <form + onSubmit={(e: FormEvent) => { + e.preventDefault(); + hide(true); + }} + > + <Field required label='Name' fieldId={nameFieldId}> + <InputWrapper> + <InputStyled + id={nameFieldId} + placeholder='My Drive' + value={name} + autoFocus={true} + onChange={e => setName(e.target.value)} + /> + </InputWrapper> + </Field> + </form> + </DialogContent> + <DialogActions> + <Button onClick={() => hide(false)} subtle> + Cancel + </Button> + <Button onClick={() => hide(true)} disabled={!name.trim()}> + Create + </Button> + </DialogActions> + </Dialog> + ); +}; + +const H1 = styled.h1` + margin: 0; +`; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx index a810813be..8a05ed1c1 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewOntologyDialog.tsx @@ -33,7 +33,9 @@ export const NewOntologyDialog: FC<CustomResourceDialogProps> = ({ [core.properties.properties]: [], [core.properties.instances]: [], }, - parent, + { + parent, + }, ); onClose(); 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 5cf5eda33..03d70258f 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,4 +1,4 @@ -import { useResource, Core, dataBrowser, core, useStore } from '@tomic/react'; +import { dataBrowser, core, useStore } from '@tomic/react'; import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; import { styled } from 'styled-components'; import { stringToSlug } from '../../../../../helpers/stringToSlug'; @@ -15,101 +15,151 @@ import Field from '../../../Field'; import { InputWrapper, InputStyled } from '../../../InputStyles'; import type { CustomResourceDialogProps } from '../../useNewResourceUI'; import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; +import { ResourceSelector } from '../../../ResourceSelector'; +import { Checkbox, CheckboxLabel } from '../../../Checkbox'; +import { useAddToOntology } from '../../../../../hooks/useAddToOntology'; -const instanceOpts = { - newResource: true, -}; +interface NewTableDialogProps extends CustomResourceDialogProps { + initialExistingClass?: string; +} -export const NewTableDialog: FC<CustomResourceDialogProps> = ({ +export const NewTableDialog: FC<NewTableDialogProps> = ({ parent, + initialExistingClass, onClose, }) => { const store = useStore(); - const [instanceSubject] = useState(() => store.createSubject('class')); - const instanceResource = useResource<Core.Class>( - instanceSubject, - instanceOpts, + const [useExistingClass, setUseExistingClass] = useState( + !!initialExistingClass, + ); + const [existingClass, setExistingClass] = useState<string | undefined>( + initialExistingClass, ); - const [name, setName] = useState(''); + const addToOntology = useAddToOntology(); const createResourceAndNavigate = useCreateAndNavigate(); const onCancel = useCallback(() => { - instanceResource.destroy(); onClose(); - }, [onClose, instanceResource]); + }, [onClose]); const onSuccess = useCallback(async () => { - await instanceResource.set(core.properties.shortname, stringToSlug(name)); - await instanceResource.set( - core.properties.description, - `Represents a row in the ${name} table`, - ); - await instanceResource.set(core.properties.isA, [core.classes.class]); - await instanceResource.set(core.properties.parent, parent); - await instanceResource.set(core.properties.recommends, [ - core.properties.name, - ]); - await instanceResource.save(); + let classSubject: string; + + if (!useExistingClass) { + const instanceResource = await store.newResource({ + isA: core.classes.class, + propVals: { + [core.properties.shortname]: stringToSlug(name), + [core.properties + .description]: `Represents a row in the ${name} table`, + [core.properties.recommends]: [core.properties.name], + }, + }); + + await addToOntology(instanceResource); + classSubject = instanceResource.subject; + } else { + if (existingClass === undefined) { + throw new Error('Existing class is undefined'); + } + + classSubject = existingClass; + } createResourceAndNavigate( dataBrowser.classes.table, { [core.properties.name]: name, - [core.properties.classtype]: instanceResource.getSubject(), + [core.properties.classtype]: classSubject, + }, + { + parent, }, - parent, ); onClose(); - }, [name, instanceResource, onClose, parent]); + }, [ + name, + onClose, + parent, + useExistingClass, + existingClass, + addToOntology, + createResourceAndNavigate, + ]); - const [dialogProps, show, hide] = useDialog({ onCancel, onSuccess }); + const [dialogProps, show, hide, isOpen] = useDialog({ onCancel, onSuccess }); useEffect(() => { show(); }, []); + const hasName = name.trim() !== ''; + const saveDisabled = useExistingClass ? !hasName || !existingClass : !hasName; + return ( <Dialog {...dialogProps}> - <RelativeDialogTitle> - <h1>New Table</h1> - <BetaBadge /> - </RelativeDialogTitle> - <WiderDialogContent> - <form - onSubmit={(e: FormEvent) => { - e.preventDefault(); - hide(true); - }} - > - <Field required label='Name'> - <InputWrapper> - <InputStyled - placeholder='New Table' - value={name} - autoFocus={true} - onChange={e => setName(e.target.value)} - /> - </InputWrapper> - </Field> - </form> - </WiderDialogContent> - <DialogActions> - <Button onClick={() => hide(false)} subtle> - Cancel - </Button> - <Button onClick={() => hide(true)} disabled={name.trim() === ''}> - Create - </Button> - </DialogActions> + {isOpen && ( + <> + <RelativeDialogTitle> + <h1>New Table</h1> + <BetaBadge /> + </RelativeDialogTitle> + <WiderDialogContent> + <form + onSubmit={(e: FormEvent) => { + e.preventDefault(); + hide(true); + }} + > + <Field required label='Name'> + <InputWrapper> + <InputStyled + placeholder='New Table' + value={name} + autoFocus={true} + onChange={e => setName(e.target.value)} + /> + </InputWrapper> + </Field> + <CheckboxLabel> + <Checkbox + checked={useExistingClass} + onChange={setUseExistingClass} + /> + Use existing class + </CheckboxLabel> + <Field> + {useExistingClass && ( + <ResourceSelector + hideCreateOption + disabled={!useExistingClass} + isA={core.classes.class} + setSubject={setExistingClass} + value={existingClass} + /> + )} + </Field> + </form> + </WiderDialogContent> + <DialogActions> + <Button onClick={() => hide(false)} subtle> + Cancel + </Button> + <Button onClick={() => hide(true)} disabled={saveDisabled}> + Create + </Button> + </DialogActions> + </> + )} </Dialog> ); }; const WiderDialogContent = styled(DialogContent)` - width: min(80vw, 20rem); + /* width: min(80vw, 20rem); */ `; const RelativeDialogTitle = styled(DialogTitle)` 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 20adb421c..af08ca91a 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,9 +1,11 @@ -import { dataBrowser, core, collections } from '@tomic/react'; +import { dataBrowser, core, collections, server } from '@tomic/react'; import { registerNewResourceDialog } from '../../useNewResourceUI'; import { NewBookmarkDialog } from './NewBookmarkDialog'; import { NewOntologyDialog } from './NewOntologyDialog'; import { NewTableDialog } from './NewTableDialog'; import { NewCollectionDialog } from './NewCollectionDialog'; +import { NewDriveDialog } from './NewDriveDialog'; +import { NewArticleDialog } from './NewArticleDialog'; export const registerCustomForms = () => { registerNewResourceDialog(dataBrowser.classes.bookmark, NewBookmarkDialog); @@ -13,4 +15,6 @@ export const registerCustomForms = () => { collections.classes.collection, NewCollectionDialog, ); + registerNewResourceDialog(server.classes.drive, NewDriveDialog); + registerNewResourceDialog(dataBrowser.classes.article, NewArticleDialog); }; diff --git a/browser/data-browser/src/components/forms/NewForm/NewFormDialog.tsx b/browser/data-browser/src/components/forms/NewForm/NewFormDialog.tsx index 86cb8e95d..ececd44d4 100644 --- a/browser/data-browser/src/components/forms/NewForm/NewFormDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/NewFormDialog.tsx @@ -1,4 +1,4 @@ -import { Core, JSONValue, useResource, useStore, useTitle } from '@tomic/react'; +import { Core, JSONValue, useResource, useStore } from '@tomic/react'; import { useState, useCallback } from 'react'; import { useEffectOnce } from '../../../hooks/useEffectOnce'; import { Button } from '../../Button'; @@ -11,7 +11,6 @@ import { NewFormProps } from './NewFormPage'; import { NewFormTitle, NewFormTitleVariant } from './NewFormTitle'; import { SubjectField } from './SubjectField'; import { useNewForm } from './useNewForm'; -import { getNamePartFromProps } from '../../../helpers/getNamePartFromProps'; export interface NewFormDialogProps extends NewFormProps { closeDialog: (success?: boolean) => void; @@ -29,7 +28,6 @@ export const NewFormDialog = ({ parent, }: NewFormDialogProps): JSX.Element => { const klass = useResource<Core.Class>(classSubject); - const [className] = useTitle(klass); const store = useStore(); const [subject, setSubject] = useState<string>(); @@ -49,14 +47,7 @@ export const NewFormDialog = ({ // Onmount we generate a new subject based on the classtype and the user input. useEffectOnce(() => { (async () => { - const namePart = getNamePartFromProps(initialProps ?? {}); - - const uniqueSubject = await store.buildUniqueSubjectFromParts( - [className, namePart], - parent, - ); - - await setSubjectValue(uniqueSubject); + await setSubjectValue(store.createSubject()); for (const [prop, value] of Object.entries(initialProps ?? {})) { await resource.set(prop, value); diff --git a/browser/data-browser/src/components/forms/NewForm/useNewForm.ts b/browser/data-browser/src/components/forms/NewForm/useNewForm.ts index b2ea3e70b..112d4ad74 100644 --- a/browser/data-browser/src/components/forms/NewForm/useNewForm.ts +++ b/browser/data-browser/src/components/forms/NewForm/useNewForm.ts @@ -27,7 +27,7 @@ export const useNewForm = (args: UseNewForm) => { const [subjectValue, setSubjectValueInternal] = useState<string>(() => { if (initialSubject === undefined) { - return store.createSubject(klass.props.shortname); + return store.createSubject(); } return initialSubject; @@ -46,7 +46,7 @@ export const useNewForm = (args: UseNewForm) => { } if (isAVal.length === 0) { - await resource.addClasses(klass.getSubject()); + await resource.addClasses(klass.subject); } setInitialized(true); diff --git a/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx b/browser/data-browser/src/components/forms/NewForm/useNewResourceUI.tsx index cc6fd967e..bdc34697f 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, Store, useStore } from '@tomic/react'; +import { Store, useStore } from '@tomic/react'; import { FC, PropsWithChildren, @@ -107,10 +107,7 @@ export function NewResourceUIProvider({ children }: PropsWithChildren) { } // Default behaviour. Navigate to a new resource form for the given class. - const classResource = await store.getResource<Core.Class>(isA); - navigate( - newURL(isA, parent, store.createSubject(classResource.props.shortname)), - ); + navigate(newURL(isA, parent, store.createSubject())); }, []); const context = useMemo( diff --git a/browser/data-browser/src/components/forms/ResourceField.tsx b/browser/data-browser/src/components/forms/ResourceField.tsx index dd553441c..8858f1023 100644 --- a/browser/data-browser/src/components/forms/ResourceField.tsx +++ b/browser/data-browser/src/components/forms/ResourceField.tsx @@ -45,9 +45,8 @@ function ResourceField({ } const label = - labelProp || property.error - ? generateErrorPropName(property) - : property.shortname; + labelProp ?? + (property.error ? generateErrorPropName(property) : property.shortname); if (property.isDynamic) { return ( diff --git a/browser/data-browser/src/helpers/getNamePartFromProps.ts b/browser/data-browser/src/helpers/getNamePartFromProps.ts deleted file mode 100644 index 70e9c8a19..000000000 --- a/browser/data-browser/src/helpers/getNamePartFromProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { core, JSONValue } from '@tomic/react'; -import { randomString } from './randomString'; - -const normalizeName = (name: string) => - encodeURIComponent(name.replace(/\s/g, '-')); - -export const getNamePartFromProps = ( - props: Record<string, JSONValue>, -): string => - normalizeName( - (props?.[core.properties.shortname] as string | undefined) ?? - (props?.[core.properties.name] as string | undefined) ?? - randomString(8), - ); diff --git a/browser/data-browser/src/helpers/useOnValueChange.ts b/browser/data-browser/src/helpers/useOnValueChange.ts new file mode 100644 index 000000000..9d2094842 --- /dev/null +++ b/browser/data-browser/src/helpers/useOnValueChange.ts @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +export function useOnValueChange(callback: () => void, dependants: unknown[]) { + const [deps, setDeps] = useState(dependants); + + if (deps.some((d, i) => d !== dependants[i])) { + setDeps(dependants); + callback(); + } +} diff --git a/browser/data-browser/src/hooks/useAddToOntology.ts b/browser/data-browser/src/hooks/useAddToOntology.ts new file mode 100644 index 000000000..2a05406a9 --- /dev/null +++ b/browser/data-browser/src/hooks/useAddToOntology.ts @@ -0,0 +1,45 @@ +import { + Resource, + useResource, + useStore, + Server, + unknownSubject, + core, +} from '@tomic/react'; +import { useSettings } from '../helpers/AppSettings'; +import { useCallback } from 'react'; + +export function useAddToOntology(ontologySubject?: string) { + const store = useStore(); + const { drive: driveSubject } = useSettings(); + const drive = useResource<Server.Drive>(driveSubject); + + const ontology = useResource( + ontologySubject ?? drive.props.defaultOntology ?? unknownSubject, + ); + + return useCallback( + async (resource: Resource) => { + if (ontology.subject === unknownSubject) { + await resource.set(core.properties.parent, driveSubject); + resource.save(); + + return; + } + + await resource.set(core.properties.parent, ontology.subject); + await resource.save(); + + if (resource.hasClasses(core.classes.class)) { + ontology.push(core.properties.classes, [resource.subject], true); + } else if (resource.hasClasses(core.classes.property)) { + ontology.push(core.properties.properties, [resource.subject], true); + } else { + ontology.push(core.properties.instances, [resource.subject], true); + } + + await ontology.save(); + }, + [store, drive, ontology], + ); +} diff --git a/browser/data-browser/src/hooks/useCreateAndNavigate.ts b/browser/data-browser/src/hooks/useCreateAndNavigate.ts index fab8e55f3..5bc63ae14 100644 --- a/browser/data-browser/src/hooks/useCreateAndNavigate.ts +++ b/browser/data-browser/src/hooks/useCreateAndNavigate.ts @@ -3,13 +3,19 @@ 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'; export type CreateAndNavigate = ( isA: string, propVals: Record<string, JSONValue>, - parent?: string, - extraParams?: Record<string, string>, + options: { + parent?: string; + noParent?: boolean; + extraParams?: Record<string, string>; + /** Query parameters for the resource / endpoint */ + onCreated?: (resource: Resource) => Promise<void>; + /** Only pass subject if you really need a custom subject. Random ULID are prefered in most cases. */ + subject?: string; + }, ) => Promise<Resource>; /** @@ -22,32 +28,30 @@ export function useCreateAndNavigate(): CreateAndNavigate { const store = useStore(); const navigate = useNavigate(); - return useCallback( + const createAndNavigate: CreateAndNavigate = useCallback( async ( - isA: string, - propVals: Record<string, JSONValue>, - parent?: string, - /** Query parameters for the resource / endpoint */ - extraParams?: Record<string, string>, + isA, + propVals, + { parent, extraParams, onCreated, subject, noParent }, ): Promise<Resource> => { const classResource = await store.getResource<Core.Class>(isA); - const namePart = getNamePartFromProps(propVals); - const newSubject = await store.buildUniqueSubjectFromParts( - [classResource.props.shortname, namePart], - parent, - ); - const resource = await store.newResource({ - subject: newSubject, + subject, isA, parent, propVals, + noParent, }); try { await resource.save(); - navigate(constructOpenURL(newSubject, extraParams)); + + if (onCreated) { + await onCreated(resource); + } + + navigate(constructOpenURL(resource.subject, extraParams)); toast.success(`${classResource.title} created`); store.notifyResourceManuallyCreated(resource); } catch (e) { @@ -58,4 +62,6 @@ export function useCreateAndNavigate(): CreateAndNavigate { }, [store, navigate, parent], ); + + return createAndNavigate; } diff --git a/browser/data-browser/src/routes/SettingsServer/DriveRow.tsx b/browser/data-browser/src/routes/SettingsServer/DriveRow.tsx index 5b6fa5914..b9771177b 100644 --- a/browser/data-browser/src/routes/SettingsServer/DriveRow.tsx +++ b/browser/data-browser/src/routes/SettingsServer/DriveRow.tsx @@ -4,17 +4,33 @@ import { Button } from '../../components/Button'; import { WSIndicator } from './WSIndicator'; import { ResourceInline } from '../../views/ResourceInline'; import { FavoriteButton } from './FavoriteButton'; +import { IconButton } from '../../components/IconButton/IconButton'; +import { FaTimes } from 'react-icons/fa'; export interface DriveRowProps { subject: string; - onClick: (subject: string) => void; disabled?: boolean; + onClick: (subject: string) => void; + onRemove?: (subject: string) => void; } -export function DriveRow({ subject, onClick, disabled }: DriveRowProps) { +export function DriveRow({ + subject, + disabled, + onClick, + onRemove, +}: DriveRowProps) { return ( <DriveRowWrapper> <TitleWrapper> + {onRemove && ( + <IconButton + title='Remove drive from list' + onClick={() => onRemove(subject)} + > + <FaTimes /> + </IconButton> + )} <ResourceInline subject={subject} /> </TitleWrapper> <Subject>{subject}</Subject> @@ -73,4 +89,11 @@ const TitleWrapper = styled.div` white-space: nowrap; text-overflow: ellipsis; font-weight: var(--title-font-weight); + display: flex; + align-items: center; + gap: 1ch; + + & svg { + color: ${p => p.theme.colors.textLight}; + } `; diff --git a/browser/data-browser/src/routes/SettingsServer/DrivesCard.tsx b/browser/data-browser/src/routes/SettingsServer/DrivesCard.tsx index 39db1fe3f..508734f77 100644 --- a/browser/data-browser/src/routes/SettingsServer/DrivesCard.tsx +++ b/browser/data-browser/src/routes/SettingsServer/DrivesCard.tsx @@ -1,20 +1,22 @@ import { NewInstanceButton } from '../../components/NewInstanceButton'; import { Card, CardInsideFull, CardRow } from '../../components/Card'; -import { urls } from '@tomic/react'; +import { server } from '@tomic/react'; import { styled } from 'styled-components'; import { useSettings } from '../../helpers/AppSettings'; import { DriveRow } from './DriveRow'; export interface DriveCardProps { drives: string[]; - onDriveSelect: (drive: string) => void; showNewOption?: boolean; + onDriveSelect: (drive: string) => void; + onDriveRemove?: (drive: string) => void; } export function DrivesCard({ drives, - onDriveSelect, showNewOption, + onDriveSelect, + onDriveRemove, }: DriveCardProps): JSX.Element { const { drive } = useSettings(); @@ -30,8 +32,9 @@ export function DrivesCard({ <CardRow key={subject} noBorder={i === 0}> <DriveRow subject={subject} - onClick={onDriveSelect} disabled={subject === drive} + onRemove={onDriveRemove} + onClick={onDriveSelect} /> </CardRow> ); @@ -39,7 +42,7 @@ export function DrivesCard({ {showNewOption && ( <CardRow> <StyledNewInstanceButton - klass={urls.classes.drive} + klass={server.classes.drive} subtle icon label='New Drive' @@ -53,7 +56,7 @@ export function DrivesCard({ const ContainerCard = styled(Card)` container-type: inline-size; - padding-top: 0; + padding-block: 0; `; const StyledNewInstanceButton = styled(NewInstanceButton)` diff --git a/browser/data-browser/src/routes/SettingsServer/index.tsx b/browser/data-browser/src/routes/SettingsServer/index.tsx index 8bfe403e4..fd7ce7dd2 100644 --- a/browser/data-browser/src/routes/SettingsServer/index.tsx +++ b/browser/data-browser/src/routes/SettingsServer/index.tsx @@ -25,7 +25,8 @@ export function SettingsServer(): JSX.Element { const [savedDrives] = useSavedDrives(); - const [history, addDriveToHistory] = useDriveHistory(savedDrives); + const [history, addDriveToHistory, removeFromHistory] = + useDriveHistory(savedDrives); function handleSetBaseUrl(url: string) { try { @@ -71,6 +72,7 @@ export function SettingsServer(): JSX.Element { <DrivesCard drives={history} onDriveSelect={subject => handleSetBaseUrl(subject)} + onDriveRemove={subject => removeFromHistory(subject)} /> </Column> </ContainerWide> diff --git a/browser/data-browser/src/routes/ShareRoute.tsx b/browser/data-browser/src/routes/ShareRoute.tsx index ee7c80356..5d9135e95 100644 --- a/browser/data-browser/src/routes/ShareRoute.tsx +++ b/browser/data-browser/src/routes/ShareRoute.tsx @@ -15,6 +15,7 @@ import { useNavigate } from 'react-router-dom'; import { ErrorLook } from '../components/ErrorLook'; import { Column } from '../components/Row'; import { Main } from '../components/Main'; +import { FaShare } from 'react-icons/fa6'; /** Form for managing and viewing rights for this resource */ export function ShareRoute(): JSX.Element { @@ -138,13 +139,14 @@ export function ShareRoute(): JSX.Element { {canWrite && !showInviteForm && ( <span> <Button onClick={() => setShowInviteForm(true)}> - Send Invite... + <FaShare /> + Create Invite </Button> </span> )} {showInviteForm && <InviteForm target={resource} />} <Card> - <RightsHeader text='rights set here:' /> + <RightsHeader text='Rights set here:' /> <CardInsideFull> {/* This key might be a bit too much, but the component wasn't properly re-rendering before */} {constructAgentProps().map(right => ( @@ -171,7 +173,7 @@ export function ShareRoute(): JSX.Element { {err && <ErrorLook>{err.message}</ErrorLook>} {inheritedRights.length > 0 && ( <Card> - <RightsHeader text='inherited rights:' /> + <RightsHeader text='Inherited rights:' /> <CardInsideFull> {inheritedRights.map(right => ( <AgentRights diff --git a/browser/data-browser/src/styling.tsx b/browser/data-browser/src/styling.tsx index 2363e1efc..943380f29 100644 --- a/browser/data-browser/src/styling.tsx +++ b/browser/data-browser/src/styling.tsx @@ -95,6 +95,7 @@ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => { textLight2: darkMode ? darken(0.8)(text) : lighten(0.8)(text), alert: '#cf5b5b', alertLight: '#e66f6f', + warning: '#f5a623', }, animation: { duration: `${animationDuration}ms`, @@ -159,6 +160,7 @@ declare module 'styled-components' { /** Error / warning color */ alert: string; alertLight: string; + warning: string; }; animation: { duration: string; diff --git a/browser/data-browser/src/views/Article/ArticlePage.tsx b/browser/data-browser/src/views/Article/ArticlePage.tsx index 5b8e22aea..4b8f968aa 100644 --- a/browser/data-browser/src/views/Article/ArticlePage.tsx +++ b/browser/data-browser/src/views/Article/ArticlePage.tsx @@ -1,7 +1,6 @@ import { - classes, - getTimestampNow, - properties, + commits, + dataBrowser, useCanWrite, useChildren, useString, @@ -18,27 +17,19 @@ import ResourceCard from '../Card/ResourceCard'; import { ResourcePageProps } from '../ResourcePage'; import { ArticleCover } from './ArticleCover'; import { ArticleDescription } from './ArticleDescription'; -import { useCreateAndNavigate } from '../../hooks/useCreateAndNavigate'; +import { useNewResourceUI } from '../../components/forms/NewForm/useNewResourceUI'; export function ArticlePage({ resource }: ResourcePageProps): JSX.Element { - const [lastCommit] = useString(resource, properties.commit.lastCommit); + const [lastCommit] = useString(resource, commits.properties.lastCommit); const [canEdit] = useCanWrite(resource); const children = useChildren(resource); - const createAndNavigate = useCreateAndNavigate(); + const showNewResourceUI = useNewResourceUI(); const createNewArticle = useCallback(() => { - createAndNavigate( - classes.article, - { - [properties.name]: 'New Article', - [properties.publishedAt]: getTimestampNow(), - [properties.description]: '', - }, - resource.getSubject(), - ); - }, [createAndNavigate]); + showNewResourceUI(dataBrowser.classes.article, resource.subject); + }, [showNewResourceUI, resource]); return ( <> diff --git a/browser/data-browser/src/views/ChatRoomPage.tsx b/browser/data-browser/src/views/ChatRoomPage.tsx index 8468f8cf0..263443c03 100644 --- a/browser/data-browser/src/views/ChatRoomPage.tsx +++ b/browser/data-browser/src/views/ChatRoomPage.tsx @@ -74,7 +74,7 @@ export function ChatRoomPage({ resource }: ResourcePageProps) { e && e.preventDefault(); if (!disableSend) { - const subject = store.createSubject('messages', resource.getSubject()); + const subject = store.createSubject(resource.subject); const msgResource = await store.newResource({ subject, diff --git a/browser/data-browser/src/views/DocumentPage.tsx b/browser/data-browser/src/views/DocumentPage.tsx index 547b5c081..d64c1dbee 100644 --- a/browser/data-browser/src/views/DocumentPage.tsx +++ b/browser/data-browser/src/views/DocumentPage.tsx @@ -186,7 +186,7 @@ function DocumentPageEdit({ async function addElement(position: number) { // When an element is created, it should be a Resource that has this document as its parent. // or maybe a nested resource? - const elementSubject = store.createSubject('element', resource.subject); + const elementSubject = store.createSubject(resource.subject); const newElements = [...elements]; newElements.splice(position, 0, elementSubject); diff --git a/browser/data-browser/src/views/DrivePage.tsx b/browser/data-browser/src/views/DrivePage.tsx index 4b11103d8..8c725b7b0 100644 --- a/browser/data-browser/src/views/DrivePage.tsx +++ b/browser/data-browser/src/views/DrivePage.tsx @@ -1,4 +1,12 @@ -import { useArray, properties, Datatype } from '@tomic/react'; +import { + useArray, + Datatype, + dataBrowser, + core, + server, + useProperty, + useCanWrite, +} from '@tomic/react'; import { ContainerNarrow } from '../components/Containers'; import { Card, CardInsideFull, CardRow } from '../components/Card'; import { ResourceInline } from './ResourceInline'; @@ -10,59 +18,80 @@ import { FaPlus } from 'react-icons/fa'; import { paths } from '../routes/paths'; import { ResourcePageProps } from './ResourcePage'; import { EditableTitle } from '../components/EditableTitle'; -import { Row } from '../components/Row'; +import { Column, Row } from '../components/Row'; +import { styled } from 'styled-components'; +import InputSwitcher from '../components/forms/InputSwitcher'; /** A View for Drives, which function similar to a homepage or dashboard. */ function DrivePage({ resource }: ResourcePageProps): JSX.Element { - const [subResources] = useArray(resource, properties.subResources); + const [subResources] = useArray( + resource, + dataBrowser.properties.subResources, + ); const { drive: baseURL, setDrive: setBaseURL } = useSettings(); + const defaultOntologyProp = useProperty(server.properties.defaultOntology); + const [canEdit] = useCanWrite(resource); + if (!baseURL) { - setBaseURL(resource.getSubject()); + setBaseURL(resource.subject); } return ( <ContainerNarrow> - <Row> - <EditableTitle resource={resource} /> - {baseURL !== resource.getSubject() && ( - <Button onClick={() => setBaseURL(resource.getSubject())}> - Set as current drive - </Button> - )} - </Row> - <ValueForm - resource={resource} - propertyURL={properties.description} - datatype={Datatype.MARKDOWN} - /> - <Card> - <h3>resources:</h3> - <CardInsideFull> - {subResources.map(child => { - return ( + <Column> + <Row> + <EditableTitle resource={resource} /> + {baseURL !== resource.subject && ( + <Button onClick={() => setBaseURL(resource.subject)}> + Set as current drive + </Button> + )} + </Row> + <ValueForm + resource={resource} + propertyURL={core.properties.description} + datatype={Datatype.MARKDOWN} + /> + <div> + <Heading>Default Ontology</Heading> + <InputSwitcher + commit + resource={resource} + property={defaultOntologyProp} + disabled={!canEdit} + /> + </div> + <Card> + <Heading>Resources:</Heading> + <CardInsideFull> + {subResources.map(child => ( <CardRow key={child}> <ResourceInline subject={child} /> </CardRow> - ); - })} - <CardRow> - <AtomicLink path={paths.new}> - <FaPlus /> Create new resource - </AtomicLink> - </CardRow> - </CardInsideFull> - </Card> - {baseURL.startsWith('http://localhost') && ( - <p> - You are running Atomic-Server on `localhost`, which means that it will - not be available from any other machine than your current local - device. If you want your Atomic-Server to be available from the web, - you should set this up at a Domain on a server. - </p> - )} + ))} + <CardRow> + <AtomicLink path={paths.new}> + <FaPlus /> Create new resource + </AtomicLink> + </CardRow> + </CardInsideFull> + </Card> + {baseURL.startsWith('http://localhost') && ( + <p> + You are running Atomic-Server on `localhost`, which means that it + will not be available from any other machine than your current local + device. If you want your Atomic-Server to be available from the web, + you should set this up at a Domain on a server. + </p> + )} + </Column> </ContainerNarrow> ); } export default DrivePage; + +const Heading = styled.h2` + font-size: 1.3rem; +`; diff --git a/browser/data-browser/src/views/Element.tsx b/browser/data-browser/src/views/Element.tsx index 276962676..c429b9177 100644 --- a/browser/data-browser/src/views/Element.tsx +++ b/browser/data-browser/src/views/Element.tsx @@ -170,7 +170,7 @@ export function ElementEdit({ onFocus={() => setCurrent(index!)} onBlur={() => setCurrent(-1)} > - <Markdown text={text || ''} noMargin /> + <Markdown text={text || ''} /> <Err /> </ElementWrapper> ); @@ -231,7 +231,7 @@ export function ElementShow({ subject }: ElementShowProps): JSX.Element { return ( <ElementWrapper> - <Markdown text={text || ''} noMargin /> + <Markdown text={text || ''} /> </ElementWrapper> ); } diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx index 747489423..02e5f8cdf 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx @@ -1,15 +1,16 @@ -import { urls, useArray, useResource, useString } from '@tomic/react'; +import { core, useArray, useResource, useString } from '@tomic/react'; import { Card } from '../../../components/Card'; import { PropertyLineRead } from '../Property/PropertyLineRead'; import { styled } from 'styled-components'; import { FaCube } from 'react-icons/fa'; -import { Column } from '../../../components/Row'; +import { Column, Row } from '../../../components/Row'; import Markdown from '../../../components/datatypes/Markdown'; import { AtomicLink } from '../../../components/AtomicLink'; import { toAnchorId } from '../toAnchorId'; import { ViewTransitionProps } from '../../../helpers/ViewTransitionProps'; import { transitionName } from '../../../helpers/transitionName'; +import { NewClassInstanceButton } from './NewClassInstanceButton'; interface ClassCardReadProps { subject: string; @@ -17,17 +18,20 @@ interface ClassCardReadProps { export function ClassCardRead({ subject }: ClassCardReadProps): JSX.Element { const resource = useResource(subject); - const [description] = useString(resource, urls.properties.description); - const [requires] = useArray(resource, urls.properties.requires); - const [recommends] = useArray(resource, urls.properties.recommends); + const [description] = useString(resource, core.properties.description); + const [requires] = useArray(resource, core.properties.requires); + const [recommends] = useArray(resource, core.properties.recommends); return ( <StyledCard subject={subject}> <Column> - <StyledH3 id={toAnchorId(subject)}> - <FaCube /> - <AtomicLink subject={subject}>{resource.title}</AtomicLink> - </StyledH3> + <Row center justify='space-between'> + <StyledH3 id={toAnchorId(subject)}> + <FaCube /> + <AtomicLink subject={subject}>{resource.title}</AtomicLink> + </StyledH3> + <NewClassInstanceButton resource={resource} /> + </Row> <Markdown text={description ?? ''} maxLength={1500} /> <StyledH4>Requires</StyledH4> <StyledTable> diff --git a/browser/data-browser/src/views/OntologyPage/Class/NewClassInstanceButton.tsx b/browser/data-browser/src/views/OntologyPage/Class/NewClassInstanceButton.tsx new file mode 100644 index 000000000..3eada6900 --- /dev/null +++ b/browser/data-browser/src/views/OntologyPage/Class/NewClassInstanceButton.tsx @@ -0,0 +1,91 @@ +import { Core, Resource } from '@tomic/react'; +import { FaPlus, FaAtom, FaTable } from 'react-icons/fa'; +import { DropdownItem, DropdownMenu } from '../../../components/Dropdown'; +import { buildDefaultTrigger } from '../../../components/Dropdown/DefaultTrigger'; +import { useState } from 'react'; +import { ParentPickerDialog } from '../../../components/ParentPicker/ParentPickerDialog'; +import { useNewResourceUI } from '../../../components/forms/NewForm/useNewResourceUI'; +import { NewTableDialog } from '../../../components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog'; +import { styled } from 'styled-components'; + +interface NewClassInstanceButtonProps { + resource: Resource<Core.Class>; +} + +enum InstanceType { + SingleInstance, + Table, +} + +export function NewClassInstanceButton({ + resource, +}: NewClassInstanceButtonProps): React.JSX.Element { + const showNewResourceUI = useNewResourceUI(); + + const [showParentPicker, setShowParentPicker] = useState(false); + const [showTableDialogWithParent, setShowTableDialogWithParent] = + useState<string>(); + const [instanceType, setInstanceType] = useState<InstanceType>(); + + const handleSelect = (parent: string) => { + if (instanceType === InstanceType.SingleInstance) { + showNewResourceUI(resource.subject, parent); + } else { + setShowTableDialogWithParent(parent); + } + + setInstanceType(undefined); + }; + + const onTableDialogClose = () => { + setShowTableDialogWithParent(undefined); + }; + + const trigger = buildDefaultTrigger( + <PlusIcon />, + `New instance of ${resource.title}`, + ); + + const newInstanceItems: DropdownItem[] = [ + { + id: 'new-instance', + label: 'Single instance', + icon: <FaAtom />, + onClick: () => { + setInstanceType(InstanceType.SingleInstance); + setShowParentPicker(true); + }, + }, + { + id: 'new-table', + label: 'Table', + icon: <FaTable />, + onClick: () => { + setInstanceType(InstanceType.Table); + setShowParentPicker(true); + }, + }, + ]; + + return ( + <> + <DropdownMenu items={newInstanceItems} trigger={trigger} /> + <ParentPickerDialog + open={showParentPicker} + onOpenChange={setShowParentPicker} + onSelect={handleSelect} + /> + {showTableDialogWithParent && ( + <NewTableDialog + parent={showTableDialogWithParent} + initialExistingClass={resource.subject} + onClose={onTableDialogClose} + /> + )} + </> + ); +} + +const PlusIcon = styled(FaPlus)` + color: ${p => p.theme.colors.textLight}; +`; diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 2aa01cfb1..65afb4f42 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -1,5 +1,5 @@ import { ResourcePageProps } from '../ResourcePage'; -import { urls, useArray, useCanWrite } from '@tomic/react'; +import { core, useArray, useCanWrite } from '@tomic/react'; import { OntologySidebar } from './OntologySidebar'; import { styled } from 'styled-components'; import { ClassCardRead } from './Class/ClassCardRead'; @@ -19,9 +19,9 @@ import { CreateInstanceButton } from './CreateInstanceButton'; import { useState } from 'react'; export function OntologyPage({ resource }: ResourcePageProps) { - const [classes] = useArray(resource, urls.properties.classes); - const [properties] = useArray(resource, urls.properties.properties); - const [instances] = useArray(resource, urls.properties.instances); + const [classes] = useArray(resource, core.properties.classes); + const [properties] = useArray(resource, core.properties.properties); + const [instances] = useArray(resource, core.properties.instances); const [canWrite] = useCanWrite(resource); const [editMode, setEditMode] = useState(false); diff --git a/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx b/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx index 050a2e6b2..612ab894b 100644 --- a/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx +++ b/browser/data-browser/src/views/ResourceInline/ResourceInline.tsx @@ -1,4 +1,14 @@ -import { useString, useResource, urls, Client, useArray } from '@tomic/react'; +import { + useString, + useResource, + Client, + useArray, + isAtomicError, + ErrorType, + core, + dataBrowser, + server, +} from '@tomic/react'; import { AtomicLink } from '../../components/AtomicLink'; import { ErrorLook } from '../../components/ErrorLook'; import { LoaderInline } from '../../components/Loader'; @@ -15,6 +25,23 @@ type ResourceInlineProps = { basic?: boolean; } & ResourceInlineInstanceProps; +function getMessageForErrorType(error: Error) { + if (isAtomicError(error)) { + switch (error.type) { + case ErrorType.NotFound: + return 'Resource not found'; + case ErrorType.Unauthorized: + return 'Unauthorized'; + case ErrorType.Server: + return 'Server error'; + case ErrorType.Client: + return 'Something went wrong'; + } + } else { + return 'Error loading resource'; + } +} + /** Renders a Resource in a compact, inline link. Shows tooltip on hover. */ export function ResourceInline({ subject, @@ -23,7 +50,7 @@ export function ResourceInline({ className, }: ResourceInlineProps): JSX.Element { const resource = useResource(subject, { allowIncomplete: true }); - const [isA] = useArray(resource, urls.properties.isA); + const [isA] = useArray(resource, core.properties.isA); const Comp = basic ? DefaultInline : classMap.get(isA[0]) ?? DefaultInline; @@ -35,7 +62,7 @@ export function ResourceInline({ return ( <AtomicLink subject={subject} untabbable={untabbable}> <ErrorLook about={subject} title={resource.error.message}> - Unknown Resource + {getMessageForErrorType(resource.error)} </ErrorLook> </AtomicLink> ); @@ -58,7 +85,7 @@ export function ResourceInline({ function DefaultInline({ subject }: ResourceInlineInstanceProps): JSX.Element { const resource = useResource(subject); - const [description] = useString(resource, urls.properties.description); + const [description] = useString(resource, core.properties.description); return <span title={description ? description : ''}>{resource.title}</span>; } @@ -67,6 +94,6 @@ const classMap = new Map< string, (props: ResourceInlineInstanceProps) => JSX.Element >([ - [urls.classes.tag, TagInline], - [urls.classes.file, FileInline], + [dataBrowser.classes.tag, TagInline], + [server.classes.file, FileInline], ]); diff --git a/browser/data-browser/src/views/TablePage/NewColumnButton.tsx b/browser/data-browser/src/views/TablePage/NewColumnButton.tsx index 4e11deb0a..7b0ec35ae 100644 --- a/browser/data-browser/src/views/TablePage/NewColumnButton.tsx +++ b/browser/data-browser/src/views/TablePage/NewColumnButton.tsx @@ -1,7 +1,7 @@ import { Datatype, useResource } from '@tomic/react'; import { useCallback, useContext, useMemo, useState } from 'react'; import { FaChevronCircleDown, FaFile, FaHashtag, FaPlus } from 'react-icons/fa'; -import { DIVIDER, DropdownMenu, Item } from '../../components/Dropdown'; +import { DIVIDER, DropdownMenu, DropdownItem } from '../../components/Dropdown'; import { buildDefaultTrigger } from '../../components/Dropdown/DefaultTrigger'; import { dataTypeIconMap } from './dataTypeMaps'; import { NewPropertyDialog } from './PropertyForm/NewPropertyDialog'; @@ -34,7 +34,7 @@ export function NewColumnButton(): JSX.Element { [], ); - const items = useMemo((): Item[] => { + const items = useMemo((): DropdownItem[] => { return [ { id: 'text', diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx index 7bc61415a..a2f1d61ba 100644 --- a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx +++ b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx @@ -1,4 +1,5 @@ import { + Core, Datatype, Resource, Store, @@ -22,7 +23,7 @@ import { PropertyForm, PropertyFormCategory } from './PropertyForm'; interface NewPropertyDialogProps { showDialog: boolean; - tableClassResource: Resource; + tableClassResource: Resource<Core.Class>; bindShow: React.Dispatch<boolean>; selectedCategory?: string; } @@ -35,10 +36,10 @@ const createSubjectWithBase = (base: string) => { const populatePropertyWithDefaults = async ( property: Resource, - tableClass: Resource, + tableClass: Resource<Core.Class>, ) => { await property.set(core.properties.isA, [core.classes.property]); - await property.set(core.properties.parent, tableClass.subject); + await property.set(core.properties.parent, tableClass.props.parent); await property.set(core.properties.shortname, 'new-column', false); await property.set(core.properties.name, '', false); await property.set(core.properties.description, 'A column in a table'); @@ -108,9 +109,24 @@ export function NewPropertyDialog({ return; } + const tableClassParent = await store.getResource( + tableClassResource.props.parent, + ); + + if (tableClassParent.hasClasses(core.classes.ontology)) { + await resource.set(core.properties.parent, tableClassParent.subject); + + tableClassParent.push( + core.properties.properties, + [resource.subject], + true, + ); + + await tableClassParent.save(); + } + await resource.save(); await saveChildren(store, resource); - await store.notifyResourceManuallyCreated(resource); pushProp([resource.subject]); setResource(null); diff --git a/browser/data-browser/src/views/TablePage/TableCell.tsx b/browser/data-browser/src/views/TablePage/TableCell.tsx index 3756e8f7c..03da41808 100644 --- a/browser/data-browser/src/views/TablePage/TableCell.tsx +++ b/browser/data-browser/src/views/TablePage/TableCell.tsx @@ -1,19 +1,12 @@ import { + commits, JSONValue, Property, Resource, - urls, useDebouncedCallback, useValue, } from '@tomic/react'; -import { - startTransition, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { Cell } from '../../components/TableEditor'; import { CellAlign } from '../../components/TableEditor/Cell'; import { @@ -34,7 +27,7 @@ interface TableCell { rowIndex: number; resource: Resource; property: Property; - invalidateTable?: () => void; + onEditNextRow?: () => void; } function useIsEditing(row: number, column: number) { @@ -59,9 +52,8 @@ export function TableCell({ rowIndex, resource, property, - invalidateTable, + onEditNextRow, }: TableCell): JSX.Element { - const [markForInvalidate, setMarkForInvalidate] = useState(false); const { setActiveCell } = useTableEditorContext(); const { addItemsToHistoryStack } = useContext(TablePageContext); const [save, savePending] = useDebouncedCallback( @@ -75,7 +67,7 @@ export function TableCell({ const [createdAt, setCreatedAt] = useValue( resource, - urls.properties.commit.createdAt, + commits.properties.createdAt, { commit: false, commitDebounce: 0 }, ); @@ -93,7 +85,6 @@ export function TableCell({ async (v: JSONValue) => { if (!createdAt) { await setCreatedAt(Date.now()); - setMarkForInvalidate(true); } addItemsToHistoryStack( @@ -115,32 +106,16 @@ export function TableCell({ ); const handleEditNextRow = useCallback(() => { - if (markForInvalidate && !savePending) { - startTransition(() => { - setMarkForInvalidate(false); - invalidateTable?.(); - setTimeout(() => { - setActiveCell(rowIndex + 1, columnIndex); - }, 0); - }); - } - }, [ - markForInvalidate, - savePending, - invalidateTable, - setActiveCell, - rowIndex, - columnIndex, - ]); - - useEffect(() => { - if (markForInvalidate && !isEditing && !savePending) { - startTransition(() => { - setMarkForInvalidate(false); - invalidateTable?.(); - }); + if (!savePending) { + onEditNextRow?.(); + + // Only go to the next row if the resource has any properties set (It has two by default, isA and parent) + // This prevents triggering a rerender and losing focus on the input. + if (resource.getPropVals().size > 2) { + setActiveCell(rowIndex + 1, columnIndex); + } } - }, [isEditing, markForInvalidate, savePending, invalidateTable]); + }, [savePending, setActiveCell, rowIndex, columnIndex]); return ( <Cell diff --git a/browser/data-browser/src/views/TablePage/TableHeading.tsx b/browser/data-browser/src/views/TablePage/TableHeading.tsx index 4a2aa970b..78de37c0e 100644 --- a/browser/data-browser/src/views/TablePage/TableHeading.tsx +++ b/browser/data-browser/src/views/TablePage/TableHeading.tsx @@ -1,4 +1,5 @@ import { + Core, Datatype, Property, Resource, @@ -43,12 +44,16 @@ export const TableHeading: TableHeadingComponent<Property> = ({ const propResource = useResource(column.subject); const [title] = useTitle(propResource); - const { setSortBy, sorting } = useContext(TablePageContext); + const { setSortBy, sorting, tableClassSubject } = + useContext(TablePageContext); + const tableClass = useResource<Core.Class>(tableClassSubject); + + const isRequired = (tableClass.props.requires ?? []).includes(column.subject); const Icon = getIcon(propResource, sorting, hoverOrFocus, column.datatype); - const isSorted = sorting.prop === propResource.getSubject(); + const isSorted = sorting.prop === propResource.subject; - const text = title || column.shortname; + const text = `${title || column.shortname}${isRequired ? '*' : ''}`; return ( <> @@ -62,7 +67,7 @@ export const TableHeading: TableHeadingComponent<Property> = ({ <Icon title='Drag column' /> </DragIconButton> <NameButton - onClick={() => setSortBy(propResource.getSubject())} + onClick={() => setSortBy(propResource.subject)} bold={isSorted} title={text} > diff --git a/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx b/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx index ced40b999..0807b938d 100644 --- a/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx +++ b/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx @@ -6,10 +6,10 @@ import { useString, core, } from '@tomic/react'; -import { useCallback, useContext, useMemo, useState } from 'react'; -import { DropdownMenu, Item } from '../../components/Dropdown'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { DropdownMenu, DropdownItem } from '../../components/Dropdown'; import { buildDefaultTrigger } from '../../components/Dropdown/DefaultTrigger'; -import { FaEdit, FaEllipsisV, FaEye, FaTimes, FaTrash } from 'react-icons/fa'; +import { FaEdit, FaEllipsisV, FaEye, FaTimes } from 'react-icons/fa'; import { styled } from 'styled-components'; import { EditPropertyDialog } from './PropertyForm/EditPropertyDialog'; import { TablePageContext } from './tablePageContext'; @@ -21,6 +21,8 @@ import { ResourceInline } from '../ResourceInline'; import { ResourceUsage } from '../../components/ResourceUsage'; import { useNavigate } from 'react-router'; import { constructOpenURL } from '../../helpers/navigation'; +import { Checkbox, CheckboxLabel } from '../../components/forms/Checkbox'; +import { Column } from '../../components/Row'; interface TableHeadingMenuProps { resource: Resource; @@ -38,11 +40,14 @@ const useIsExternalProperty = (property: Resource) => { export function TableHeadingMenu({ resource, }: TableHeadingMenuProps): JSX.Element { - const canWrite = useCanWrite(resource); const [showEditDialog, setShowEditDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [fullDelete, setFullDelete] = useState(false); + const { tableClassSubject } = useContext(TablePageContext); const tableClassResource = useResource(tableClassSubject); + const [canWriteClass] = useCanWrite(tableClassResource); + const [canWriteProperty] = useCanWrite(resource); const navigate = useNavigate(); const isExternalProperty = useIsExternalProperty(resource); @@ -82,45 +87,40 @@ export function TableHeadingMenu({ } }, [deleteProperty, removeProperty, isExternalProperty]); - const items = useMemo((): Item[] => { - const initialItems = [ - { - id: 'edit', - label: 'Edit', - onClick: () => setShowEditDialog(true), - icon: <FaEdit />, - disabled: !canWrite || isExternalProperty, - }, + const items = useMemo( + (): DropdownItem[] => [ { id: 'view', label: 'View', onClick: () => { - navigate(constructOpenURL(resource.getSubject())); + navigate(constructOpenURL(resource.subject)); }, icon: <FaEye />, }, - ]; - - if (isExternalProperty) { - initialItems.push({ + { + id: 'edit', + label: 'Edit', + onClick: () => setShowEditDialog(true), + icon: <FaEdit />, + disabled: !canWriteProperty, + }, + { id: 'remove', label: 'Remove', onClick: () => setShowDeleteDialog(true), icon: <FaTimes />, - disabled: !canWrite, - }); - } else { - initialItems.push({ - id: 'delete', - label: 'Delete', - onClick: () => setShowDeleteDialog(true), - icon: <FaTrash />, - disabled: !canWrite, - }); - } + disabled: !canWriteClass, + }, + ], + [canWriteClass, canWriteProperty, navigate, resource], + ); - return initialItems; - }, []); + // Reset fullDelete when dialog is closed + useEffect(() => { + if (!showDeleteDialog) { + setFullDelete(false); + } + }, [showDeleteDialog]); return ( <Wrapper> @@ -131,30 +131,24 @@ export function TableHeadingMenu({ bindShow={setShowEditDialog} /> <ConfirmationDialog - title={isExternalProperty ? 'Remove column' : 'Delete column'} - confirmLabel={isExternalProperty ? 'Remove' : 'Delete'} + title={fullDelete ? 'Delete property' : 'Remove column'} + confirmLabel={fullDelete ? 'Delete' : 'Remove'} show={showDeleteDialog} bindShow={setShowDeleteDialog} theme={ConfirmationDialogTheme.Alert} onConfirm={onConfirm} > - {isExternalProperty ? ( + <Column> <p> - Remove <ResourceInline subject={resource.getSubject()} /> from this - table + Remove <ResourceInline subject={resource.subject} /> from{' '} + <ResourceInline subject={tableClassSubject} /> </p> - ) : ( - <> - <p> - Are you sure you want to delete this column? - <br /> - This will delete the{' '} - <ResourceInline subject={resource.getSubject()} /> property and - its children. - </p> - <ResourceUsage resource={resource} /> - </> - )} + <ResourceUsage resource={resource} /> + <CheckboxLabel> + <Checkbox checked={fullDelete} onChange={setFullDelete} /> + Delete property and its children + </CheckboxLabel> + </Column> </ConfirmationDialog> </Wrapper> ); diff --git a/browser/data-browser/src/views/TablePage/TableRow.tsx b/browser/data-browser/src/views/TablePage/TableRow.tsx index 608a3b18c..8ade88d3f 100644 --- a/browser/data-browser/src/views/TablePage/TableRow.tsx +++ b/browser/data-browser/src/views/TablePage/TableRow.tsx @@ -12,6 +12,9 @@ import { import { TableCell } from './TableCell'; import { randomSubject } from '../../helpers/randomString'; import { styled, keyframes } from 'styled-components'; +import { useTableEditorContext } from '../../components/TableEditor/TableEditorContext'; +import { FaTriangleExclamation } from 'react-icons/fa6'; +import { useTableInvalidation } from './useTableInvalidation'; interface TableRowProps { collection: Collection; @@ -19,8 +22,40 @@ interface TableRowProps { columns: Property[]; } +const WarningIcon = styled(FaTriangleExclamation)` + color: ${p => p.theme.colors.warning}; +`; + +const saveWarning = ( + <WarningIcon title='Row is incomplete or has invalid data' /> +); + const TableCellMemo = memo(TableCell); +function useMarkings(row: Resource, index: number) { + const { setMarkings } = useTableEditorContext(); + + useEffect(() => { + if (row.commitError) { + setMarkings(markings => { + const newMap = new Map(markings); + newMap.set(index, saveWarning); + + return newMap; + }); + } + + return () => { + setMarkings(markings => { + const newMap = new Map(markings); + newMap.delete(index); + + return newMap; + }); + }; + }, [row, index]); +} + export function TableRow({ collection, index, @@ -28,6 +63,8 @@ export function TableRow({ }: TableRowProps): JSX.Element { const resource = useMemberFromCollection(collection, index); + useMarkings(resource, index); + if (resource.subject === unknownSubject) { return ( <> @@ -76,6 +113,10 @@ export function TableNewRow({ const resource = useResource(subject, resourceOpts); + const onEditNextRow = useTableInvalidation(resource, invalidateTable); + + useMarkings(resource, index); + useEffect(() => { if (resource.subject === unknownSubject) { return; @@ -108,7 +149,7 @@ export function TableNewRow({ columnIndex={cIndex + 1} resource={resource} property={column} - invalidateTable={invalidateTable} + onEditNextRow={onEditNextRow} /> ))} </> diff --git a/browser/data-browser/src/views/TablePage/useTableInvalidation.ts b/browser/data-browser/src/views/TablePage/useTableInvalidation.ts new file mode 100644 index 000000000..457f3c634 --- /dev/null +++ b/browser/data-browser/src/views/TablePage/useTableInvalidation.ts @@ -0,0 +1,46 @@ +import { Resource, StoreEvents, useStore } from '@tomic/react'; +import { useCallback, useEffect, useState } from 'react'; +import { + CursorMode, + useTableEditorContext, +} from '../../components/TableEditor/TableEditorContext'; +import { useOnValueChange } from '../../helpers/useOnValueChange'; + +export function useTableInvalidation( + resource: Resource, + invalidateTable: () => void, +) { + const store = useStore(); + const { cursorMode, selectedColumn, selectedRow } = useTableEditorContext(); + + const [markedForInvalidation, setMarkedForInvalidation] = useState(false); + + const onEnter = useCallback(() => { + if (markedForInvalidation) { + invalidateTable(); + } + }, [invalidateTable, markedForInvalidation]); + + useOnValueChange(() => { + if (markedForInvalidation) { + invalidateTable(); + } + }, [selectedRow, selectedColumn]); + + useEffect(() => { + if (markedForInvalidation && cursorMode !== CursorMode.Edit) { + invalidateTable(); + } + }, [markedForInvalidation, cursorMode]); + + // The first time a resource is saved, mark it for invalidation + useEffect(() => { + return store.on(StoreEvents.ResourceSaved, r => { + if (!markedForInvalidation && r.subject === resource.subject) { + setMarkedForInvalidation(true); + } + }); + }, [resource, store, markedForInvalidation]); + + return onEnter; +} diff --git a/browser/data-browser/tsconfig.json b/browser/data-browser/tsconfig.json index 928b4acb0..5f3bcb8ca 100644 --- a/browser/data-browser/tsconfig.json +++ b/browser/data-browser/tsconfig.json @@ -15,7 +15,6 @@ }, "include": [ "./src", - "../e2e" ], "references": [ { diff --git a/browser/e2e/package.json b/browser/e2e/package.json index 79f01d83f..3662b83c2 100644 --- a/browser/e2e/package.json +++ b/browser/e2e/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atomicdata-dev/atomic-server/" }, "devDependencies": { - "@playwright/test": "^1.37.0" + "@playwright/test": "^1.43.1" }, "scripts": { "playwright-install": "playwright install chromium", diff --git a/browser/e2e/tests/e2e.spec.ts b/browser/e2e/tests/e2e.spec.ts index a862b1335..6caaf621a 100644 --- a/browser/e2e/tests/e2e.spec.ts +++ b/browser/e2e/tests/e2e.spec.ts @@ -45,7 +45,7 @@ test.describe('data-browser', async () => { // TODO: this keeps hanging. How do I make sure something is _not_ visible? // await expect(page.locator('text=new resource')).not.toBeVisible(); await page.click('[data-test="sidebar-toggle"]'); - await expect(page.locator(currentDriveTitle)).toBeVisible(); + await expect(currentDriveTitle(page)).toBeVisible(); }); test('switch Server URL', async ({ page }) => { @@ -164,7 +164,7 @@ test.describe('data-browser', async () => { // Remove public read rights for Drive await signIn(page); const { driveURL, driveTitle } = await newDrive(page); - await page.click(currentDriveTitle); + await currentDriveTitle(page).click(); await contextMenuClick('share', page); expect(publicReadRightLocator(page)).not.toBeChecked(); @@ -178,9 +178,9 @@ test.describe('data-browser', async () => { await expect(page2.locator('text=Unauthorized').first()).toBeVisible(); // Create invite - await page.click('button:has-text("Send invite")'); - context.grantPermissions(['clipboard-read', 'clipboard-write']); await page.click('button:has-text("Create Invite")'); + context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.click('button:has-text("Create")'); await expect(page.locator('text=Invite created and copied ')).toBeVisible(); const inviteUrl = await page.evaluate(() => document @@ -330,8 +330,6 @@ test.describe('data-browser', async () => { test('drive switcher', async ({ page }) => { await signIn(page); - await page.locator(`${currentDriveTitle} > text=localhost`); - await page.click(sideBarDriveSwitcher); // temp disable for trailing slash // const dropdownId = await page @@ -350,21 +348,21 @@ test.describe('data-browser', async () => { test('configure drive page', async ({ page }) => { await signIn(page); await openConfigureDrive(page); - await expect(page.locator(currentDriveTitle)).toHaveText('localhost'); + await expect(currentDriveTitle(page)).toHaveText('localhost'); // temp disable this, because of trailing slash in base URL // await page.click(':text("https://atomicdata.dev") + button:text("Select")'); - // await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); + // await expect(currentDriveTitle(page)).toHaveText('Atomic Data'); await openConfigureDrive(page); await page.fill('[data-test="server-url-input"]', 'https://example.com'); await page.click('[data-test="server-url-save"]'); - await expect(page.locator(currentDriveTitle)).toHaveText('example.com'); + await expect(currentDriveTitle(page)).toHaveText('example.com'); await openConfigureDrive(page); await page.click(':text("https://atomicdata.dev") + button:text("Select")'); - await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); + await expect(currentDriveTitle(page)).toHaveText('Atomic Data'); await openConfigureDrive(page); }); diff --git a/browser/e2e/tests/filePicker.spec.ts b/browser/e2e/tests/filePicker.spec.ts index 5b70793df..b606a0a65 100644 --- a/browser/e2e/tests/filePicker.spec.ts +++ b/browser/e2e/tests/filePicker.spec.ts @@ -53,7 +53,7 @@ const createModel = async (page: Page) => { await page.getByRole('button', { name: 'add required property' }).click(); await page .getByPlaceholder('Search for a property or enter a URL') - .type('programming'); + .fill('programming'); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); @@ -131,7 +131,7 @@ test.describe('File Picker', () => { ).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText('Resource Saved')).toBeVisible(); + await expect(page.getByText('New robot')).not.toBeVisible(); } { @@ -157,8 +157,7 @@ test.describe('File Picker', () => { ).toBeVisible(); await page.getByRole('button', { name: 'Save' }).click(); - - await expect(page.getByText('Resource Saved')).toBeVisible(); + await expect(page.getByText('New robot')).not.toBeVisible(); await expect(page.getByText('testFile3.txt').nth(1)).toBeVisible(); await page.getByText('testFile3.txt').nth(1).click(); diff --git a/browser/e2e/tests/tables.spec.ts b/browser/e2e/tests/tables.spec.ts index 0175f3d96..7481c9055 100644 --- a/browser/e2e/tests/tables.spec.ts +++ b/browser/e2e/tests/tables.spec.ts @@ -45,10 +45,11 @@ test.describe('tables', async () => { await page.keyboard.press('Enter'); await expect( page.locator( - `[aria-rowindex="${rowIndex}"] > [aria-colindex="${2}"] > input`, + `[aria-rowindex="${rowIndex}"] > [aria-colindex="2"] > input`, ), ).toBeFocused(); await page.keyboard.type(name); + await page.waitForTimeout(300); await tab(); // Flay newline await page.waitForTimeout(300); @@ -86,7 +87,7 @@ test.describe('tables', async () => { ).toBeVisible(); await expect( page.locator( - `[aria-rowindex="${rowIndex + 1}"] > [aria-colindex="${2}"] > input`, + `[aria-rowindex="${rowIndex + 1}"] > [aria-colindex="2"] > input`, ), "Next row's first cell isn't focused", ).toBeFocused(); diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts index b9f75ff53..3602b6213 100644 --- a/browser/e2e/tests/test-utils.ts +++ b/browser/e2e/tests/test-utils.ts @@ -27,7 +27,8 @@ export const timestamp = () => new Date().toLocaleTimeString(); export const editableTitle = '[data-test="editable-title"]'; export const sideBarDriveSwitcher = '[title="Open Drive Settings"]'; export const sideBarNewResource = '[data-test="sidebar-new-resource"]'; -export const currentDriveTitle = '[data-test=current-drive-title]'; +export const currentDriveTitle = (page: Page) => + page.getByTestId('current-drive-title'); export const publicReadRightLocator = (page: Page) => page .locator( @@ -58,7 +59,7 @@ export const before = async ({ page }: { page: Page }) => { await changeDrive(SERVER_URL, page); } - await expect(page.locator(currentDriveTitle)).toBeVisible(); + await expect(currentDriveTitle(page)).toBeVisible(); }; export async function setTitle(page: Page, title: string) { @@ -101,20 +102,27 @@ export async function signIn(page: Page) { */ export async function newDrive(page: Page) { // Create new drive to prevent polluting the main drive + const driveTitle = `testdrive-${timestamp()}`; await page.locator(sideBarDriveSwitcher).click(); await page.locator('button:has-text("New Drive")').click(); - expect(page.locator(`${currentDriveTitle} > localhost`)).not.toBeVisible(); + await expect( + currentDialog(page).getByRole('heading', { name: 'New Drive' }), + ).toBeVisible(); + + await currentDialog(page).getByLabel('Name').fill(driveTitle); + + await currentDialog(page).getByRole('button', { name: 'Create' }).click(); + await expect(currentDriveTitle(page)).not.toHaveText('localhost'); + await expect(currentDriveTitle(page)).toHaveText(driveTitle); await expect(page.locator('text="Create new resource"')).toBeVisible(); const driveURL = await getCurrentSubject(page); expect(driveURL).toContain('localhost'); - const driveTitle = `testdrive-${timestamp()}`; - await editTitle(driveTitle, page); return { driveURL: driveURL as string, driveTitle }; } export async function makeDrivePublic(page: Page) { - await page.click(currentDriveTitle); + await currentDriveTitle(page).click(); await page.click(contextMenu); await page.click('button:has-text("share")'); await expect( @@ -187,7 +195,7 @@ export async function openAgentPage(page: Page) { export async function openAtomic(page: Page) { await changeDrive('https://atomicdata.dev', page); // Accept the invite, create an account if necessary - await expect(page.locator(currentDriveTitle)).toHaveText('Atomic Data'); + await expect(currentDriveTitle(page)).toHaveText('Atomic Data'); } /** Opens the users' profile, sets a username */ diff --git a/browser/lib/package.json b/browser/lib/package.json index c7d25bb72..07f19dca9 100644 --- a/browser/lib/package.json +++ b/browser/lib/package.json @@ -6,7 +6,8 @@ "@noble/hashes": "^0.5.7", "base64-arraybuffer": "^1.0.2", "cross-fetch": "^3.1.4", - "fast-json-stable-stringify": "^2.1.0" + "fast-json-stable-stringify": "^2.1.0", + "ulid": "^2.3.0" }, "description": "", "devDependencies": { @@ -36,7 +37,7 @@ "start": "pnpm watch", "test": "NODE_OPTIONS='--experimental-vm-modules' ../node_modules/jest/bin/jest.js", "tsc": "tsc --build", - "typecheck": "tsc --noEmit", + "typecheck": "pnpm exec tsc --noEmit", "watch": "tsc --build --watch" }, "source": "src/index.ts", diff --git a/browser/lib/src/error.ts b/browser/lib/src/error.ts index 9955661cf..a907b4421 100644 --- a/browser/lib/src/error.ts +++ b/browser/lib/src/error.ts @@ -20,6 +20,10 @@ export function isUnauthorized(error?: Error): boolean { return false; } +export function isAtomicError(error: Error): error is AtomicError { + return error instanceof AtomicError; +} + /** * Atomic Data Errors have an additional Type, which tells the client what kind * of error to render. diff --git a/browser/lib/src/ontologies/collections.ts b/browser/lib/src/ontologies/collections.ts index 2800da98c..e8d1eb1cb 100644 --- a/browser/lib/src/ontologies/collections.ts +++ b/browser/lib/src/ontologies/collections.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const collections = { classes: { diff --git a/browser/lib/src/ontologies/commits.ts b/browser/lib/src/ontologies/commits.ts index 50eb6e018..a75e78cb5 100644 --- a/browser/lib/src/ontologies/commits.ts +++ b/browser/lib/src/ontologies/commits.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const commits = { classes: { diff --git a/browser/lib/src/ontologies/core.ts b/browser/lib/src/ontologies/core.ts index 2a5503cda..e18ad4d05 100644 --- a/browser/lib/src/ontologies/core.ts +++ b/browser/lib/src/ontologies/core.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const core = { classes: { diff --git a/browser/lib/src/ontologies/dataBrowser.ts b/browser/lib/src/ontologies/dataBrowser.ts index 12dd67d71..4f524a370 100644 --- a/browser/lib/src/ontologies/dataBrowser.ts +++ b/browser/lib/src/ontologies/dataBrowser.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const dataBrowser = { classes: { @@ -194,7 +194,7 @@ declare module '../index.js' { interface PropTypeMapping { [dataBrowser.properties.subResources]: string[]; [dataBrowser.properties.displayStyle]: string; - [dataBrowser.properties.publishedAt]: string; + [dataBrowser.properties.publishedAt]: number; [dataBrowser.properties.elements]: string[]; [dataBrowser.properties.messages]: string[]; [dataBrowser.properties.nextPage]: string; diff --git a/browser/lib/src/ontologies/server.ts b/browser/lib/src/ontologies/server.ts index 510b850c7..c12319fd6 100644 --- a/browser/lib/src/ontologies/server.ts +++ b/browser/lib/src/ontologies/server.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const server = { classes: { @@ -41,6 +41,8 @@ export const server = { status: 'https://atomicdata.dev/ontology/server/property/status', responseMessage: 'https://atomicdata.dev/ontology/server/property/response-message', + defaultOntology: + 'https://atomicdata.dev/ontology/server/property/default-ontology', }, } as const; @@ -75,7 +77,8 @@ declare module '../index.js' { | typeof server.properties.children | 'https://atomicdata.dev/properties/description' | 'https://atomicdata.dev/properties/subresources' - | 'https://atomicdata.dev/properties/write'; + | 'https://atomicdata.dev/properties/write' + | typeof server.properties.defaultOntology; }; [server.classes.redirect]: { requires: BaseProps | typeof server.properties.destination; @@ -132,6 +135,7 @@ declare module '../index.js' { [server.properties.destination]: string; [server.properties.status]: number; [server.properties.responseMessage]: string; + [server.properties.defaultOntology]: string; } interface PropSubjectToNameMapping { @@ -158,5 +162,6 @@ declare module '../index.js' { [server.properties.destination]: 'destination'; [server.properties.status]: 'status'; [server.properties.responseMessage]: 'responseMessage'; + [server.properties.defaultOntology]: 'defaultOntology'; } } diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index 53c13b286..400a6a33d 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -726,17 +726,6 @@ export class Resource<C extends OptionalClass = any> { this._subject = subject; } - /** Returns true if the value has not changed */ - private equalsCurrentValue(prop: string, value: JSONValue) { - const ownValue = this.get(prop); - - if (value === Object(value)) { - return JSON.stringify(ownValue) === JSON.stringify(value); - } - - return ownValue === value; - } - private isParentNew() { const parentSubject = this.propvals.get(core.properties.parent) as string; diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index 686d8b93a..0ec1d43ee 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -1,3 +1,4 @@ +import { ulid } from 'ulid'; import { removeCookieAuthentication, setCookieAuthentication, @@ -26,6 +27,7 @@ import { buildSearchSubject, server, } from './index.js'; +import { stringToSlug } from './stringToSlug.js'; import { authenticate, fetchWebSocket, startWebsocket } from './websockets.js'; /** Function called when a resource is updated or removed */ @@ -48,6 +50,8 @@ type CreateResourceOptions = { subject?: string; /** Parent the subject belongs to, defaults to the serverUrl */ parent?: string; + /** Set to true if the resource should not have a parent. (For example Drives don't have parents) */ + noParent?: boolean; /** Subject(s) of the resources class */ isA?: string | string[]; /** Any additional properties the resource should have */ @@ -214,13 +218,15 @@ export class Store { * isA (optional), * properties (optional) - any additional properties to be set on the resource. */ - public async newResource<C extends OptionalClass = UnknownClass>( - options: CreateResourceOptions = {}, - ): Promise<Resource<C>> { - const { subject, parent, isA, propVals } = options; - + public async newResource<C extends OptionalClass = UnknownClass>({ + subject, + parent, + isA, + propVals, + noParent, + }: CreateResourceOptions = {}): Promise<Resource<C>> { const normalizedIsA = Array.isArray(isA) ? isA : [isA]; - const newSubject = subject ?? this.createSubject(normalizedIsA[0], parent); + const newSubject = subject ?? this.createSubject(); const resource = this.getResourceLoading(newSubject, { newResource: true }); @@ -228,7 +234,9 @@ export class Store { await resource.addClasses(...(normalizedIsA as string[])); } - await resource.set(core.properties.parent, parent ?? this.serverUrl); + if (!noParent) { + await resource.set(core.properties.parent, parent ?? this.serverUrl); + } if (propVals) { for (const [key, value] of Object.entries(propVals)) { @@ -288,22 +296,19 @@ export class Store { parts: string[], parent?: string, ): Promise<string> { - const path = parts.join('/'); + const path = parts.map(part => stringToSlug(part)).join('/'); const parentUrl = parent ?? this.getServerUrl(); return this.findAvailableSubject(path, parentUrl); } - /** Creates a random URL. Add a classnme (e.g. 'persons') to make a nicer name */ - public createSubject(className?: string, parentSubject?: string): string { - const random = this.randomPart(); - className = className ? className : 'things'; - + /** Creates a random subject url. You can pass a parent subject if you want that to be included in the url. */ + public createSubject(parentSubject?: string): string { if (parentSubject) { - return `${parentSubject}/${className}/${random}`; + return `${parentSubject}/${this.randomPart()}`; } - return `${this.getServerUrl()}/${className}/${random}`; + return new URL(`/${this.randomPart()}`, this.serverUrl).toString(); } /** @@ -507,7 +512,7 @@ export class Store { /** Gets a property by URL. */ public async getProperty(subject: string): Promise<Property> { // This leads to multiple fetches! - const resource = await this.getResourceAsync(subject); + const resource = await this.getResource(subject); if (resource === undefined) { throw Error(`Property ${subject} is not found`); @@ -517,7 +522,7 @@ export class Store { throw Error(`Property ${subject} cannot be loaded: ${resource.error}`); } - const datatypeUrl = resource.get(urls.properties.datatype); + const datatypeUrl = resource.get(core.properties.datatype); if (datatypeUrl === undefined) { throw Error( @@ -525,7 +530,7 @@ export class Store { ); } - const shortname = resource.get(urls.properties.shortname); + const shortname = resource.get(core.properties.shortname); if (shortname === undefined) { throw Error( @@ -533,7 +538,7 @@ export class Store { ); } - const description = resource.get(urls.properties.description); + const description = resource.get(core.properties.description); if (description === undefined) { throw Error( @@ -541,7 +546,7 @@ export class Store { ); } - const classTypeURL = resource.get(urls.properties.classType)?.toString(); + const classTypeURL = resource.get(core.properties.classtype)?.toString(); const propery: Property = { subject, @@ -933,7 +938,7 @@ export class Store { } private randomPart(): string { - return Math.random().toString(36).substring(2); + return ulid().toLowerCase(); } private async findAvailableSubject( @@ -941,7 +946,7 @@ export class Store { parent: string, firstTry = true, ): Promise<string> { - let url = `${parent}/${path}`; + let url = new URL(`${parent}/${path}`).toString(); if (!firstTry) { const randomPart = this.randomPart(); @@ -968,7 +973,6 @@ export class Store { return; } - // We clone for react, because otherwise it won't rerender Promise.allSettled(callbacks.map(async cb => cb(resource))); } } diff --git a/browser/lib/src/stringToSlug.ts b/browser/lib/src/stringToSlug.ts new file mode 100644 index 000000000..a6f8bf199 --- /dev/null +++ b/browser/lib/src/stringToSlug.ts @@ -0,0 +1,7 @@ +export function stringToSlug(str: string): string { + return str + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/[^\w-]+/g, ''); +} diff --git a/browser/package.json b/browser/package.json index ea861ac7b..261a4d94d 100644 --- a/browser/package.json +++ b/browser/package.json @@ -26,7 +26,7 @@ "ts-jest": "^29.0.1", "typedoc": "^0.25.3", "typedoc-plugin-missing-exports": "^2.1.0", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "vite": "^5.0.12" }, "name": "@tomic/root", diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 7b85ed7fa..8e8b71759 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -38,10 +38,10 @@ importers: version: 5.3.3 '@typescript-eslint/eslint-plugin': specifier: ^5.9.0 - version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.23.0)(typescript@5.3.2) + version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.23.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^5.9.0 - version: 5.62.0(eslint@8.23.0)(typescript@5.3.2) + version: 5.62.0(eslint@8.23.0)(typescript@5.4.5) chai: specifier: ^4.3.4 version: 4.3.7 @@ -83,16 +83,16 @@ importers: version: 18.2.0 ts-jest: specifier: ^29.0.1 - version: 29.1.1(@babel/core@7.22.9)(jest@29.6.2)(typescript@5.3.2) + version: 29.1.1(@babel/core@7.22.9)(jest@29.6.2)(typescript@5.4.5) typedoc: specifier: ^0.25.3 - version: 0.25.4(typescript@5.3.2) + version: 0.25.4(typescript@5.4.5) typedoc-plugin-missing-exports: specifier: ^2.1.0 version: 2.1.0(typedoc@0.25.4) typescript: - specifier: ^5.3.2 - version: 5.3.2 + specifier: ^5.4.5 + version: 5.4.5 vite: specifier: ^5.0.12 version: 5.0.12(@types/node@20.11.5) @@ -144,13 +144,13 @@ importers: version: 1.2.1 '@radix-ui/react-popover': specifier: ^1.0.6 - version: 1.0.6(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.6(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-scroll-area': specifier: ^1.0.1 - version: 1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tabs': specifier: ^1.0.4 - version: 1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@tomic/react': specifier: workspace:* version: link:../react @@ -246,8 +246,8 @@ importers: specifier: ^3.1.0 version: 3.1.2 gh-pages: - specifier: ^3.1.0 - version: 3.2.3 + specifier: ^5.0.0 + version: 5.0.0 lint-staged: specifier: ^10.5.4 version: 10.5.4 @@ -267,8 +267,8 @@ importers: e2e: devDependencies: '@playwright/test': - specifier: ^1.37.0 - version: 1.37.0 + specifier: ^1.43.1 + version: 1.43.1 lib: dependencies: @@ -287,6 +287,9 @@ importers: fast-json-stable-stringify: specifier: ^2.1.0 version: 2.1.0 + ulid: + specifier: ^2.3.0 + version: 2.3.0 devDependencies: '@tomic/cli': specifier: workspace:* @@ -328,13 +331,13 @@ importers: devDependencies: '@rollup/plugin-typescript': specifier: ^10.0.1 - version: 10.0.1(tslib@2.6.1)(typescript@5.3.2) + version: 10.0.1(tslib@2.6.1)(typescript@4.9.5) '@typescript-eslint/parser': specifier: ^5.47.0 - version: 5.62.0(eslint@8.23.0)(typescript@5.3.2) + version: 5.62.0(eslint@8.23.0)(typescript@4.9.5) rollup-plugin-typescript-paths: specifier: ^1.4.0 - version: 1.4.0(typescript@5.3.2) + version: 1.4.0(typescript@4.9.5) tslib: specifier: ^2.4.1 version: 2.6.1 @@ -438,7 +441,7 @@ packages: '@babel/traverse': 7.22.8 '@babel/types': 7.22.5 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -533,7 +536,7 @@ packages: '@babel/core': 7.22.9 '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) lodash.debounce: 4.0.8 resolve: 1.22.3 transitivePeerDependencies: @@ -1634,7 +1637,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.7 '@babel/types': 7.22.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -2793,8 +2796,8 @@ packages: strip-ansi: 7.1.0 supports-color: 9.4.0 terminal-link: 3.0.0 - ts-node: 10.9.1(@types/node@20.11.5)(typescript@5.3.2) - typescript: 5.3.2 + ts-node: 10.9.1(@types/node@20.11.5)(typescript@5.4.5) + typescript: 5.4.5 uuid: 9.0.0 yargs: 17.7.2 transitivePeerDependencies: @@ -3670,15 +3673,12 @@ packages: engines: {node: '>=14'} dev: true - /@playwright/test@1.37.0: - resolution: {integrity: sha512-181WBLk4SRUyH1Q96VZl7BP6HcK0b7lbdeKisn3N/vnjitk+9HbdlFz/L5fey05vxaAhldIDnzo8KUoy8S3mmQ==} + /@playwright/test@1.43.1: + resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==} engines: {node: '>=16'} hasBin: true dependencies: - '@types/node': 20.11.5 - playwright-core: 1.37.0 - optionalDependencies: - fsevents: 2.3.2 + playwright: 1.43.1 dev: true /@pnpm/config.env-replace@1.1.0: @@ -3757,7 +3757,7 @@ packages: '@babel/runtime': 7.22.6 dev: false - /@radix-ui/react-arrow@1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: '@types/react': '*' @@ -3771,13 +3771,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.6 - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-collection@1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: '@types/react': '*' @@ -3793,9 +3794,10 @@ packages: '@babel/runtime': 7.22.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3842,7 +3844,7 @@ packages: react: 18.2.0 dev: false - /@radix-ui/react-dismissable-layer@1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: '@types/react': '*' @@ -3858,10 +3860,11 @@ packages: '@babel/runtime': 7.22.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3880,7 +3883,7 @@ packages: react: 18.2.0 dev: false - /@radix-ui/react-focus-scope@1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} peerDependencies: '@types/react': '*' @@ -3895,9 +3898,10 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3917,7 +3921,7 @@ packages: react: 18.2.0 dev: false - /@radix-ui/react-popover@1.0.6(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-popover@1.0.6(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==} peerDependencies: '@types/react': '*' @@ -3934,24 +3938,25 @@ packages: '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-popper': 1.1.2(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-remove-scroll: 2.5.5(@types/react@18.2.34)(react@18.2.0) dev: false - /@radix-ui/react-popper@1.1.2(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: '@types/react': '*' @@ -3966,21 +3971,22 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@floating-ui/react-dom': 2.0.1(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-arrow': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/rect': 1.0.1 '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-portal@1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} peerDependencies: '@types/react': '*' @@ -3994,13 +4000,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.6 - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-presence@1.0.1(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: '@types/react': '*' @@ -4017,11 +4024,12 @@ packages: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-primitive@1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: '@types/react': '*' @@ -4037,11 +4045,12 @@ packages: '@babel/runtime': 7.22.6 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-roving-focus@1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: '@types/react': '*' @@ -4056,20 +4065,21 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-scroll-area@1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-scroll-area@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==} peerDependencies: '@types/react': '*' @@ -4088,11 +4098,12 @@ packages: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -4112,7 +4123,7 @@ packages: react: 18.2.0 dev: false - /@radix-ui/react-tabs@1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: '@types/react': '*' @@ -4130,11 +4141,12 @@ packages: '@radix-ui/react-context': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.34)(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.34)(react@18.2.0) '@types/react': 18.2.34 + '@types/react-dom': 18.2.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -4388,7 +4400,7 @@ packages: rollup: 2.79.1 dev: true - /@rollup/plugin-typescript@10.0.1(tslib@2.6.1)(typescript@5.3.2): + /@rollup/plugin-typescript@10.0.1(tslib@2.6.1)(typescript@4.9.5): resolution: {integrity: sha512-wBykxRLlX7EzL8BmUqMqk5zpx2onnmRMSw/l9M1sVfkJvdwfxogZQVNUM9gVMJbjRLDR5H6U0OMOrlDGmIV45A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4404,7 +4416,7 @@ packages: '@rollup/pluginutils': 5.0.2 resolve: 1.22.3 tslib: 2.6.1 - typescript: 5.3.2 + typescript: 4.9.5 dev: true /@rollup/pluginutils@3.1.0(rollup@2.79.1): @@ -5104,7 +5116,6 @@ packages: resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} dependencies: '@types/react': 18.2.34 - dev: true /@types/react-pdf@6.2.0: resolution: {integrity: sha512-OSCYmrfaJvpXkM5V4seUMAhUDOAOqbGQf9kwv14INyTf7AjDs2ukfkkQrLWRQ8OjWrDklbXYWh5l7pT7l0N76g==} @@ -5209,7 +5220,7 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.23.0)(typescript@5.3.2): + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.23.0)(typescript@5.4.5): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5221,23 +5232,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.3.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.23.0)(typescript@5.3.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.23.0)(typescript@5.3.2) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.23.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.23.0)(typescript@5.4.5) debug: 4.3.4(supports-color@9.4.0) eslint: 8.23.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.3.2) - typescript: 5.3.2 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.62.0(eslint@8.23.0)(typescript@5.3.2): + /@typescript-eslint/parser@5.62.0(eslint@8.23.0)(typescript@4.9.5): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5249,10 +5260,30 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.3.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) debug: 4.3.4(supports-color@9.4.0) eslint: 8.23.0 - typescript: 5.3.2 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.23.0)(typescript@5.4.5): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.4.5) + debug: 4.3.4(supports-color@9.4.0) + eslint: 8.23.0 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -5263,7 +5294,7 @@ packages: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - /@typescript-eslint/type-utils@5.62.0(eslint@8.23.0)(typescript@5.3.2): + /@typescript-eslint/type-utils@5.62.0(eslint@8.23.0)(typescript@5.4.5): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5273,12 +5304,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.3.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.23.0)(typescript@5.3.2) + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.4.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.23.0)(typescript@5.4.5) debug: 4.3.4(supports-color@9.4.0) eslint: 8.23.0 - tsutils: 3.21.0(typescript@5.3.2) - typescript: 5.3.2 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -5287,7 +5318,27 @@ packages: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.3.2): + /@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.4.5): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5302,12 +5353,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.3.2) - typescript: 5.3.2 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 transitivePeerDependencies: - supports-color + dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.23.0)(typescript@5.3.2): + /@typescript-eslint/utils@5.62.0(eslint@8.23.0)(typescript@5.4.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5318,7 +5370,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.3.2) + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.4.5) eslint: 8.23.0 eslint-scope: 5.1.1 semver: 7.5.4 @@ -5361,7 +5413,7 @@ packages: vite: ^4 || ^5 dependencies: '@swc/core': 1.3.104 - vite: 5.0.12 + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -5985,12 +6037,6 @@ packages: resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} dev: true - /async@2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - dependencies: - lodash: 4.17.21 - dev: true - /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true @@ -6039,16 +6085,6 @@ packages: engines: {node: '>=4'} dev: true - /axios@1.6.2: - resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} - dependencies: - follow-redirects: 1.15.3 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: true - /axios@1.6.2(debug@4.3.4): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: @@ -7342,17 +7378,6 @@ packages: ms: 2.1.3 dev: false - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@9.4.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -7594,10 +7619,10 @@ packages: resolution: {integrity: sha512-Mq8egjnW2NSCkzEb/Az15/JnBI/Ryyl6Po0Y+0mABTFvOS6DAyUGRZqz1nyhu4QJmWWe0zaGs/ITIBeWkvCkGw==} engines: {node: ^14.14.0 || >=16.0.0} dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.3.2) + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.4.5) ast-module-types: 5.0.0 node-source-walk: 6.0.2 - typescript: 5.3.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -7708,8 +7733,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /email-addresses@3.1.0: - resolution: {integrity: sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==} + /email-addresses@5.0.0: + resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} dev: true /emittery@0.13.1: @@ -8008,7 +8033,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.3.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.4.5) debug: 3.2.7 eslint: 8.23.0 eslint-import-resolver-node: 0.3.7 @@ -8026,7 +8051,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.3.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.23.0)(typescript@5.4.5) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 @@ -8850,16 +8875,6 @@ packages: from2: 2.3.0 dev: true - /follow-redirects@1.15.3: - resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: true - /follow-redirects@1.15.3(debug@4.3.4): resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} engines: {node: '>=4.0'} @@ -9120,14 +9135,14 @@ packages: engines: {node: '>=0.10.0'} dev: true - /gh-pages@3.2.3: - resolution: {integrity: sha512-jA1PbapQ1jqzacECfjUaO9gV8uBgU6XNMV0oXLtfCX3haGLe5Atq8BxlrADhbD6/UdG9j6tZLWAkAybndOXTJg==} + /gh-pages@5.0.0: + resolution: {integrity: sha512-Nqp1SjkPIB94Xw/3yYNTUL+G2dxlhjvv1zeN/4kMC1jfViTEqhtVz/Ba1zSXHuvXCN9ADNS1dN4r5/J/nZWEQQ==} engines: {node: '>=10'} hasBin: true dependencies: - async: 2.6.4 + async: 3.2.4 commander: 2.20.3 - email-addresses: 3.1.0 + email-addresses: 5.0.0 filenamify: 4.3.0 find-cache-dir: 3.3.2 fs-extra: 8.1.0 @@ -11091,7 +11106,7 @@ packages: cli-truncate: 2.1.0 commander: 6.2.1 cosmiconfig: 7.1.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) dedent: 0.7.0 enquirer: 2.4.1 execa: 4.1.0 @@ -11920,7 +11935,7 @@ packages: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: '@types/debug': 4.1.8 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -13118,12 +13133,22 @@ packages: pathe: 1.1.1 dev: true - /playwright-core@1.37.0: - resolution: {integrity: sha512-1c46jhTH/myQw6sesrcuHVtLoSNfJv8Pfy9t3rs6subY7kARv0HRw5PpyfPYPpPtQvBOmgbE6K+qgYUpj81LAA==} + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} engines: {node: '>=16'} hasBin: true dev: true + /playwright@1.43.1: + resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.43.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} dependencies: @@ -14225,12 +14250,12 @@ packages: terser: 5.19.2 dev: true - /rollup-plugin-typescript-paths@1.4.0(typescript@5.3.2): + /rollup-plugin-typescript-paths@1.4.0(typescript@4.9.5): resolution: {integrity: sha512-6EgeLRjTVmymftEyCuYu91XzY5XMB5lR0YrJkeT0D7OG2RGSdbNL+C/hfPIdc/sjMa9Sl5NLsxIr6C/+/5EUpA==} peerDependencies: typescript: '>=3.4' dependencies: - typescript: 5.3.2 + typescript: 4.9.5 dev: true /rollup@2.79.1: @@ -15436,7 +15461,7 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false - /ts-jest@29.1.1(@babel/core@7.22.9)(jest@29.6.2)(typescript@5.3.2): + /ts-jest@29.1.1(@babel/core@7.22.9)(jest@29.6.2)(typescript@5.4.5): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -15466,11 +15491,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.4 - typescript: 5.3.2 + typescript: 5.4.5 yargs-parser: 21.1.1 dev: true - /ts-node@10.9.1(@types/node@20.11.5)(typescript@5.3.2): + /ts-node@10.9.1(@types/node@20.11.5)(typescript@5.4.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -15496,7 +15521,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.2 + typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -15516,14 +15541,24 @@ packages: /tslib@2.6.1: resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} - /tsutils@3.21.0(typescript@5.3.2): + /tsutils@3.21.0(typescript@4.9.5): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.5 + dev: true + + /tsutils@3.21.0(typescript@5.4.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.3.2 + typescript: 5.4.5 /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -15633,10 +15668,10 @@ packages: peerDependencies: typedoc: 0.24.x || 0.25.x dependencies: - typedoc: 0.25.4(typescript@5.3.2) + typedoc: 0.25.4(typescript@5.4.5) dev: true - /typedoc@0.25.4(typescript@5.3.2): + /typedoc@0.25.4(typescript@5.4.5): resolution: {integrity: sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==} engines: {node: '>= 16'} hasBin: true @@ -15647,7 +15682,7 @@ packages: marked: 4.3.0 minimatch: 9.0.3 shiki: 0.14.3 - typescript: 5.3.2 + typescript: 5.4.5 dev: true /types-wm@1.1.0: @@ -15660,8 +15695,8 @@ packages: hasBin: true dev: true - /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true @@ -15679,7 +15714,6 @@ packages: /ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true - dev: true /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -16066,10 +16100,10 @@ packages: workbox-build: ^7.0.0 workbox-window: ^7.0.0 dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.0.12 + vite: 5.0.12(@types/node@20.11.5) workbox-build: 7.0.0 workbox-window: 7.0.0 transitivePeerDependencies: @@ -16081,50 +16115,15 @@ packages: peerDependencies: vite: ^2 || ^3 || ^4 || ^5 dependencies: - axios: 1.6.2 + axios: 1.6.2(debug@4.3.4) clean-css: 5.3.3 flat-cache: 3.0.4 picocolors: 1.0.0 - vite: 5.0.12 + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - debug dev: true - /vite@5.0.12: - resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.19.6 - postcss: 8.4.33 - rollup: 4.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vite@5.0.12(@types/node@20.11.5): resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/browser/react/package.json b/browser/react/package.json index 0e79d8a22..5e10b4e03 100644 --- a/browser/react/package.json +++ b/browser/react/package.json @@ -29,7 +29,7 @@ "start": "pnpm watch", "watch": "tsc --build --watch", "tsc": "tsc --build", - "typecheck": "tsc --noEmit" + "typecheck": "pnpm exec tsc --noEmit" }, "source": "src/index.ts", "type": "module", diff --git a/docs/src/js-lib/store.md b/docs/src/js-lib/store.md index 800e36d2d..884455a98 100644 --- a/docs/src/js-lib/store.md +++ b/docs/src/js-lib/store.md @@ -122,6 +122,26 @@ const resource = await store.newResource({ await resource.save(); ``` +## Generating random subjects + +In some cases you might need a subject before you have created the resource with that subject. +To generate a random subject, use the `store.createSubject()` method. +This method generates a new subject with the current serverURL as hostname and a random lowercased [ULID](https://github.com/ulid/spec) string as the path. + +The method also allows you to pass a parent subject to generate a subject under that parent. + +```typescript +const subject = store.createSubject(); +// Result: https://myserver.com/01hw30e1w6t9y0y5aqg0aghhf4 + + +// With parent subject +const subject = store.createSubject(parent.subject); +``` + +Keep in mind that subjects never change once they are set, even if the parent changes. +This means you can't reliably infer the parent from the subject. + ## Full-Text Search AtomicServer comes included with a full-text search API.