From 63da3619eade779afda0407a96f09525827a22f7 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Thu, 20 Jun 2024 15:39:30 +0200 Subject: [PATCH 01/13] #896 Fix double tap on ios + move new resource buttons --- browser/CHANGELOG.md | 1 + .../src/components/EditableTitle.tsx | 10 +- .../components/ResourceContextMenu/index.tsx | 141 +++++++++--------- .../ResourceUsage/ResourceUsage.tsx | 13 +- .../SideBar/ResourceSideBar/DropEdge.tsx | 3 +- .../ResourceSideBar/FloatingActions.tsx | 31 ++-- .../ResourceSideBar/ResourceSideBar.tsx | 37 +++-- .../src/components/SideBar/SideBarDrive.tsx | 61 +++++--- .../src/components/SideBar/SidebarCSSVars.ts | 1 + .../src/components/SideBar/index.tsx | 10 +- browser/data-browser/src/config.ts | 1 - browser/data-browser/src/helpers/addIf.ts | 10 ++ .../src/views/FolderPage/ListView.tsx | 2 +- .../Property/PropertyWriteDialog.tsx | 12 +- browser/e2e/tests/e2e.spec.ts | 3 +- browser/e2e/tests/filePicker.spec.ts | 4 +- browser/e2e/tests/search.spec.ts | 6 +- browser/e2e/tests/test-utils.ts | 4 +- 18 files changed, 188 insertions(+), 162 deletions(-) create mode 100644 browser/data-browser/src/components/SideBar/SidebarCSSVars.ts create mode 100644 browser/data-browser/src/helpers/addIf.ts diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 4ff93952b..db537672d 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#855](https://github.com/atomicdata-dev/atomic-server/issues/855) Add a dialog that shows how to fetch and use the current resource in your code. - [#825](https://github.com/atomicdata-dev/atomic-server/issues/825) Folder display styles are now saved locally instead of on the resource. The display style property will now act as the default view style. +- [#896](https://github.com/atomicdata-dev/atomic-server/issues/896) Fix issue where sidebar items require a double tap on ios. - Updated look of the default resource form. ### @tomic/react diff --git a/browser/data-browser/src/components/EditableTitle.tsx b/browser/data-browser/src/components/EditableTitle.tsx index 79e0d999e..cfb7b1529 100644 --- a/browser/data-browser/src/components/EditableTitle.tsx +++ b/browser/data-browser/src/components/EditableTitle.tsx @@ -1,7 +1,7 @@ import { Resource, useCanWrite, useTitle } from '@tomic/react'; import { useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { FaEdit } from 'react-icons/fa'; +import { FaPencil } from 'react-icons/fa6'; import { styled, css } from 'styled-components'; import { transitionName } from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; @@ -107,8 +107,6 @@ const Title = styled.h1` display: flex; align-items: center; gap: ${p => p.theme.margin}rem; - justify-content: space-between; - cursor: pointer; cursor: ${props => (props.canEdit ? 'pointer' : 'initial')}; opacity: ${props => (props.subtle ? 0.5 : 1)}; @@ -138,14 +136,10 @@ const TitleInput = styled.input` } `; -const Icon = styled(FaEdit)` +const Icon = styled(FaPencil)` opacity: 0; font-size: 0.8em; ${Title}:hover & { opacity: 0.5; - - &:hover { - opacity: 1; - } } `; diff --git a/browser/data-browser/src/components/ResourceContextMenu/index.tsx b/browser/data-browser/src/components/ResourceContextMenu/index.tsx index 241c74272..43c72f58a 100644 --- a/browser/data-browser/src/components/ResourceContextMenu/index.tsx +++ b/browser/data-browser/src/components/ResourceContextMenu/index.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { Client, core, useResource, useStore } from '@tomic/react'; +import { Client, core, useCanWrite, useResource } from '@tomic/react'; import { editURL, dataURL, @@ -19,13 +19,13 @@ import { FaClock, FaCode, FaDownload, - FaEdit, - FaEllipsisV, - FaRedo, - FaSearch, - FaShareSquare, + FaPencil, + FaEllipsisVertical, + FaMagnifyingGlass, + FaShare, FaTrash, -} from 'react-icons/fa'; + FaPlus, +} from 'react-icons/fa6'; import { useQueryScopeHandler } from '../../hooks/useQueryScope'; import { ConfirmationDialog, @@ -35,18 +35,20 @@ import { ResourceInline } from '../../views/ResourceInline'; import { ResourceUsage } from '../ResourceUsage'; import { useCurrentSubject } from '../../helpers/useCurrentSubject'; import { ResourceCodeUsageDialog } from '../../views/CodeUsage/ResourceCodeUsageDialog'; +import { useNewRoute } from '../../helpers/useNewRoute'; +import { addIf } from '../../helpers/addIf'; export enum ContextMenuOptions { View = 'view', Data = 'data', Edit = 'edit', - Refresh = 'refresh', Scope = 'scope', Share = 'share', Delete = 'delete', History = 'history', Import = 'import', UseInCode = 'useInCode', + NewChild = 'newChild', } export interface ResourceContextMenuProps { @@ -74,15 +76,14 @@ function ResourceContextMenu({ bindActive, onAfterDelete, }: ResourceContextMenuProps) { - const store = useStore(); const navigate = useNavigate(); const location = useLocation(); const resource = useResource(subject); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showCodeUsageDialog, setShowCodeUsageDialog] = useState(false); - + const handleAddClick = useNewRoute(subject); const [currentSubject] = useCurrentSubject(); - + const [canWrite] = useCanWrite(resource); const { enableScope } = useQueryScopeHandler(subject); // Try to not have a useResource hook in here, as that will lead to many costly fetches when the user enters a new subject @@ -111,43 +112,44 @@ function ResourceContextMenu({ } const items: DropdownItem[] = [ - ...(simple - ? [] - : [ - { - disabled: location.pathname.startsWith(paths.show), - id: ContextMenuOptions.View, - label: 'normal view', - helper: 'Open the regular, default View.', - onClick: () => navigate(constructOpenURL(subject)), - }, - { - disabled: location.pathname.startsWith(paths.data), - id: ContextMenuOptions.Data, - label: 'data view', - helper: 'View the resource and its properties in the Data View.', - shortcut: shortcuts.data, - onClick: () => navigate(dataURL(subject)), - }, - DIVIDER, - { - id: ContextMenuOptions.Refresh, - icon: , - label: 'refresh', - helper: - 'Fetch the resouce again from the server, possibly see new changes.', - onClick: () => store.fetchResourceFromServer(subject), - }, - ]), - { - // disabled: !canWrite || location.pathname.startsWith(paths.edit), - id: ContextMenuOptions.Edit, - label: 'edit', - helper: 'Open the edit form.', - icon: , - shortcut: simple ? '' : shortcuts.edit, - onClick: () => navigate(editURL(subject)), - }, + ...addIf( + !simple, + { + disabled: location.pathname.startsWith(paths.show), + id: ContextMenuOptions.View, + label: 'normal view', + helper: 'Open the regular, default View.', + onClick: () => navigate(constructOpenURL(subject)), + }, + { + disabled: location.pathname.startsWith(paths.data), + id: ContextMenuOptions.Data, + label: 'data view', + helper: 'View the resource and its properties in the Data View.', + shortcut: shortcuts.data, + onClick: () => navigate(dataURL(subject)), + }, + DIVIDER, + ), + ...addIf( + canWrite, + { + // disabled: !canWrite || location.pathname.startsWith(paths.edit), + id: ContextMenuOptions.Edit, + label: 'edit', + helper: 'Open the edit form.', + icon: , + shortcut: simple ? '' : shortcuts.edit, + onClick: () => navigate(editURL(subject)), + }, + { + id: ContextMenuOptions.NewChild, + label: 'add child', + helper: 'Create a new resource under this resource.', + icon: , + onClick: handleAddClick, + }, + ), { id: ContextMenuOptions.UseInCode, label: 'use in code', @@ -158,27 +160,19 @@ function ResourceContextMenu({ }, { id: ContextMenuOptions.Scope, - label: 'search in', + label: 'search children', helper: 'Scope search to resource', - icon: , + icon: , onClick: enableScope, }, { - // disabled: !canWrite || history.location.pathname.startsWith(paths.edit), id: ContextMenuOptions.Share, label: 'share', - icon: , + icon: , helper: 'Open the share menu', onClick: () => navigate(shareURL(subject)), }, - { - // disabled: !canWrite, - id: ContextMenuOptions.Delete, - icon: , - label: 'delete', - helper: 'Delete this resource.', - onClick: () => setShowDeleteDialog(true), - }, + { id: ContextMenuOptions.History, icon: , @@ -186,13 +180,24 @@ function ResourceContextMenu({ helper: 'Show the history of this resource', onClick: () => navigate(historyURL(subject)), }, - { - id: ContextMenuOptions.Import, - icon: , - label: 'import', - helper: 'Import Atomic Data to this resource', - onClick: () => navigate(importerURL(subject)), - }, + ...addIf( + canWrite, + { + id: ContextMenuOptions.Import, + icon: , + label: 'import', + helper: 'Import Atomic Data to this resource', + onClick: () => navigate(importerURL(subject)), + }, + { + disabled: !canWrite, + id: ContextMenuOptions.Delete, + icon: , + label: 'delete', + helper: 'Delete this resource.', + onClick: () => setShowDeleteDialog(true), + }, + ), ]; const filteredItems = showOnly @@ -205,7 +210,7 @@ function ResourceContextMenu({ const triggerComp = trigger ?? buildDefaultTrigger( - , + , title ?? `Open ${resource.title} menu`, ); diff --git a/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx b/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx index e02a1c992..8b910c60f 100644 --- a/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx +++ b/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx @@ -1,20 +1,21 @@ -import { Resource, classes, useCollection } from '@tomic/react'; +import { Resource, core, useCollection } from '@tomic/react'; import { PropertyUsage } from './PropertyUsage'; import { UsageCard } from './UsageCard'; import { ClassUsage } from './ClassUsage'; import { ChildrenUsage } from './ChildrenUsage'; +import { Column } from '../Row'; interface ResourceUsageProps { resource: Resource; } export function ResourceUsage({ resource }: ResourceUsageProps): JSX.Element { - if (resource.hasClasses(classes.property)) { + if (resource.hasClasses(core.classes.property)) { return ; } - if (resource.hasClasses(classes.class)) { + if (resource.hasClasses(core.classes.class)) { return ; } @@ -23,11 +24,11 @@ export function ResourceUsage({ resource }: ResourceUsageProps): JSX.Element { function BasicUsage({ resource }: ResourceUsageProps): JSX.Element { const { collection } = useCollection({ - value: resource.getSubject(), + value: resource.subject, }); return ( - <> + } /> - + ); } diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/DropEdge.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/DropEdge.tsx index 88714fa58..8c860198b 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/DropEdge.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/DropEdge.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { transition } from '../../../helpers/transition'; import { SideBarDropData } from '../useSidebarDnd'; import { useCanWrite, useResource } from '@tomic/react'; +import { SIDEBAR_WIDTH_PROP } from '../SidebarCSSVars'; interface DropEdgeProps { parentHierarchy: string[]; @@ -62,7 +63,7 @@ const DropEdgeElement = styled.div<{ visible: boolean; active: boolean }>` background: ${p => p.theme.colors.main}; opacity: ${p => (p.active ? 1 : 0)}; z-index: 2; - width: calc(var(--width) - 2rem); + width: calc(var(${SIDEBAR_WIDTH_PROP}) - 2rem); ${transition('opacity', 'transform')} `; diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/FloatingActions.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/FloatingActions.tsx index 19d60a057..06b35ec3d 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/FloatingActions.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/FloatingActions.tsx @@ -1,10 +1,7 @@ -import { useCanWrite, useResource, useTitle } from '@tomic/react'; import { useState } from 'react'; -import { FaEllipsisV, FaPlus } from 'react-icons/fa'; +import { FaEllipsisVertical } from 'react-icons/fa6'; import { styled, css } from 'styled-components'; -import { useNewRoute } from '../../../helpers/useNewRoute'; import { buildDefaultTrigger } from '../../Dropdown/DefaultTrigger'; -import { IconButton } from '../../IconButton/IconButton'; import ResourceContextMenu from '../../ResourceContextMenu'; export interface FloatingActionsProps { @@ -17,24 +14,10 @@ export function FloatingActions({ subject, className, }: FloatingActionsProps): JSX.Element { - const parentResource = useResource(subject); - const [parentName] = useTitle(parentResource); const [dropdownActive, setDropdownActive] = useState(false); - const [canWrite] = useCanWrite(parentResource); - - const handleAddClick = useNewRoute(subject); return ( - {canWrite && ( - - - - )} ` - visibility: ${p => (p.dropdownActive ? 'visible' : 'hidden')}; + visibility: hidden; font-size: 0.9rem; color: ${p => p.theme.colors.main}; + + @media (pointer: fine) { + visibility: ${p => (p.dropdownActive ? 'visible' : 'hidden')}; + } `; export const floatingHoverStyles = css` position: relative; &:hover ${Wrapper}, &:focus-within ${Wrapper} { - visibility: visible; + @media (pointer: fine) { + visibility: visible; + } } `; -const SideBarDropDownTrigger = buildDefaultTrigger(); +const SideBarDropDownTrigger = buildDefaultTrigger(); diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx index 4409c95fd..1efeb8f28 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx @@ -55,7 +55,6 @@ export function ResourceSideBar({ resource, dataBrowser.properties.subResources, ); - const hasSubResources = subResources.length > 0; const dragData: SideBarDragData = { renderedUnder: renderedHierargy.at(-1)!, @@ -73,24 +72,6 @@ export function ResourceSideBar({ disabled: !canWrite, }); - const isDragging = draggingNode?.id === subject; - - useEffect(() => { - if (isDragging) { - setOpen(false); - } - }, [isDragging]); - - const isHoveringOver = over?.data.current?.parent === subject; - - useEffect(() => { - if (ancestry.includes(subject) && ancestry[0] !== subject) { - setOpen(true); - } - }, [ancestry]); - - const hierarchyWithItself = [...renderedHierargy, subject]; - const TitleComp = useMemo( () => ( 0; + const isDragging = draggingNode?.id === subject; + const isHoveringOver = over?.data.current?.parent === subject; + const hierarchyWithItself = [...renderedHierargy, subject]; + + useEffect(() => { + if (isDragging) { + setOpen(false); + } + }, [isDragging]); + + useEffect(() => { + if (ancestry.includes(subject) && ancestry[0] !== subject) { + setOpen(true); + } + }, [ancestry]); + if (resource.loading) { return ( unknown; + onItemClick: () => unknown; onIsRearangingChange: (isRearanging: boolean) => void; } /** Shows the current Drive, it's children and an option to change to a different Drive */ export function SideBarDrive({ - handleClickItem, + onItemClick, onIsRearangingChange, }: SideBarDriveProps): JSX.Element { const store = useStore(); @@ -52,7 +50,10 @@ export function SideBarDrive({ announcements, } = useSidebarDnd(onIsRearangingChange); const driveResource = useResource(drive); - const [subResources] = useArray(driveResource, urls.properties.subResources); + const [subResources] = useArray( + driveResource, + dataBrowser.properties.subResources, + ); const [title] = useTitle(driveResource); const navigate = useNavigate(); const [agentCanWrite] = useCanWrite(driveResource); @@ -74,7 +75,7 @@ export function SideBarDrive({ title={`Your current baseURL is ${drive}`} data-test='sidebar-drive-open' onClick={() => { - handleClickItem(); + onItemClick(); navigate(constructOpenURL(drive)); }} > @@ -83,15 +84,6 @@ export function SideBarDrive({ - {agentCanWrite && ( - navigate(paths.new)} - title={`Create a new resource in this drive (${shortcuts.new})`} - data-test='sidebar-new-resource' - > - - - )} @@ -117,7 +109,7 @@ export function SideBarDrive({ subject={child} renderedHierargy={[drive]} ancestry={ancestry} - onClick={handleClickItem} + onClick={onItemClick} /> @@ -133,6 +125,15 @@ export function SideBarDrive({ : driveResource.error.message)} )} + {agentCanWrite && ( + navigate(paths.new)} + > + + + )} {createPortal( @@ -182,3 +183,25 @@ const HeadingButtonWrapper = styled(Row)` const StyledScrollArea = styled(ScrollArea)` overflow: hidden; `; + +const AddButton = styled.button` + display: flex; + justify-content: center; + color: ${p => p.theme.colors.textLight}; + background: none; + appearance: none; + border: 1px dashed ${p => p.theme.colors.bg2}; + border-radius: ${p => p.theme.radius}; + width: calc(100% - 4rem); + padding-block: 0.3rem; + margin-inline-start: 1.5rem; + margin-block: 0.5rem; + cursor: pointer; + ${transition('color', 'border')} + + &:hover, + &:focus-visible { + color: ${p => p.theme.colors.main}; + border: 1px solid ${p => p.theme.colors.main}; + } +`; diff --git a/browser/data-browser/src/components/SideBar/SidebarCSSVars.ts b/browser/data-browser/src/components/SideBar/SidebarCSSVars.ts new file mode 100644 index 000000000..a5849cd63 --- /dev/null +++ b/browser/data-browser/src/components/SideBar/SidebarCSSVars.ts @@ -0,0 +1 @@ +export const SIDEBAR_WIDTH_PROP = '--sidebar-width'; diff --git a/browser/data-browser/src/components/SideBar/index.tsx b/browser/data-browser/src/components/SideBar/index.tsx index 5f46db09d..ec6778808 100644 --- a/browser/data-browser/src/components/SideBar/index.tsx +++ b/browser/data-browser/src/components/SideBar/index.tsx @@ -14,6 +14,7 @@ import { Column } from '../Row'; import { OntologiesPanel } from './OntologySideBar/OntologiesPanel'; import { SideBarPanel } from './SideBarPanel'; import { Panel, usePanelList } from './usePanelList'; +import { SIDEBAR_WIDTH_PROP } from './SidebarCSSVars'; /** Amount of pixels where the sidebar automatically shows */ export const SIDEBAR_TOGGLE_WIDTH = 600; @@ -69,7 +70,7 @@ export function SideBar(): JSX.Element { {/* The key is set to make sure the component is re-loaded when the baseURL changes */} @@ -119,7 +120,7 @@ interface SideBarOverlayProps { //@ts-ignore const SideBarStyled = styled.nav.attrs(p => ({ style: { - '--width': p.size, + [SIDEBAR_WIDTH_PROP]: p.size, }, }))` z-index: ${p => p.theme.zIndex.sidebar}; @@ -128,11 +129,12 @@ const SideBarStyled = styled.nav.attrs(p => ({ transition: opacity 0.3s, left 0.3s; - left: ${p => (p.exposed ? '0' : `calc(var(--width) * -1 + 0.5rem)`)}; + left: ${p => + p.exposed ? '0' : `calc(var(${SIDEBAR_WIDTH_PROP}) * -1 + 0.5rem)`}; /* When the user is hovering, show half opacity */ opacity: ${p => (p.exposed ? 1 : 0)}; height: 100vh; - width: var(--width); + width: var(${SIDEBAR_WIDTH_PROP}); position: ${p => (p.locked ? 'relative' : 'absolute')}; border-right: ${p => `1px solid ${p.theme.colors.bg2}`}; box-shadow: ${p => (p.locked ? 'none' : p.theme.boxShadowSoft)}; diff --git a/browser/data-browser/src/config.ts b/browser/data-browser/src/config.ts index d609ff0ee..659a4cc1b 100644 --- a/browser/data-browser/src/config.ts +++ b/browser/data-browser/src/config.ts @@ -1,5 +1,4 @@ /** Returns true if this is run in locally, in Development mode */ export function isDev(): boolean { - //@ts-ignore This key does exist return import.meta.env['MODE'] === 'development'; } diff --git a/browser/data-browser/src/helpers/addIf.ts b/browser/data-browser/src/helpers/addIf.ts new file mode 100644 index 000000000..1358251b1 --- /dev/null +++ b/browser/data-browser/src/helpers/addIf.ts @@ -0,0 +1,10 @@ +/** Simple helper function for adding items to an array only if the condition is true + * @example + * const someArray = [ + * 'pizza', + * ...addIf(likesCheese, 'cheese'), + * ]; + * + */ +export const addIf = (condition: boolean, ...items: T[]): T[] => + condition ? items : []; diff --git a/browser/data-browser/src/views/FolderPage/ListView.tsx b/browser/data-browser/src/views/FolderPage/ListView.tsx index 72e06ce46..1aec1fca0 100644 --- a/browser/data-browser/src/views/FolderPage/ListView.tsx +++ b/browser/data-browser/src/views/FolderPage/ListView.tsx @@ -50,7 +50,7 @@ export function ListView({ {showNewButton && ( - + New Resource diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx index 4f386fe29..0d143c6cf 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx @@ -24,7 +24,7 @@ export function PropertyWriteDialog({ const shortnameProp = useProperty(urls.properties.shortname); return ( - + {dialogProps.show && ( <> @@ -35,19 +35,21 @@ export function PropertyWriteDialog({ property={shortnameProp} /> - + + {/* Spacer fixes an issue where the top border of the description field gets cut of by the container */} + - + )} ); } -const WiderDialogContent = styled(DialogContent)` - width: min(40rem, 90vw); +const Spacer = styled.div` + height: 2px; `; diff --git a/browser/e2e/tests/e2e.spec.ts b/browser/e2e/tests/e2e.spec.ts index ad5a13358..1e76381a1 100644 --- a/browser/e2e/tests/e2e.spec.ts +++ b/browser/e2e/tests/e2e.spec.ts @@ -403,8 +403,7 @@ test.describe('data-browser', async () => { await setTitle(page, d0); // Create a subresource, and later check it in the sidebar - await page.getByTestId('sidebar').getByText(d0).hover(); - await page.locator(`[title="Create new resource under ${d0}"]`).click(); + await page.getByTestId('new-resource-folder').click(); await page.click(`button:has-text("${klass}")`); const d1 = 'depth1'; diff --git a/browser/e2e/tests/filePicker.spec.ts b/browser/e2e/tests/filePicker.spec.ts index b606a0a65..328dc0762 100644 --- a/browser/e2e/tests/filePicker.spec.ts +++ b/browser/e2e/tests/filePicker.spec.ts @@ -8,7 +8,7 @@ import { fillSearchBox, newDrive, newResource, - sideBarNewResource, + sideBarNewResourceTestId, signIn, testFilePath, waitForCommit, @@ -17,7 +17,7 @@ import { const ONTOLOGY_NAME = 'filepicker-test'; const uploadFile = async (page: Page, fileName: string) => { - await page.locator(sideBarNewResource).click(); + await page.getByTestId(sideBarNewResourceTestId).click(); await expect(page).toHaveURL(`${FRONTEND_URL}/app/new`); const fileChooserPromise = page.waitForEvent('filechooser'); diff --git a/browser/e2e/tests/search.spec.ts b/browser/e2e/tests/search.spec.ts index eb5227219..b91eb6876 100644 --- a/browser/e2e/tests/search.spec.ts +++ b/browser/e2e/tests/search.spec.ts @@ -9,7 +9,7 @@ import { clickSidebarItem, editTitle, setTitle, - sideBarNewResource, + sideBarNewResourceTestId, } from './test-utils'; test.describe('search', async () => { test.beforeEach(before); @@ -26,7 +26,7 @@ test.describe('search', async () => { await newDrive(page); // Create folder called 1 - await page.locator(sideBarNewResource).click(); + await page.getByTestId(sideBarNewResourceTestId).click(); await page.locator('button:has-text("folder")').click(); await setTitle(page, 'Salad folder'); @@ -38,7 +38,7 @@ test.describe('search', async () => { await waitForCommit(page); await editTitle('Avocado Salad', page); - await page.locator(sideBarNewResource).click(); + await page.getByTestId(sideBarNewResourceTestId).click(); // Create folder called 'Cake folder' await page.locator('button:has-text("folder")').click(); diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts index 2996e0a4d..ff0c43ff1 100644 --- a/browser/e2e/tests/test-utils.ts +++ b/browser/e2e/tests/test-utils.ts @@ -23,7 +23,7 @@ export const testFilePath = (filename: string) => { 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 sideBarNewResourceTestId = 'sidebar-new-resource'; export const currentDriveTitle = (page: Page) => page.getByTestId('current-drive-title'); export const publicReadRightLocator = (page: Page) => @@ -243,7 +243,7 @@ export async function fillSearchBox( /** Create a new Resource in the current Drive. * Class can be an Class URL or a shortname available in the new page. */ export async function newResource(klass: string, page: Page) { - await page.locator(sideBarNewResource).click(); + await page.getByTestId(sideBarNewResourceTestId).click(); await expect(page).toHaveURL(`${FRONTEND_URL}/app/new`); if (klass.startsWith('https://')) { From 7e82fa45611051d7a00c55bc6d9e9b13f0137fa4 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Thu, 20 Jun 2024 16:30:22 +0200 Subject: [PATCH 02/13] #893 Fix tables not working from different server --- browser/CHANGELOG.md | 5 +++- .../src/views/TablePage/useTableData.ts | 15 +++++++----- browser/react/src/useCollection.ts | 17 ++++++++++---- docs/src/react/useCollection.md | 23 ++++++++++++------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index db537672d..ccb162589 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -8,8 +8,11 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#855](https://github.com/atomicdata-dev/atomic-server/issues/855) Add a dialog that shows how to fetch and use the current resource in your code. - [#825](https://github.com/atomicdata-dev/atomic-server/issues/825) Folder display styles are now saved locally instead of on the resource. The display style property will now act as the default view style. -- [#896](https://github.com/atomicdata-dev/atomic-server/issues/896) Fix issue where sidebar items require a double tap on ios. +- [#896](https://github.com/atomicdata-dev/atomic-server/issues/896) Fix an issue where sidebar items require a double tap on iOS. - Updated look of the default resource form. +- [#896](https://github.com/atomicdata-dev/atomic-server/issues/896) Fix an issue where sidebar items require a double tap on iOS. +- Updated the look & feel of the sidebar a bit. +- [#893](https://github.com/atomicdata-dev/atomic-server/issues/893) Fix tables not showing any rows when viewing from a different server. ### @tomic/react diff --git a/browser/data-browser/src/views/TablePage/useTableData.ts b/browser/data-browser/src/views/TablePage/useTableData.ts index d8bcb5229..de2a0548b 100644 --- a/browser/data-browser/src/views/TablePage/useTableData.ts +++ b/browser/data-browser/src/views/TablePage/useTableData.ts @@ -1,6 +1,6 @@ import { + core, Resource, - urls, useCollection, UseCollectionResult, useResource, @@ -46,23 +46,26 @@ const useTableSorting = () => export function useTableData(resource: Resource): UseTableDataResult { const [sorting, setSortBy] = useTableSorting(); - const [classSubject] = useSubject(resource, urls.properties.classType); + const [classSubject] = useSubject(resource, core.properties.classtype); const tableClass = useResource(classSubject); const queryFilter = useMemo( () => ({ - property: urls.properties.parent, - value: resource.getSubject(), + property: core.properties.parent, + value: resource.subject, sort_by: sorting.prop, sort_desc: sorting.sortDesc, }), - [resource.getSubject(), sorting.prop, sorting.sortDesc], + [resource.subject, sorting.prop, sorting.sortDesc], ); return { tableClass, sorting, setSortBy, - ...useCollection(queryFilter, PAGE_SIZE), + ...useCollection(queryFilter, { + pageSize: PAGE_SIZE, + server: new URL(resource.subject).origin, + }), }; } diff --git a/browser/react/src/useCollection.ts b/browser/react/src/useCollection.ts index 40d0b2197..256ee3b1b 100644 --- a/browser/react/src/useCollection.ts +++ b/browser/react/src/useCollection.ts @@ -6,7 +6,6 @@ import { Store, } from '@tomic/lib'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useServerURL } from './useServerURL.js'; import { useStore } from './hooks.js'; export type UseCollectionResult = { @@ -14,9 +13,16 @@ export type UseCollectionResult = { invalidateCollection: () => Promise; }; +export type UseCollectionOptions = { + /** The max number of members on one page, defaults to 30 */ + pageSize?: number; + /** URL of the server that should be queried. defaults to the store's serverURL */ + server?: string; +}; + const buildCollection = ( store: Store, - server: string, + server: string | undefined, { property, value, sort_by, sort_desc }: QueryFilter, pageSize?: number, ) => { @@ -38,13 +44,14 @@ const buildCollection = ( */ export function useCollection( queryFilter: QueryFilter, - pageSize?: number, + { pageSize, server }: UseCollectionOptions = { + pageSize: undefined, + server: undefined, + }, ): UseCollectionResult { const [firstRun, setFirstRun] = useState(true); const store = useStore(); - const [server] = useServerURL(); - const queryFilterMemo = useQueryFilterMemo(queryFilter); const [collection, setCollection] = useState(() => diff --git a/docs/src/react/useCollection.md b/docs/src/react/useCollection.md index cc22b0bc1..1a0d0b597 100644 --- a/docs/src/react/useCollection.md +++ b/docs/src/react/useCollection.md @@ -15,26 +15,33 @@ const { collection ,invalidateCollection } = useCollection({ ### Parameters -- **query**: QueryFilter - The query used to build the collection -- **pageSize**: number - The max number of items per page +- **query**: [QueryFilter](#queryfilter) - The query used to build the collection +- **options**: [UseCollectionOptions?](#usecollectionoptions) - An options object described below. + ### Returns Returns an object containing the following items: - **collection**: [Collection](../js-lib/collection.md) - The collection. -- **invalidateCollection**: `function` - A function to invalidate and re-fetch the collection. +- **invalidateCollection**: `() => void` - A function to invalidate and re-fetch the collection. -## QueryFilter +### QueryFilter A QueryFilter is an object with the following properties: | Name | Type | Description | | --- | --- | --- | -| property | `string` | The subject of the property you want to filter by. | -| value | `string` | The value of the property you want to filter by. | -| sort_by | `string` | The subject of the property you want to sort by. By default collections are sorted by subject | -| sort_desc | `boolean` | If true, the collection will be sorted in descending order. (Default: false) | +| property | `string?` | The subject of the property you want to filter by. | +| value | `string?` | The value of the property you want to filter by. | +| sort_by | `string?` | The subject of the property you want to sort by. By default collections are sorted by subject | +| sort_desc | `boolean?` | If true, the collection will be sorted in descending order. (Default: false) | + +### UseCollectionOptions +| Name | Type | Description | +| --- | --- | --- | +| pageSize | `number?` | The max number of members per page. Defaults to 30 | +| server | `string?` | The server that this collection should query. Defaults to the store's serverURL | ## Additional Hooks From bd1017bab715dc6e2ef53fad5aa982d8c6ac3943 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Thu, 20 Jun 2024 16:55:46 +0200 Subject: [PATCH 03/13] Tweak new resource buttons appearance --- browser/data-browser/src/components/Button.tsx | 2 +- .../src/components/SideBar/SideBarDrive.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/browser/data-browser/src/components/Button.tsx b/browser/data-browser/src/components/Button.tsx index cb8a99226..31be00e89 100644 --- a/browser/data-browser/src/components/Button.tsx +++ b/browser/data-browser/src/components/Button.tsx @@ -195,7 +195,7 @@ export const ButtonSubtle = styled(ButtonDefault)` --button-text-color: ${p => p.theme.colors.textLight}; --button-text-color-hover: ${p => p.theme.colors.main}; - box-shadow: ${p => p.theme.boxShadow}; + box-shadow: ${p => (p.theme.darkMode ? 'none' : p.theme.boxShadow)}; `; export const ButtonAlert = styled(ButtonDefault)` diff --git a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx index a37c12af4..d952731ce 100644 --- a/browser/data-browser/src/components/SideBar/SideBarDrive.tsx +++ b/browser/data-browser/src/components/SideBar/SideBarDrive.tsx @@ -192,16 +192,28 @@ const AddButton = styled.button` appearance: none; border: 1px dashed ${p => p.theme.colors.bg2}; border-radius: ${p => p.theme.radius}; - width: calc(100% - 4rem); + width: calc(100% - 5rem); padding-block: 0.3rem; - margin-inline-start: 1.5rem; - margin-block: 0.5rem; + margin-inline-start: 2rem; + margin-block-start: 0.5rem; + margin-block-end: 1rem; cursor: pointer; ${transition('color', 'border')} + & svg { + ${transition('transform')} + } &:hover, &:focus-visible { color: ${p => p.theme.colors.main}; border: 1px solid ${p => p.theme.colors.main}; + + & svg { + transform: scale(1.3); + } + } + + &:active { + background-color: ${p => p.theme.colors.bg1}; } `; From 1c3b2bb02293b1f6bf5dfa97da94af295dee8e49 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 24 Jun 2024 11:19:06 +0200 Subject: [PATCH 04/13] Change ulid to ulidx lib to support non-secure contexts --- browser/CHANGELOG.md | 1 + browser/lib/package.json | 6 ++++-- browser/lib/src/resource.ts | 11 ++++++++++- browser/lib/src/store.ts | 4 +--- browser/pnpm-lock.yaml | 17 +++++++++++++++-- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index ccb162589..f8a5393c6 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -43,6 +43,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Added `resource.setVersion()` method. - Added `collection.getMembersOnPage()` method. - Added `collection.totalPages`. +- Fix lib not working in non-secure browser contexts. - 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. diff --git a/browser/lib/package.json b/browser/lib/package.json index 0a495b395..f3021715b 100644 --- a/browser/lib/package.json +++ b/browser/lib/package.json @@ -7,7 +7,7 @@ "base64-arraybuffer": "^1.0.2", "cross-fetch": "^3.1.4", "fast-json-stable-stringify": "^2.1.0", - "ulid": "^2.3.0" + "ulidx": "^2.3.0" }, "description": "", "devDependencies": { @@ -20,7 +20,9 @@ "vitest": "^0.34.6", "whatwg-fetch": "^3.6.2" }, - "files": ["dist"], + "files": [ + "dist" + ], "gitHead": "2172c73d8df4e5f273e6386676abc91b6c5b2707", "license": "MIT", "main": "dist/index.js", diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index 15074aa4a..8d3e9a62f 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -691,7 +691,16 @@ export class Resource { if (validate) { const fullProp = await this.store.getProperty(prop); - validateDatatype(value, fullProp.datatype); + + try { + validateDatatype(value, fullProp.datatype); + } catch (e) { + if (e instanceof Error) { + e.message = `Error validating ${fullProp.shortname} with value ${value} for ${this.subject}: ${e.message}`; + } + + throw e; + } } if (value === undefined) { diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index 722714ca5..1903ed6d1 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -1,5 +1,4 @@ -import { ulid } from 'ulid'; - +import { ulid } from 'ulidx'; import type { Agent } from './agent.js'; import { removeCookieAuthentication, @@ -148,7 +147,6 @@ export class Store { this.client.setFetch(fetchOverride); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public addResources( resources: Resource | Resource[], opts?: AddResourcesOpts, diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 0b040eebe..a5f02fa49 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -328,7 +328,7 @@ importers: fast-json-stable-stringify: specifier: ^2.1.0 version: 2.1.0 - ulid: + ulidx: specifier: ^2.3.0 version: 2.3.0 devDependencies: @@ -6984,6 +6984,9 @@ packages: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} + layerr@2.1.0: + resolution: {integrity: sha512-xDD9suWxfBYeXgqffRVH/Wqh+mqZrQcqPRn0I0ijl7iJQ7vu8gMGPt1Qop59pEW/jaIDNUN7+PX1Qk40+vuflg==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -9683,6 +9686,10 @@ packages: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true + ulidx@2.3.0: + resolution: {integrity: sha512-36piWNqcdp9hKlQewyeehCaALy4lyx3FodsCxHuV6i0YdexSkjDOubwxEVr2yi4kh62L/0MgyrxqG4K+qtovnw==} + engines: {node: '>=16'} + unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -14341,7 +14348,7 @@ snapshots: '@types/react-dom@18.2.14': dependencies: - '@types/react': 18.2.34 + '@types/react': 18.3.1 '@types/react-pdf@6.2.0': dependencies: @@ -18157,6 +18164,8 @@ snapshots: dependencies: package-json: 8.1.1 + layerr@2.1.0: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -21420,6 +21429,10 @@ snapshots: ulid@2.3.0: {} + ulidx@2.3.0: + dependencies: + layerr: 2.1.0 + unbox-primitive@1.0.2: dependencies: call-bind: 1.0.2 From 783619f27a9d8e906f77e8cb7949920e33c1e8da Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 24 Jun 2024 17:03:24 +0200 Subject: [PATCH 05/13] Fix build issue in js cli --- browser/cli/package.json | 2 +- browser/data-browser/package.json | 3 ++ browser/package.json | 4 --- browser/pnpm-lock.yaml | 52 +++++++++++++------------------ browser/react/package.json | 9 ++++-- 5 files changed, 33 insertions(+), 37 deletions(-) diff --git a/browser/cli/package.json b/browser/cli/package.json index d72657d0a..9a6a6a953 100644 --- a/browser/cli/package.json +++ b/browser/cli/package.json @@ -22,7 +22,7 @@ "prepublishOnly": "pnpm run build && pnpm run lint-fix", "watch": "tsc --build --watch", "start": "pnpm watch", - "tsc": "tsc --build", + "tsc": "pnpm exec tsc --build", "typecheck": "pnpm exec tsc --noEmit" }, "bin": { diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index fcadda89c..923d71935 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -57,6 +57,9 @@ "devDependencies": { "@swc/plugin-styled-components": "^1.5.110", "@types/prismjs": "^1.26.4", + "@types/react": "^18.2.34", + "@types/react-dom": "^18.2.14", + "@types/react-router-dom": "^5.3.3", "@types/react-pdf": "^6.2.0", "@types/react-window": "^1.8.7", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/browser/package.json b/browser/package.json index 596a7ac5d..cac894ce7 100644 --- a/browser/package.json +++ b/browser/package.json @@ -5,9 +5,6 @@ "@types/chai": "^4.2.22", "@types/jest": "^27.0.2", "@types/node": "^20.11.5", - "@types/react": "^18.2.34", - "@types/react-dom": "^18.2.14", - "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "chai": "^4.3.4", @@ -22,7 +19,6 @@ "netlify-cli": "16.2.0", "prettier": "3.2.5", "prettier-plugin-jsdoc": "^1.3.0", - "react": "^18.2.0", "ts-jest": "^29.0.1", "typedoc": "^0.25.3", "typedoc-plugin-missing-exports": "^2.1.0", diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index a5f02fa49..b68d6b089 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -23,15 +23,6 @@ importers: '@types/node': specifier: ^20.11.5 version: 20.11.5 - '@types/react': - specifier: ^18.2.34 - version: 18.2.34 - '@types/react-dom': - specifier: ^18.2.14 - version: 18.2.14 - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 '@typescript-eslint/eslint-plugin': specifier: ^7.8.0 version: 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -77,9 +68,6 @@ importers: prettier-plugin-jsdoc: specifier: ^1.3.0 version: 1.3.0(prettier@3.2.5) - react: - specifier: ^18.2.0 - version: 18.2.0 ts-jest: specifier: ^29.0.1 version: 29.1.1(@babel/core@7.24.5)(@jest/types@29.6.1)(babel-jest@29.6.2(@babel/core@7.24.5))(jest@29.6.2(@types/node@20.11.5)(ts-node@10.9.1(@swc/core@1.3.104)(@types/node@20.11.5)(typescript@5.4.5)))(typescript@5.4.5) @@ -265,9 +253,18 @@ importers: '@types/prismjs': specifier: ^1.26.4 version: 1.26.4 + '@types/react': + specifier: ^18.2.34 + version: 18.3.1 + '@types/react-dom': + specifier: ^18.2.14 + version: 18.2.14 '@types/react-pdf': specifier: ^6.2.0 version: 6.2.0 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@types/react-window': specifier: ^1.8.7 version: 1.8.7 @@ -366,6 +363,15 @@ importers: specifier: '>17.0.2' version: 18.2.0 devDependencies: + '@types/react': + specifier: ^18.2.34 + version: 18.3.1 + '@types/react-dom': + specifier: ^18.2.14 + version: 18.2.14 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 typescript: specifier: ^5.4.5 version: 5.4.5 @@ -3789,9 +3795,6 @@ packages: '@types/react-window@1.8.7': resolution: {integrity: sha512-FpPHEhmGVOBKomuR4LD2nvua1Ajcw6PfnfbDysuCwwPae3JNulcq3+uZIpQNbDN2AI1z+Y4tKj2xQ4ELiQ4QDw==} - '@types/react@18.2.34': - resolution: {integrity: sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==} - '@types/react@18.3.1': resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} @@ -3807,9 +3810,6 @@ packages: '@types/retry@0.12.1': resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} - '@types/scheduler@0.16.3': - resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -14352,7 +14352,7 @@ snapshots: '@types/react-pdf@6.2.0': dependencies: - '@types/react': 18.2.34 + '@types/react': 18.3.1 pdfjs-dist: 2.16.105 transitivePeerDependencies: - worker-loader @@ -14360,23 +14360,17 @@ snapshots: '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.34 + '@types/react': 18.3.1 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.34 + '@types/react': 18.3.1 '@types/react-window@1.8.7': dependencies: - '@types/react': 18.2.34 - - '@types/react@18.2.34': - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.2 + '@types/react': 18.3.1 '@types/react@18.3.1': dependencies: @@ -14395,8 +14389,6 @@ snapshots: '@types/retry@0.12.1': {} - '@types/scheduler@0.16.3': {} - '@types/semver@7.5.8': {} '@types/stack-utils@2.0.1': {} diff --git a/browser/react/package.json b/browser/react/package.json index a8a744759..40714e63b 100644 --- a/browser/react/package.json +++ b/browser/react/package.json @@ -6,12 +6,17 @@ "@tomic/lib": "workspace:*" }, "devDependencies": { - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "@types/react": "^18.2.34", + "@types/react-dom": "^18.2.14", + "@types/react-router-dom": "^5.3.3" }, "peerDependencies": { "react": ">17.0.2" }, - "files": ["dist"], + "files": [ + "dist" + ], "license": "MIT", "name": "@tomic/react", "main-dev": "src/index.ts", From 4bf258fee4fb412276613149397689262b04a58a Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Tue, 25 Jun 2024 13:07:43 +0200 Subject: [PATCH 06/13] Fix default ontology not generating on first server setup --- .../src/views/OntologyPage/OntologyPage.tsx | 6 +- lib/src/populate.rs | 55 ++++++++++++++++++- lib/src/urls.rs | 3 + 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 65afb4f42..7aed1f94c 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -18,13 +18,17 @@ import { Graph } from './Graph'; import { CreateInstanceButton } from './CreateInstanceButton'; import { useState } from 'react'; +const isEmpty = (arr: Array) => arr.length === 0; + export function OntologyPage({ resource }: ResourcePageProps) { 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); + const [editMode, setEditMode] = useState( + isEmpty(classes) && isEmpty(properties) && isEmpty(instances), + ); return ( diff --git a/lib/src/populate.rs b/lib/src/populate.rs index 197bb888d..829850830 100644 --- a/lib/src/populate.rs +++ b/lib/src/populate.rs @@ -12,6 +12,8 @@ use crate::{ urls, Storelike, Value, }; +const DEFAULT_ONTOLOGY_PATH: &str = "defaultOntology"; + /// Populates a store with some of the most fundamental Properties and Classes needed to bootstrap the whole. /// This is necessary to prevent a loop where Property X (like the `shortname` Property) /// cannot be added, because it's Property Y (like `description`) has to be fetched before it can be added, @@ -165,6 +167,42 @@ pub fn create_drive(store: &impl Storelike) -> AtomicResult<()> { store, )?; drive.save_locally(store)?; + + Ok(()) +} + +pub fn create_default_ontology(store: &impl Storelike) -> AtomicResult<()> { + let mut drive = store.get_resource(store.get_server_url())?; + + let ontology_subject = format!("{}/{}", drive.get_subject(), DEFAULT_ONTOLOGY_PATH); + + // If the ontology already exists, don't change it. + if store.get_resource(&ontology_subject).is_ok() { + return Ok(()); + } + + let mut ontology = store.get_resource_new(&ontology_subject); + + ontology.set_class(urls::ONTOLOGY); + ontology.set_string(urls::SHORTNAME.into(), "ontology", store)?; + ontology.set_string( + urls::DESCRIPTION.into(), + "Default ontology for this drive", + store, + )?; + ontology.set_string(urls::PARENT.into(), drive.get_subject(), store)?; + ontology.set(urls::CLASSES.into(), Value::ResourceArray(vec![]), store)?; + ontology.set(urls::PROPERTIES.into(), Value::ResourceArray(vec![]), store)?; + ontology.set(urls::INSTANCES.into(), Value::ResourceArray(vec![]), store)?; + ontology.save_locally(store)?; + + drive.set_string(urls::DEFAULT_ONTOLOGY.into(), ontology.get_subject(), store)?; + drive.push( + urls::SUBRESOURCES, + crate::values::SubResource::Subject(ontology.get_subject().into()), + false, + )?; + drive.save_locally(store)?; Ok(()) } @@ -183,11 +221,20 @@ pub fn set_drive_rights(store: &impl Storelike, public_read: bool) -> AtomicResu if let Err(_no_description) = drive.get(urls::DESCRIPTION) { drive.set_string(urls::DESCRIPTION.into(), &format!(r#"## Welcome to your Atomic-Server! - -Register your Agent by visiting [`/setup`]({}/setup). After that, edit this page by pressing `edit` in the navigation bar menu. +### Getting started +Start by registering your Agent by visiting [`/setup`]({}/setup). Note that, by default, all resources are `public`. You can edit this by opening the context menu (the three dots in the navigation bar), and going to `share`. -"#, store.get_server_url()), store)?; + +Once you've setup an agent you should start editing your schema using ontologies. +We've created a [default ontology]({}) for you but you can create more if you want. + +Next create some resources by clicking on the plus button in the sidebar. +You can create folders to organise your resources. + +To use the data in your web apps checkout our client libraries: [@tomic/lib](https://docs.atomicdata.dev/js), [@tomic/react](https://docs.atomicdata.dev/usecases/react) and [@tomic/svelte](https://docs.atomicdata.dev/svelte) +Use [@tomic/cli](https://docs.atomicdata.dev/js-cli) to generate typed ontologies inside your code. +"#, store.get_server_url(), &format!("{}/{}", drive.get_subject(), DEFAULT_ONTOLOGY_PATH)), store)?; } drive.save_locally(store)?; Ok(()) @@ -296,6 +343,8 @@ pub fn populate_all(store: &crate::Db) -> AtomicResult<()> { populate_default_store(store) .map_err(|e| format!("Failed to populate default store. {}", e))?; create_drive(store).map_err(|e| format!("Failed to create drive. {}", e))?; + create_default_ontology(store) + .map_err(|e| format!("Failed to create default ontology. {}", e))?; set_drive_rights(store, true)?; populate_collections(store).map_err(|e| format!("Failed to populate collections. {}", e))?; populate_endpoints(store).map_err(|e| format!("Failed to populate endpoints. {}", e))?; diff --git a/lib/src/urls.rs b/lib/src/urls.rs index 68279b899..335150d09 100644 --- a/lib/src/urls.rs +++ b/lib/src/urls.rs @@ -37,6 +37,9 @@ pub const ALLOWS_ONLY: &str = "https://atomicdata.dev/properties/allowsOnly"; // ... for Classes pub const REQUIRES: &str = "https://atomicdata.dev/properties/requires"; pub const RECOMMENDS: &str = "https://atomicdata.dev/properties/recommends"; +// ... for Drives +pub const DEFAULT_ONTOLOGY: &str = + "https://atomicdata.dev/ontology/server/property/default-ontology"; // ... for Commits pub const SUBJECT: &str = "https://atomicdata.dev/properties/subject"; pub const SET: &str = "https://atomicdata.dev/properties/set"; From 45b74ec0ad5d39d26dfe8b9dad8f7ddfb7dd2378 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Tue, 25 Jun 2024 14:46:30 +0200 Subject: [PATCH 07/13] Fix resource-array props not removed when input is empty --- browser/CHANGELOG.md | 1 + .../src/components/SideBar/DriveSwitcher.tsx | 2 +- .../src/components/forms/InputResourceArray.tsx | 4 ++-- .../views/OntologyPage/PropertyDatatypePicker.tsx | 12 ++++++------ browser/e2e/tests/filePicker.spec.ts | 2 -- browser/e2e/tests/ontology.spec.ts | 1 - 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index f8a5393c6..50770de67 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -13,6 +13,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#896](https://github.com/atomicdata-dev/atomic-server/issues/896) Fix an issue where sidebar items require a double tap on iOS. - Updated the look & feel of the sidebar a bit. - [#893](https://github.com/atomicdata-dev/atomic-server/issues/893) Fix tables not showing any rows when viewing from a different server. +- Fix an issue where the resource-array properties would be set to an empty array instead of removing the property when removing all items in the input. ### @tomic/react diff --git a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx index 511918c1a..6d4ba73c7 100644 --- a/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx +++ b/browser/data-browser/src/components/SideBar/DriveSwitcher.tsx @@ -91,7 +91,7 @@ export function DriveSwitcher() { disabled: !agent, }, ], - [savedDrivesMap, drive, historyMap], + [savedDrivesMap, drive, historyMap, agent], ); return ; diff --git a/browser/data-browser/src/components/forms/InputResourceArray.tsx b/browser/data-browser/src/components/forms/InputResourceArray.tsx index 1f486abad..292d6e422 100644 --- a/browser/data-browser/src/components/forms/InputResourceArray.tsx +++ b/browser/data-browser/src/components/forms/InputResourceArray.tsx @@ -41,14 +41,14 @@ export default function InputResourceArray({ } function handleClear() { - setArray([]); + setArray(undefined); } const handleRemoveRowList = useIndexDependantCallback( (index: number) => () => { const newArray = [...array]; newArray.splice(index, 1); - setArray(newArray); + setArray(newArray.length === 0 ? undefined : newArray); }, array, [setArray], diff --git a/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx b/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx index 61e9df0e6..6d889b307 100644 --- a/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx +++ b/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx @@ -19,6 +19,12 @@ const options = Object.entries(reverseDatatypeMapping) })) .filter(x => x.value !== 'unknown-datatype'); +const isResourceLike = (datatype: string) => { + return ( + datatype === Datatype.ATOMIC_URL || datatype === Datatype.RESOURCEARRAY + ); +}; + export function PropertyDatatypePicker({ resource, disabled, @@ -30,12 +36,6 @@ export function PropertyDatatypePicker({ commit: true, }); - const isResourceLike = (datatype: string) => { - return ( - datatype === Datatype.ATOMIC_URL || datatype === Datatype.RESOURCEARRAY - ); - }; - const clearInapplicableProps = (datatype: string) => { if (!isResourceLike(datatype)) { setClassType(undefined); diff --git a/browser/e2e/tests/filePicker.spec.ts b/browser/e2e/tests/filePicker.spec.ts index 328dc0762..01b76c606 100644 --- a/browser/e2e/tests/filePicker.spec.ts +++ b/browser/e2e/tests/filePicker.spec.ts @@ -42,8 +42,6 @@ const createModel = async (page: Page) => { await expect(page.locator(`h1:has-text("${ONTOLOGY_NAME}")`)).toBeVisible(); - page.getByRole('button', { name: 'Edit', exact: true }).click(); - await page.getByRole('button', { name: 'Add class', exact: true }).click(); await page.getByPlaceholder('shortname').fill('robot'); await page.getByRole('button', { name: 'Save' }).click(); diff --git a/browser/e2e/tests/ontology.spec.ts b/browser/e2e/tests/ontology.spec.ts index 9ca6bb7dc..764e5ce67 100644 --- a/browser/e2e/tests/ontology.spec.ts +++ b/browser/e2e/tests/ontology.spec.ts @@ -36,7 +36,6 @@ test.describe('Ontology', async () => { await page.locator('dialog[open] button:has-text("Create")').click(); await expect(page.locator(`h1:has-text("${ontologyName}")`)).toBeVisible(); - await page.getByRole('button', { name: 'Edit', exact: true }).click(); await page .getByTestId('markdown-editor') .fill('Data model for youtube thumbnail editor'); From 69b9599f45f5d791f65309ba7111eff80d431c55 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 1 Jul 2024 15:08:21 +0200 Subject: [PATCH 08/13] Fix dropdowns jumping --- .../src/components/Dropdown/index.tsx | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx index 83dbd97a9..d8698bf28 100644 --- a/browser/data-browser/src/components/Dropdown/index.tsx +++ b/browser/data-browser/src/components/Dropdown/index.tsx @@ -129,9 +129,7 @@ export function DropdownMenu({ const handleClose = useCallback(() => { triggerRef.current?.focus(); - setTimeout(() => { - setIsActive(false); - }, 100); + setIsActive(false); }, [setIsActive]); useClickAwayListener([triggerRef, dropdownRef], handleClose, isActive, [ @@ -140,8 +138,6 @@ export function DropdownMenu({ const normalizedItems = useMemo(() => normalizeItems(items), [items]); - const [x, setX] = useState(0); - const [y, setY] = useState(0); const getNewIndex = createIndexOffset(normalizedItems); const [selectedIndex, setSelectedIndex] = useState(getNewIndex(0, 0)); // if the keyboard is used to navigate the menu items @@ -167,25 +163,21 @@ export function DropdownMenu({ // If the top is outside of the screen, render it below if (topPos < 0) { - setY(triggerRect.y + triggerRect.height / 2); + dropdownRef.current.style.top = `${triggerRect.y + triggerRect.height / 2}px`; } else { - setY(topPos + triggerRect.height / 2); + dropdownRef.current.style.top = `${topPos + triggerRect.height / 2}px`; } const leftPos = triggerRect.x - menuRect.width; // If the left is outside of the screen, render it to the right if (leftPos < 0) { - setX(triggerRect.x); + dropdownRef.current.style.left = `${triggerRect.x}px`; } else { - setX(triggerRect.x - menuRect.width + triggerRect.width); + dropdownRef.current.style.left = `${triggerRect.x - menuRect.width + triggerRect.width}px`; } - // The dropdown is hidden at first because in the first few frames it is still position at 0,0. - // We only want to show the dropdown after it has been positioned correctly. - requestAnimationFrame(() => { - dropdownRef.current!.style.visibility = 'visible'; - }); + dropdownRef.current.style.visibility = 'visible'; }); }, [isActive, setIsActive]); @@ -285,8 +277,6 @@ export function DropdownMenu({ { interface MenuProps { isActive: boolean; - x: number; - y: number; } export interface MenuItemSidebarProps extends MenuItemMinimial { @@ -457,11 +445,12 @@ const Menu = styled.div` border-radius: 8px; position: fixed; z-index: ${p => p.theme.zIndex.dropdown}; - top: ${p => p.y}px; - left: ${p => p.x}px; width: auto; box-shadow: ${p => p.theme.boxShadowSoft}; opacity: ${p => (p.isActive ? 1 : 0)}; + @starting-style { + opacity: 0; + } ${transition('opacity')}; `; From 76eade1442ad7f6f28fdc6ecd9946544cf117f8a Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 1 Jul 2024 15:36:57 +0200 Subject: [PATCH 09/13] Add TagPage --- browser/CHANGELOG.md | 7 + browser/data-browser/package.json | 4 +- .../src/chunks/EmojiInput/EmojiInput.tsx | 9 +- .../src/chunks/MarkdownEditor/BubbleMenu.tsx | 1 + browser/data-browser/src/components/Card.tsx | 7 +- .../data-browser/src/components/Details.tsx | 14 +- .../src/components/EditableTitle.tsx | 7 +- browser/data-browser/src/components/Main.tsx | 12 +- .../src/components/PalettePicker.tsx | 9 +- .../data-browser/src/components/Popover.tsx | 5 +- .../ResourceUsage/ReferenceUsage.tsx | 27 ++ .../ResourceUsage/ResourceUsage.tsx | 18 +- .../components/ResourceUsage/UsageCard.tsx | 71 +++- .../src/components/ResourceUsage/UsageRow.tsx | 27 +- browser/data-browser/src/components/Row.tsx | 4 + .../ResourceSideBar/SidebarItemTitle.tsx | 7 +- .../src/components/SideBar/useSidebarDnd.ts | 7 +- .../src/components/Tag/CreateTagRow.tsx | 35 +- .../data-browser/src/components/Tag/Tag.tsx | 7 +- .../src/components/forms/EmojiInput.tsx | 21 + .../src/components/forms/InputSlug.tsx | 2 + .../src/components/forms/InputString.tsx | 2 + .../src/components/forms/ResourceField.tsx | 3 + .../src/helpers/transitionName.ts | 5 + .../src/views/Article/ArticleCard.tsx | 7 +- .../src/views/Card/ResourceCardTitle.tsx | 13 +- .../File/useFileImageTransitionStyles.ts | 7 +- .../views/FolderPage/GridItem/components.tsx | 10 +- .../src/views/FolderPage/iconMap.ts | 44 +- .../OntologyPage/Class/ClassCardRead.tsx | 7 +- .../views/OntologyPage/OntologySidebar.tsx | 6 +- .../data-browser/src/views/ResourcePage.tsx | 4 + .../TablePage/EditorCells/AtomicURLCell.tsx | 1 + .../EditorCells/MultiRelationCell.tsx | 1 + .../TablePage/EditorCells/SelectCell.tsx | 1 + .../src/views/TagPage/TagPage.tsx | 77 ++++ .../src/views/TagPage/TagPropertyCard.tsx | 65 +++ browser/lib/src/resource.ts | 22 + browser/pnpm-lock.yaml | 377 ++++++++++++------ browser/react/src/hooks.ts | 9 + 40 files changed, 707 insertions(+), 255 deletions(-) create mode 100644 browser/data-browser/src/components/ResourceUsage/ReferenceUsage.tsx create mode 100644 browser/data-browser/src/components/forms/EmojiInput.tsx create mode 100644 browser/data-browser/src/views/TagPage/TagPage.tsx create mode 100644 browser/data-browser/src/views/TagPage/TagPropertyCard.tsx diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 50770de67..d65c58353 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -14,10 +14,17 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Updated the look & feel of the sidebar a bit. - [#893](https://github.com/atomicdata-dev/atomic-server/issues/893) Fix tables not showing any rows when viewing from a different server. - Fix an issue where the resource-array properties would be set to an empty array instead of removing the property when removing all items in the input. +- Fix an issue where dropdown menus sometimes jump from the upper left corner of the screen. +- Added a full page view for tags. + +### @tomic/lib + +- Added `LocalChange` event to `Resource`. ### @tomic/react - BREAKING CHANGE: Removed the `useLocalStorage` hook. +- When using any `useValue` type hook, values will now update when local changes are made to the resource from elsewhere in the app. ## v0.38.0 diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index 923d71935..d65790c46 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -14,7 +14,7 @@ "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.2.1", - "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.0.1", "@radix-ui/react-tabs": "^1.0.4", "@tiptap/extension-image": "^2.4.0", @@ -26,7 +26,7 @@ "@tiptap/starter-kit": "^2.3.0", "@tiptap/suggestion": "^2.4.0", "@tomic/react": "workspace:*", - "emoji-mart": "^5.5.2", + "emoji-mart": "^5.6.0", "polished": "^4.1.0", "prismjs": "^1.29.0", "query-string": "^7.0.0", diff --git a/browser/data-browser/src/chunks/EmojiInput/EmojiInput.tsx b/browser/data-browser/src/chunks/EmojiInput/EmojiInput.tsx index a8d9d53c2..5610abe7a 100644 --- a/browser/data-browser/src/chunks/EmojiInput/EmojiInput.tsx +++ b/browser/data-browser/src/chunks/EmojiInput/EmojiInput.tsx @@ -5,7 +5,7 @@ import * as RadixPopover from '@radix-ui/react-popover'; import { transition } from '../../helpers/transition'; import { Popover } from '../../components/Popover'; -interface EmojiInputProps { +export interface EmojiInputProps { initialValue?: string; onChange: (value: string | undefined) => void; } @@ -25,7 +25,7 @@ const fetchAndCacheData = async () => { return data; }; -export default function EmojiInput({ +export default function EmojiInputASYNC({ initialValue, onChange, }: EmojiInputProps): JSX.Element { @@ -63,7 +63,8 @@ export default function EmojiInput({ } const Preview = styled.span` - transition: ${transition('font-size')}; + will-change: font-size; + ${transition('font-size')}; `; const Placeholder = styled(Preview)` @@ -74,10 +75,10 @@ const PickerButton = styled(RadixPopover.Trigger)` border: none; border-radius: ${({ theme }) => theme.radius}; width: 2rem; + height: 2rem; background: transparent; padding: 0; cursor: pointer; - user-select: none; &:hover > ${Preview} { diff --git a/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx b/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx index d204d174b..538c4be9e 100644 --- a/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx +++ b/browser/data-browser/src/chunks/MarkdownEditor/BubbleMenu.tsx @@ -72,6 +72,7 @@ export function BubbleMenu(): React.JSX.Element { (p => ({ // When we render a lot of cards it is more performant to use styles instead of classes when each card has a unique style - style: getTransitionStyle('resource-page', p.about), + style: getTransitionStyle(RESOURCE_PAGE_TRANSITION_TAG, p.about), }))` background-color: ${props => props.theme.colors.bg}; diff --git a/browser/data-browser/src/components/Details.tsx b/browser/data-browser/src/components/Details.tsx index 46ed6c84a..c543978ca 100644 --- a/browser/data-browser/src/components/Details.tsx +++ b/browser/data-browser/src/components/Details.tsx @@ -10,6 +10,7 @@ export interface DetailsProps { disabled?: boolean; /** Event that fires when a user opens or closes the details */ onStateToggle?: (state: boolean) => void; + noIndent?: boolean; } /** A collapsible item with a title. Similar to the
HTML element. */ @@ -19,6 +20,7 @@ export function Details({ children, title, disabled, + noIndent, onStateToggle, }: PropsWithChildren): JSX.Element { const [isOpen, setIsOpen] = useState(initialState); @@ -27,6 +29,10 @@ export function Details({ setIsOpen(open); }, [open]); + useEffect(() => { + setIsOpen(initialState); + }, [initialState]); + const toggleOpen = useCallback(() => { setIsOpen(p => { onStateToggle?.(!p); @@ -49,7 +55,9 @@ export function Details({ {title} - {children} + + {children} + ); } @@ -108,7 +116,7 @@ const IconButton = styled.button` } `; -const StyledCollapse = styled(Collapse)` +const StyledCollapse = styled(Collapse)<{ noIndent?: boolean }>` overflow-x: hidden; - margin-left: ${({ theme }) => theme.margin}rem; + margin-left: ${p => (p.noIndent ? 0 : p.theme.margin) + 'rem'}; `; diff --git a/browser/data-browser/src/components/EditableTitle.tsx b/browser/data-browser/src/components/EditableTitle.tsx index cfb7b1529..fc5e66694 100644 --- a/browser/data-browser/src/components/EditableTitle.tsx +++ b/browser/data-browser/src/components/EditableTitle.tsx @@ -3,7 +3,10 @@ import { useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { FaPencil } from 'react-icons/fa6'; import { styled, css } from 'styled-components'; -import { transitionName } from '../helpers/transitionName'; +import { + PAGE_TITLE_TRANSITION_TAG, + transitionName, +} from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; export interface EditableTitleProps { @@ -110,7 +113,7 @@ const Title = styled.h1` cursor: ${props => (props.canEdit ? 'pointer' : 'initial')}; opacity: ${props => (props.subtle ? 0.5 : 1)}; - ${props => transitionName('page-title', props.subject)}; + ${props => transitionName(PAGE_TITLE_TRANSITION_TAG, props.subject)}; `; const TitleInput = styled.input` diff --git a/browser/data-browser/src/components/Main.tsx b/browser/data-browser/src/components/Main.tsx index ab1388db5..dbf23923c 100644 --- a/browser/data-browser/src/components/Main.tsx +++ b/browser/data-browser/src/components/Main.tsx @@ -1,9 +1,11 @@ import { PropsWithChildren, memo } from 'react'; import { VisuallyHidden } from './VisuallyHidden'; import { styled } from 'styled-components'; -import { transitionName } from '../helpers/transitionName'; +import { + RESOURCE_PAGE_TRANSITION_TAG, + transitionName, +} from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; -import { PARENT_PADDING_BLOCK } from './Parent'; import { MAIN_CONTAINER } from '../helpers/containers'; /** Main landmark. Every page should have one of these. @@ -26,9 +28,5 @@ export function Main({ const StyledMain = memo(styled.main` container: ${MAIN_CONTAINER} / inline-size; - /* Makes the contents fit the entire page */ - /* height: calc( - 100% - (${p => p.theme.heights.breadCrumbBar} + ${PARENT_PADDING_BLOCK} * 2) - ); */ - ${p => transitionName('resource-page', p.subject)} + ${p => transitionName(RESOURCE_PAGE_TRANSITION_TAG, p.subject)} `); diff --git a/browser/data-browser/src/components/PalettePicker.tsx b/browser/data-browser/src/components/PalettePicker.tsx index ac3857414..5ac05a91b 100644 --- a/browser/data-browser/src/components/PalettePicker.tsx +++ b/browser/data-browser/src/components/PalettePicker.tsx @@ -38,11 +38,16 @@ const PaletteButton = styled.button` border-radius: 50%; cursor: pointer; transform-origin: center; - transition: ${transition('transform')}; + transform: scale(1); + ${transition('transform')}; &:hover, - &:focus { + &:focus-visible { outline: none; transform: scale(1.3); } + + &:active { + transform: scale(1.1); + } `; diff --git a/browser/data-browser/src/components/Popover.tsx b/browser/data-browser/src/components/Popover.tsx index 03c3184f4..587cd2d43 100644 --- a/browser/data-browser/src/components/Popover.tsx +++ b/browser/data-browser/src/components/Popover.tsx @@ -24,6 +24,7 @@ export interface PopoverProps { className?: string; noArrow?: boolean; noLock?: boolean; + modal?: boolean; } export function Popover({ @@ -33,6 +34,7 @@ export function Popover({ defaultOpen, noArrow, noLock, + modal, onOpenChange, Trigger, }: PropsWithChildren): JSX.Element { @@ -57,7 +59,7 @@ export function Popover({ return ( p.theme.boxShadowSoft}; border-radius: ${p => p.theme.radius}; - position: relative; z-index: 10000000; animation: ${fadeIn} 0.1s ease-in-out; diff --git a/browser/data-browser/src/components/ResourceUsage/ReferenceUsage.tsx b/browser/data-browser/src/components/ResourceUsage/ReferenceUsage.tsx new file mode 100644 index 000000000..57db356ae --- /dev/null +++ b/browser/data-browser/src/components/ResourceUsage/ReferenceUsage.tsx @@ -0,0 +1,27 @@ +import { useCollection, type Resource } from '@tomic/react'; +import { UsageCard } from './UsageCard'; + +interface ReferenceUsageProps { + resource: Resource; + initialOpenState?: boolean; +} + +export function ReferenceUsage({ + resource, + initialOpenState, +}: ReferenceUsageProps) { + const { collection } = useCollection({ value: resource.subject }); + + return ( + + {collection.totalMembers} resources reference{' '} + {resource.title} + + } + /> + ); +} diff --git a/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx b/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx index 8b910c60f..b78d12afc 100644 --- a/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx +++ b/browser/data-browser/src/components/ResourceUsage/ResourceUsage.tsx @@ -1,10 +1,10 @@ -import { Resource, core, useCollection } from '@tomic/react'; +import { Resource, core } from '@tomic/react'; import { PropertyUsage } from './PropertyUsage'; -import { UsageCard } from './UsageCard'; import { ClassUsage } from './ClassUsage'; import { ChildrenUsage } from './ChildrenUsage'; import { Column } from '../Row'; +import { ReferenceUsage } from './ReferenceUsage'; interface ResourceUsageProps { resource: Resource; @@ -23,22 +23,10 @@ export function ResourceUsage({ resource }: ResourceUsageProps): JSX.Element { } function BasicUsage({ resource }: ResourceUsageProps): JSX.Element { - const { collection } = useCollection({ - value: resource.subject, - }); - return ( - - {collection.totalMembers} resources reference{' '} - {resource.title} - - } - /> + ); } diff --git a/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx b/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx index 9be7128f0..e0910eae4 100644 --- a/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx +++ b/browser/data-browser/src/components/ResourceUsage/UsageCard.tsx @@ -1,39 +1,69 @@ -import { Collection } from '@tomic/react'; +import { Collection, useCollectionPage } from '@tomic/react'; import { styled } from 'styled-components'; import { Details } from '../Details'; import { UsageRow } from './UsageRow'; -import { Column } from '../Row'; +import { Column, Row } from '../Row'; +import { useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'; +import { IconButton } from '../IconButton/IconButton'; interface UsageCardProps { collection: Collection; title: string | React.ReactNode; + initialOpenState?: boolean; } -function mapOverCollection( - collection: Collection, - mapFn: (index: number) => T, -): T[] { - return new Array(Math.min(100, collection.totalMembers)) - .fill(0) - .map((_, i) => mapFn(i)); -} +export function UsageCard({ + collection, + title, + initialOpenState = false, +}: UsageCardProps): JSX.Element { + const [page, setPage] = useState(0); + const [isOpen, setIsOpen] = useState(initialOpenState); + const members = useCollectionPage(collection, page); + + const PageButtons = ( + + setPage(p => p - 1)} + disabled={page === 0} + > + + + {page + 1} + setPage(p => p + 1)} + disabled={page === collection.totalPages - 1} + > + + + + ); -export function UsageCard({ collection, title }: UsageCardProps): JSX.Element { return ( -
{title}}> +
+ {title} + {isOpen && PageButtons} + + } + initialState={initialOpenState} + onStateToggle={setIsOpen} + > - {collection.totalMembers > 100 && ( - - Showing 100 of {collection.totalMembers} - - )} - {mapOverCollection(collection, i => ( - + {/* We need to filter out duplicate members because react will do weird things when duplicate keys are present */} + {Array.from(new Set(members)).map(s => ( + ))} + {PageButtons}
@@ -57,7 +87,6 @@ const ContentWrapper = styled(Column)` margin-top: ${({ theme }) => theme.margin}rem; `; -const LimitMessage = styled.span` - text-align: end; +const PageNumber = styled.span` color: ${({ theme }) => theme.colors.textLight}; `; diff --git a/browser/data-browser/src/components/ResourceUsage/UsageRow.tsx b/browser/data-browser/src/components/ResourceUsage/UsageRow.tsx index 9f94a2a14..abd56541c 100644 --- a/browser/data-browser/src/components/ResourceUsage/UsageRow.tsx +++ b/browser/data-browser/src/components/ResourceUsage/UsageRow.tsx @@ -1,23 +1,17 @@ -import { - Collection, - classes, - unknownSubject, - useMemberFromCollection, -} from '@tomic/react'; +import { commits, unknownSubject, useResource } from '@tomic/react'; import { ResourceInline } from '../../views/ResourceInline'; import { styled } from 'styled-components'; import { ErrorLook } from '../ErrorLook'; interface UsageRowProps { - collection: Collection; - index: number; + subject: string; } -export function UsageRow({ collection, index }: UsageRowProps): JSX.Element { - const resource = useMemberFromCollection(collection, index); +export function UsageRow({ subject }: UsageRowProps): JSX.Element { + const resource = useResource(subject); - if (resource.getSubject() === unknownSubject) { + if (subject === unknownSubject) { return ( Insufficient rights to view resource @@ -25,24 +19,25 @@ export function UsageRow({ collection, index }: UsageRowProps): JSX.Element { ); } - if (resource.hasClasses(classes.commit)) { + if (resource.hasClasses(commits.classes.commit)) { return <>; } return ( - + ); } const ListItem = styled.li` + display: flex; + align-items: center; list-style: none; padding: 0.5rem 1rem; border-radius: ${({ theme }) => theme.radius}; - - margin-left: 0; - + margin: 0; + height: 3rem; &:nth-child(odd) { background-color: ${({ theme }) => theme.colors.bg1}; } diff --git a/browser/data-browser/src/components/Row.tsx b/browser/data-browser/src/components/Row.tsx index cc35c3a10..26f04cd04 100644 --- a/browser/data-browser/src/components/Row.tsx +++ b/browser/data-browser/src/components/Row.tsx @@ -62,6 +62,10 @@ const Flex = styled.div` width: ${p => (p.fullWidth ? '100%' : 'initial')}; height: ${p => (p.fullHeight ? '100%' : 'initial')}; + & > :is(h1, h2, h3, h4, h5, h6) { + margin-bottom: 0; + } + & ${ButtonDefault} { align-self: flex-start; } diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx index 4ec5b9587..8602d2139 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx @@ -7,7 +7,10 @@ import { useResource, useArray, core, useString } from '@tomic/react'; import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; import { DraggableAttributes } from '@dnd-kit/core'; import { StyledLink, TextWrapper } from './shared'; -import { getTransitionName } from '../../../helpers/transitionName'; +import { + SIDEBAR_TRANSITION_TAG, + getTransitionName, +} from '../../../helpers/transitionName'; import { useSettings } from '../../../helpers/AppSettings'; import { IconButton } from '../../IconButton/IconButton'; import { FaGripVertical } from 'react-icons/fa6'; @@ -47,7 +50,7 @@ export const SidebarItemTitle = forwardRef< return ( {sidebarKeyboardDndEnabled ? ( diff --git a/browser/data-browser/src/components/SideBar/useSidebarDnd.ts b/browser/data-browser/src/components/SideBar/useSidebarDnd.ts index 678302cc0..8aeb26813 100644 --- a/browser/data-browser/src/components/SideBar/useSidebarDnd.ts +++ b/browser/data-browser/src/components/SideBar/useSidebarDnd.ts @@ -11,7 +11,10 @@ import { } from '@dnd-kit/core'; import { Resource, core, dataBrowser, useStore } from '@tomic/react'; import { useCallback, useState } from 'react'; -import { getTransitionName } from '../../helpers/transitionName'; +import { + SIDEBAR_TRANSITION_TAG, + getTransitionName, +} from '../../helpers/transitionName'; import { useSettings } from '../../helpers/AppSettings'; export type SideBarDropData = { @@ -108,7 +111,7 @@ export const useSidebarDnd = ( waitForSavePromise?.then(() => { const targetNode = document.querySelector( `[data-sidebar-id="${getTransitionName( - 'sidebar', + SIDEBAR_TRANSITION_TAG, active.id as string, )}"]`, ) as HTMLElement; diff --git a/browser/data-browser/src/components/Tag/CreateTagRow.tsx b/browser/data-browser/src/components/Tag/CreateTagRow.tsx index 0e939f827..1532bf74b 100644 --- a/browser/data-browser/src/components/Tag/CreateTagRow.tsx +++ b/browser/data-browser/src/components/Tag/CreateTagRow.tsx @@ -1,5 +1,5 @@ import { Resource, core, dataBrowser, useStore } from '@tomic/react'; -import { useState, useCallback, Suspense, lazy } from 'react'; +import { useState, useCallback } from 'react'; import { FaPlus } from 'react-icons/fa'; import { randomItem } from '../../helpers/randomItem'; import { stringToSlug } from '../../helpers/stringToSlug'; @@ -7,8 +7,7 @@ import { Button } from '../Button'; import { Row } from '../Row'; import { InputWrapper, InputStyled } from '../forms/InputStyles'; import { tagColours } from './tagColours'; - -const EmojiInput = lazy(() => import('../../chunks/EmojiInput/EmojiInput')); +import { EmojiInput } from '../forms/EmojiInput'; interface CreateTagRowProps { parent: string; @@ -61,21 +60,19 @@ export function CreateTagRow({ parent, onNewTag }: CreateTagRowProps) { ); return ( - Loading...}> - - - - - - - - + + + + + + + ); } diff --git a/browser/data-browser/src/components/Tag/Tag.tsx b/browser/data-browser/src/components/Tag/Tag.tsx index 6d919eba7..f7ed87be7 100644 --- a/browser/data-browser/src/components/Tag/Tag.tsx +++ b/browser/data-browser/src/components/Tag/Tag.tsx @@ -60,10 +60,10 @@ const TagWrapper = styled.span` --tag-light-color: ${props => setSaturation(0.5, setLightness(0.9, props.color))}; display: inline-flex; - gap: 0.5rem; + gap: 1ch; align-items: center; - padding-inline: 0.5rem; - padding-block: 0.4rem; + padding-inline: 0.5em; + padding-block: 0.4em; border-radius: 1em; border: 1px solid var(--tag-mid-color); color: ${p => @@ -136,6 +136,7 @@ export function EditableTag({ return ( import('../../chunks/EmojiInput/EmojiInput'), +); + +export function EmojiInput(props: EmojiInputProps) { + return ( + }> + + + ); +} + +const Fallback = styled.span` + display: inline-block; + width: 2rem; + height: 2rem; +`; diff --git a/browser/data-browser/src/components/forms/InputSlug.tsx b/browser/data-browser/src/components/forms/InputSlug.tsx index 0650a1854..69ba0bcbc 100644 --- a/browser/data-browser/src/components/forms/InputSlug.tsx +++ b/browser/data-browser/src/components/forms/InputSlug.tsx @@ -11,6 +11,7 @@ export default function InputSlug({ resource, property, commit, + commitDebounceInterval, ...props }: InputProps): JSX.Element { const [err, setErr, onBlur] = useValidation(); @@ -19,6 +20,7 @@ export default function InputSlug({ handleValidationError: setErr, validate: false, commit, + commitDebounce: commitDebounceInterval, }); const [inputValue, setInputValue] = useState(value); diff --git a/browser/data-browser/src/components/forms/InputString.tsx b/browser/data-browser/src/components/forms/InputString.tsx index 1a7ea1b73..e6a8870a2 100644 --- a/browser/data-browser/src/components/forms/InputString.tsx +++ b/browser/data-browser/src/components/forms/InputString.tsx @@ -9,11 +9,13 @@ export default function InputString({ resource, property, commit, + commitDebounceInterval, ...props }: InputProps): JSX.Element { const [err, setErr, onBlur] = useValidation(); const [value, setValue] = useString(resource, property.subject, { commit, + commitDebounce: commitDebounceInterval, validate: false, }); diff --git a/browser/data-browser/src/components/forms/ResourceField.tsx b/browser/data-browser/src/components/forms/ResourceField.tsx index 6ae7ed286..43eb8a586 100644 --- a/browser/data-browser/src/components/forms/ResourceField.tsx +++ b/browser/data-browser/src/components/forms/ResourceField.tsx @@ -141,7 +141,10 @@ export type InputProps = { disabled?: boolean; /** Whether the field should be focused on render */ autoFocus?: boolean; + /** Whether the field should commit on change */ commit?: boolean; + /** The debounce interval for the commit event in miliseconds */ + commitDebounceInterval?: number; }; interface IFieldProps { diff --git a/browser/data-browser/src/helpers/transitionName.ts b/browser/data-browser/src/helpers/transitionName.ts index 537b9361f..f095d98d2 100644 --- a/browser/data-browser/src/helpers/transitionName.ts +++ b/browser/data-browser/src/helpers/transitionName.ts @@ -1,3 +1,8 @@ +export const FILE_IMAGE_TRANSITION_TAG = 'file-image'; +export const SIDEBAR_TRANSITION_TAG = 'sidebar'; +export const PAGE_TITLE_TRANSITION_TAG = 'page-title'; +export const RESOURCE_PAGE_TRANSITION_TAG = 'resource-page'; + const hashStringWithCYRB53 = (str, seed = 0) => { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; diff --git a/browser/data-browser/src/views/Article/ArticleCard.tsx b/browser/data-browser/src/views/Article/ArticleCard.tsx index 5eb44c130..3a4fa2ed2 100644 --- a/browser/data-browser/src/views/Article/ArticleCard.tsx +++ b/browser/data-browser/src/views/Article/ArticleCard.tsx @@ -3,7 +3,10 @@ import { core, useString } from '@tomic/react'; import { styled } from 'styled-components'; import { AtomicLink } from '../../components/AtomicLink'; import { markdownToPlainText } from '../../helpers/markdown'; -import { transitionName } from '../../helpers/transitionName'; +import { + PAGE_TITLE_TRANSITION_TAG, + transitionName, +} from '../../helpers/transitionName'; import { ViewTransitionProps } from '../../helpers/ViewTransitionProps'; import { CardViewProps } from '../Card/CardViewProps'; @@ -37,5 +40,5 @@ const Title = styled.h2` width: 100%; overflow: hidden; font-size: 1.3rem; - ${props => transitionName('page-title', props.subject)} + ${props => transitionName(PAGE_TITLE_TRANSITION_TAG, props.subject)} `; diff --git a/browser/data-browser/src/views/Card/ResourceCardTitle.tsx b/browser/data-browser/src/views/Card/ResourceCardTitle.tsx index 25cf4fc0e..aed30155f 100644 --- a/browser/data-browser/src/views/Card/ResourceCardTitle.tsx +++ b/browser/data-browser/src/views/Card/ResourceCardTitle.tsx @@ -2,8 +2,11 @@ import { Resource, core, useArray } from '@tomic/react'; import { FC, PropsWithChildren } from 'react'; import { styled } from 'styled-components'; import { AtomicLink } from '../../components/AtomicLink'; -import { ViewTransitionProps } from '../../helpers/ViewTransitionProps'; -import { transitionName } from '../../helpers/transitionName'; +import { type ViewTransitionProps } from '../../helpers/ViewTransitionProps'; +import { + PAGE_TITLE_TRANSITION_TAG, + transitionName, +} from '../../helpers/transitionName'; import { getIconForClass } from '../FolderPage/iconMap'; import { Row } from '../../components/Row'; @@ -20,8 +23,8 @@ export const ResourceCardTitle: FC< return ( - - {resource.title} + + {resource.title} {children} @@ -31,7 +34,7 @@ export const ResourceCardTitle: FC< const Title = styled.h2` font-size: 1.4rem; margin: 0; - ${props => transitionName('page-title', props.subject)}; + ${props => transitionName(PAGE_TITLE_TRANSITION_TAG, props.subject)}; white-space: nowrap; text-overflow: ellipsis; `; diff --git a/browser/data-browser/src/views/File/useFileImageTransitionStyles.ts b/browser/data-browser/src/views/File/useFileImageTransitionStyles.ts index 4d4502700..51e7126d9 100644 --- a/browser/data-browser/src/views/File/useFileImageTransitionStyles.ts +++ b/browser/data-browser/src/views/File/useFileImageTransitionStyles.ts @@ -1,4 +1,7 @@ -import { getTransitionName } from '../../helpers/transitionName'; +import { + FILE_IMAGE_TRANSITION_TAG, + getTransitionName, +} from '../../helpers/transitionName'; import { useGlobalStylesWhileMounted } from '../../hooks/useGlobalStylesWhileMounted'; export function useFileImageTransitionStyles(subject: string) { @@ -6,7 +9,7 @@ export function useFileImageTransitionStyles(subject: string) { let name = 'none'; try { - name = getTransitionName('file-image', subject); + name = getTransitionName(FILE_IMAGE_TRANSITION_TAG, subject); css = ` ::view-transition-old(${name}), ::view-transition-new(${name}) { diff --git a/browser/data-browser/src/views/FolderPage/GridItem/components.tsx b/browser/data-browser/src/views/FolderPage/GridItem/components.tsx index 39867569a..a5115702e 100644 --- a/browser/data-browser/src/views/FolderPage/GridItem/components.tsx +++ b/browser/data-browser/src/views/FolderPage/GridItem/components.tsx @@ -1,9 +1,13 @@ import { styled } from 'styled-components'; -import { getTransitionStyle } from '../../../helpers/transitionName'; +import { + PAGE_TITLE_TRANSITION_TAG, + RESOURCE_PAGE_TRANSITION_TAG, + getTransitionStyle, +} from '../../../helpers/transitionName'; import { ViewTransitionProps } from '../../../helpers/ViewTransitionProps'; export const GridCard = styled.div.attrs(p => ({ - style: getTransitionStyle('resource-page', p.subject), + style: getTransitionStyle(RESOURCE_PAGE_TRANSITION_TAG, p.subject), }))` grid-area: card; background-color: ${p => p.theme.colors.bg1}; @@ -48,7 +52,7 @@ export const GridItemWrapper = styled.a` `; export const GridItemTitle = styled.div.attrs(p => ({ - style: getTransitionStyle('page-title', p.subject), + style: getTransitionStyle(PAGE_TITLE_TRANSITION_TAG, p.subject), }))` grid-area: title; font-size: 1rem; diff --git a/browser/data-browser/src/views/FolderPage/iconMap.ts b/browser/data-browser/src/views/FolderPage/iconMap.ts index 9812e5eac..4a31bb704 100644 --- a/browser/data-browser/src/views/FolderPage/iconMap.ts +++ b/browser/data-browser/src/views/FolderPage/iconMap.ts @@ -1,6 +1,7 @@ -import { classes } from '@tomic/react'; +import { collections, commits, core, dataBrowser, server } from '@tomic/react'; import { IconType } from 'react-icons'; import { + FaTag, FaAtom, FaBook, FaClock, @@ -8,33 +9,34 @@ import { FaCube, FaCubes, FaFile, - FaFileAlt, + FaFileLines, FaFileImport, FaFolder, FaHashtag, - FaHdd, - FaListAlt, + FaHardDrive, + FaList, FaShapes, - FaShareSquare, + FaShareFromSquare, FaTable, -} from 'react-icons/fa'; +} from 'react-icons/fa6'; const iconMap = new Map([ - [classes.folder, FaFolder], - [classes.bookmark, FaBook], - [classes.chatRoom, FaComment], - [classes.document, FaFileAlt], - [classes.file, FaFile], - [classes.drive, FaHdd], - [classes.commit, FaClock], - [classes.importer, FaFileImport], - [classes.invite, FaShareSquare], - [classes.collection, FaListAlt], - [classes.class, FaCube], - [classes.property, FaCubes], - [classes.table, FaTable], - [classes.property, FaHashtag], - [classes.ontology, FaShapes], + [dataBrowser.classes.folder, FaFolder], + [dataBrowser.classes.bookmark, FaBook], + [dataBrowser.classes.chatroom, FaComment], + [dataBrowser.classes.document, FaFileLines], + [server.classes.file, FaFile], + [server.classes.drive, FaHardDrive], + [commits.classes.commit, FaClock], + [dataBrowser.classes.importer, FaFileImport], + [server.classes.invite, FaShareFromSquare], + [collections.classes.collection, FaList], + [core.classes.class, FaCube], + [core.classes.property, FaCubes], + [dataBrowser.classes.table, FaTable], + [core.classes.property, FaHashtag], + [core.classes.ontology, FaShapes], + [dataBrowser.classes.tag, FaTag], ]); export function getIconForClass( diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx index 02e5f8cdf..4410c9eb6 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx @@ -9,7 +9,10 @@ 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 { + RESOURCE_PAGE_TRANSITION_TAG, + transitionName, +} from '../../../helpers/transitionName'; import { NewClassInstanceButton } from './NewClassInstanceButton'; interface ClassCardReadProps { @@ -56,7 +59,7 @@ export function ClassCardRead({ subject }: ClassCardReadProps): JSX.Element { const StyledCard = styled(Card)` padding-bottom: ${p => p.theme.margin}rem; - ${props => transitionName('resource-page', props.subject)}; + ${props => transitionName(RESOURCE_PAGE_TRANSITION_TAG, props.subject)}; `; const StyledH3 = styled.h3` diff --git a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx index 35544d44f..fe5ab2691 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx @@ -21,7 +21,7 @@ export function OntologySidebar({
@@ -36,7 +36,7 @@ export function OntologySidebar({
@@ -51,7 +51,7 @@ export function OntologySidebar({
diff --git a/browser/data-browser/src/views/ResourcePage.tsx b/browser/data-browser/src/views/ResourcePage.tsx index b11892b75..b81671301 100644 --- a/browser/data-browser/src/views/ResourcePage.tsx +++ b/browser/data-browser/src/views/ResourcePage.tsx @@ -6,6 +6,7 @@ import { Resource, urls, type OptionalClass, + dataBrowser, } from '@tomic/react'; import { ContainerNarrow } from '../components/Containers'; @@ -30,6 +31,7 @@ import { ArticlePage } from './Article'; import { TablePage } from './TablePage'; import { Main } from '../components/Main'; import { OntologyPage } from './OntologyPage'; +import { TagPage } from './TagPage/TagPage'; /** These properties are passed to every View at Page level */ export type ResourcePageProps = { @@ -122,6 +124,8 @@ function selectComponent(klass: string) { return TablePage; case urls.classes.ontology: return OntologyPage; + case dataBrowser.classes.tag: + return TagPage; default: return ResourcePageDefault; } diff --git a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx index cd34847ae..f01e4d581 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx @@ -150,6 +150,7 @@ function AtomicURLCellEdit({ return ( ))} ))} ) { + const [, setColor] = useString(resource, dataBrowser.properties.color, { + commit: true, + }); + const [emoji, setEmoji] = useString(resource, dataBrowser.properties.emoji, { + commit: true, + }); + const shortnameProp = useProperty(core.properties.shortname); + const [canWrite] = useCanWrite(resource); + + return ( + + + + + + {canWrite && ( + + +

Edit tag

+ + + + + + + +
+
+ )} + + +
+
+ ); +} + +const EmojiInputWrapper = styled.div` + border: 1px solid ${p => p.theme.colors.bg2}; + height: 2.2rem; + width: 2.2rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: ${p => p.theme.radius}; +`; + +const TagWrapper = styled.span` + font-size: 2rem; + width: fit-content; +`; diff --git a/browser/data-browser/src/views/TagPage/TagPropertyCard.tsx b/browser/data-browser/src/views/TagPage/TagPropertyCard.tsx new file mode 100644 index 000000000..6b950a33a --- /dev/null +++ b/browser/data-browser/src/views/TagPage/TagPropertyCard.tsx @@ -0,0 +1,65 @@ +import { + core, + useArray, + useCollection, + useMemberFromCollection, + useTitle, + type Collection, + type DataBrowser, + type Resource, +} from '@tomic/react'; +import { Card } from '../../components/Card'; +import { Column } from '../../components/Row'; +import { InlineFormattedResourceList } from '../../components/InlineFormattedResourceList'; + +interface TagPropertyCardProps { + resource: Resource; +} + +export function TagPropertyCard({ resource }: TagPropertyCardProps) { + const { collection } = useCollection( + { + property: core.properties.allowsOnly, + value: resource.subject, + }, + { pageSize: 100 }, + ); + + if (collection.totalMembers === 0) { + return Not used in any properties; + } + + return ( + + + {Array.from({ length: collection.totalMembers }).map((_, index) => ( + + ))} + + + ); +} + +interface PropertyRowProps { + index: number; + collection: Collection; +} + +function PropertyRow({ index, collection }: PropertyRowProps) { + const resource = useMemberFromCollection(collection, index); + const [allowsOnlyList] = useArray(resource, core.properties.allowsOnly); + const [shortname] = useTitle(resource); + + if (resource.loading) { + return <>; + } + + return ( + +

{shortname}

+
+ +
+
+ ); +} diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index 8d3e9a62f..e49b0ef31 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -1,3 +1,4 @@ +import { EventManager } from './EventManager.js'; import type { Agent } from './agent.js'; import { Client } from './client.js'; import type { Collection } from './collection.js'; @@ -32,6 +33,14 @@ export type PropVals = Map; */ export const unknownSubject = 'unknown-subject'; +export enum ResourceEvents { + LocalChange = 'local-change', +} + +type ResourceEventHandlers = { + [ResourceEvents.LocalChange]: (prop: string, value: JSONValue) => void; +}; + /** * Describes an Atomic Resource, which has a Subject URL and a bunch of Property * / Value combinations. @@ -68,6 +77,10 @@ export class Resource { private hasQueue = false; private _store?: Store; + private eventManager = new EventManager< + ResourceEvents, + ResourceEventHandlers + >(); public constructor(subject: string, newResource?: boolean) { if (typeof subject !== 'string') { @@ -122,6 +135,13 @@ export class Resource { return this._store; } + public on( + event: T, + callback: ResourceEventHandlers[T], + ) { + return this.eventManager.register(event, callback); + } + /** @internal */ public setStore(store: Store): void { this._store = store; @@ -705,6 +725,7 @@ export class Resource { if (value === undefined) { this.remove(prop); + this.eventManager.emit(ResourceEvents.LocalChange, prop, value); return; } @@ -712,6 +733,7 @@ export class Resource { this.propvals.set(prop, value); // Add the change to the Commit Builder, so we can commit our changes later this.commitBuilder.addSetAction(prop, value); + this.eventManager.emit(ResourceEvents.LocalChange, prop, value); } /** diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index b68d6b089..52a61013a 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -125,13 +125,13 @@ importers: version: 3.2.2(react@18.2.0) '@emoji-mart/react': specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) + version: 1.1.1(emoji-mart@5.6.0)(react@18.2.0) '@emotion/is-prop-valid': specifier: ^1.2.1 version: 1.2.1 '@radix-ui/react-popover': - specifier: ^1.0.6 - version: 1.0.6(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-scroll-area': specifier: ^1.0.1 version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -166,8 +166,8 @@ importers: specifier: workspace:* version: link:../react emoji-mart: - specifier: ^5.5.2 - version: 5.5.2 + specifier: ^5.6.0 + version: 5.6.0 polished: specifier: ^4.1.0 version: 4.2.2 @@ -2724,13 +2724,16 @@ packages: '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} - '@radix-ui/react-arrow@1.0.3': - resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -2759,6 +2762,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.0.1': resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -2768,6 +2780,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-direction@1.0.1': resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -2777,35 +2798,35 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.0.4': - resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} + '@radix-ui/react-dismissable-layer@1.1.0': + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.0.1': - resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + '@radix-ui/react-focus-guards@1.1.0': + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.0.3': - resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -2821,39 +2842,48 @@ packages: '@types/react': optional: true - '@radix-ui/react-popover@1.0.6': - resolution: {integrity: sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==} + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.1': + resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@radix-ui/react-popper@1.1.2': - resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@radix-ui/react-portal@1.0.3': - resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} + '@radix-ui/react-portal@1.1.1': + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -2873,6 +2903,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.0': + resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@1.0.3': resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -2886,6 +2929,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.0.4': resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -2921,6 +2977,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.0.4': resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: @@ -2943,6 +3008,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.0.1': resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: @@ -2952,11 +3026,20 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.0.3': - resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true @@ -2970,26 +3053,35 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.0.1': - resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - '@radix-ui/react-use-size@1.0.1': - resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - '@radix-ui/rect@1.0.1': - resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} '@reactflow/background@11.2.8': resolution: {integrity: sha512-5o41N2LygiNC2/Pk8Ak2rIJjXbKHfQ23/Y9LFsnAlufqwdzFqKA8txExpsMoPVHHlbAdA/xpQaMuoChGPqmyDw==} @@ -5329,8 +5421,8 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-mart@5.5.2: - resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + emoji-mart@5.6.0: + resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -8521,8 +8613,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.5.5: - resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + react-remove-scroll@2.5.7: + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11965,9 +12057,9 @@ snapshots: react: 18.2.0 tslib: 2.6.1 - '@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0)': + '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.2.0)': dependencies: - emoji-mart: 5.5.2 + emoji-mart: 5.6.0 react: 18.2.0 '@emotion/is-prop-valid@1.2.1': @@ -13265,10 +13357,11 @@ snapshots: dependencies: '@babel/runtime': 7.22.6 - '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: @@ -13295,6 +13388,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.1)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + '@radix-ui/react-context@1.0.1(@types/react@18.3.1)(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13302,6 +13401,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-context@1.1.0(@types/react@18.3.1)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + '@radix-ui/react-direction@1.0.1(@types/react@18.3.1)(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13309,33 +13414,30 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.1)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: '@types/react': 18.3.1 '@types/react-dom': 18.2.14 - '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.1)(react@18.2.0)': + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 react: 18.2.0 optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: @@ -13350,53 +13452,58 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-popover@1.0.6(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/react-id@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + + '@radix-ui/react-popover@1.1.1(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.1)(react@18.2.0) 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.3.1)(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.2.0) optionalDependencies: '@types/react': 18.3.1 '@types/react-dom': 18.2.14 - '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 '@floating-ui/react-dom': 2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.1)(react@18.2.0) - '@radix-ui/rect': 1.0.1 + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/rect': 1.1.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: '@types/react': 18.3.1 '@types/react-dom': 18.2.14 - '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) optionalDependencies: @@ -13414,6 +13521,16 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.2.14 + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.2.14 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13424,6 +13541,15 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.2.14 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.2.14 + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13468,6 +13594,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.1)(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + '@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.14)(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13492,6 +13625,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.1)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.1)(react@18.2.0)': dependencies: '@babel/runtime': 7.22.6 @@ -13500,10 +13639,16 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.1)(react@18.2.0)': + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.1)(react@18.2.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.2.0) react: 18.2.0 optionalDependencies: '@types/react': 18.3.1 @@ -13515,25 +13660,27 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.1)(react@18.2.0)': + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/rect': 1.0.1 react: 18.2.0 optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/react-use-size@1.0.1(@types/react@18.3.1)(react@18.2.0)': + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.2.0) + '@radix-ui/rect': 1.1.0 react: 18.2.0 optionalDependencies: '@types/react': 18.3.1 - '@radix-ui/rect@1.0.1': + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.1)(react@18.2.0)': dependencies: - '@babel/runtime': 7.22.6 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.3.1 + + '@radix-ui/rect@1.1.0': {} '@reactflow/background@11.2.8(@types/react@18.3.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -14907,7 +15054,7 @@ snapshots: aria-hidden@1.2.3: dependencies: - tslib: 2.6.1 + tslib: 2.6.2 aria-query@5.3.0: dependencies: @@ -16091,7 +16238,7 @@ snapshots: emittery@0.13.1: {} - emoji-mart@5.5.2: {} + emoji-mart@5.6.0: {} emoji-regex@8.0.0: {} @@ -20153,16 +20300,16 @@ snapshots: dependencies: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.2.0) - tslib: 2.6.1 + tslib: 2.6.2 optionalDependencies: '@types/react': 18.3.1 - react-remove-scroll@2.5.5(@types/react@18.3.1)(react@18.2.0): + react-remove-scroll@2.5.7(@types/react@18.3.1)(react@18.2.0): dependencies: react: 18.2.0 react-remove-scroll-bar: 2.3.4(@types/react@18.3.1)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.2.0) - tslib: 2.6.1 + tslib: 2.6.2 use-callback-ref: 1.3.0(@types/react@18.3.1)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.2.0) optionalDependencies: @@ -20185,7 +20332,7 @@ snapshots: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 - tslib: 2.6.1 + tslib: 2.6.2 optionalDependencies: '@types/react': 18.3.1 @@ -21591,7 +21738,7 @@ snapshots: use-callback-ref@1.3.0(@types/react@18.3.1)(react@18.2.0): dependencies: react: 18.2.0 - tslib: 2.6.1 + tslib: 2.6.2 optionalDependencies: '@types/react': 18.3.1 @@ -21599,7 +21746,7 @@ snapshots: dependencies: detect-node-es: 1.1.0 react: 18.2.0 - tslib: 2.6.1 + tslib: 2.6.2 optionalDependencies: '@types/react': 18.3.1 diff --git a/browser/react/src/hooks.ts b/browser/react/src/hooks.ts index a3d31ac3c..b3995e606 100644 --- a/browser/react/src/hooks.ts +++ b/browser/react/src/hooks.ts @@ -27,6 +27,7 @@ import { OptionalClass, proxyResource, type Core, + ResourceEvents, } from '@tomic/lib'; import { useDebouncedCallback } from './index.js'; @@ -275,6 +276,14 @@ export function useValue( setPrevResourceReference(resource); } + useEffect(() => { + return resource.on(ResourceEvents.LocalChange, (prop, value) => { + if (prop === propertyURL) { + set(value); + } + }); + }, [resource, propertyURL]); + return [val, validateAndSet]; } From 755286cb14427cfb82946f6fd08278d3903ab215 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Tue, 9 Jul 2024 14:06:09 +0200 Subject: [PATCH 10/13] Various UI improvements --- browser/CHANGELOG.md | 3 + browser/data-browser/src/components/Card.tsx | 39 ++- browser/data-browser/src/components/Main.tsx | 27 +- .../src/components/Navigation.tsx | 13 +- .../data-browser/src/components/Parent.tsx | 37 +-- .../MenuBarDropdownTrigger.tsx | 10 +- .../src/components/forms/BasicSelect.tsx | 3 +- .../src/components/forms/InputString.tsx | 2 +- .../data-browser/src/helpers/containers.ts | 1 + .../src/helpers/transitionName.ts | 6 +- browser/data-browser/src/routes/DataRoute.tsx | 8 +- browser/data-browser/src/routes/EditRoute.tsx | 62 ++-- .../src/routes/History/HistoryRoute.tsx | 27 +- browser/data-browser/src/routes/Routes.tsx | 2 +- .../src/routes/Share/AgentRights.tsx | 86 +++++ .../src/routes/Share/PermissionRow.tsx | 26 ++ .../src/routes/Share/ShareRoute.tsx | 131 ++++++++ .../src/routes/Share/useInheritedRights.ts | 45 +++ .../src/routes/Share/useRights.ts | 86 +++++ .../data-browser/src/routes/ShareRoute.tsx | 299 ------------------ browser/data-browser/src/styling.tsx | 71 ++++- .../OntologyPage/Class/ClassCardRead.tsx | 7 +- .../src/views/OntologyPage/OntologyPage.tsx | 47 +-- .../views/OntologyPage/OntologySidebar.tsx | 2 +- .../Property/PropertyLineRead.tsx | 52 ++- .../Property/PropertyLineWrite.tsx | 32 +- .../OntologyPage/Property/filterAllowsOnly.ts | 2 +- .../OntologyPage/PropertyDatatypePicker.tsx | 7 +- .../data-browser/src/views/ResourcePage.tsx | 53 ++-- browser/e2e/tests/search.spec.ts | 3 +- browser/lib/src/resource.ts | 2 +- 31 files changed, 681 insertions(+), 510 deletions(-) create mode 100644 browser/data-browser/src/routes/Share/AgentRights.tsx create mode 100644 browser/data-browser/src/routes/Share/PermissionRow.tsx create mode 100644 browser/data-browser/src/routes/Share/ShareRoute.tsx create mode 100644 browser/data-browser/src/routes/Share/useInheritedRights.ts create mode 100644 browser/data-browser/src/routes/Share/useRights.ts delete mode 100644 browser/data-browser/src/routes/ShareRoute.tsx diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index d65c58353..49b5d4e9d 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -16,6 +16,9 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Fix an issue where the resource-array properties would be set to an empty array instead of removing the property when removing all items in the input. - Fix an issue where dropdown menus sometimes jump from the upper left corner of the screen. - Added a full page view for tags. +- Redesigned the ontology page. +- Moved the resource context menu to the top of the page. +- [#861](https://github.com/atomicdata-dev/atomic-server/issues/861) Fix long usernames overflowing on the share page. ### @tomic/lib diff --git a/browser/data-browser/src/components/Card.tsx b/browser/data-browser/src/components/Card.tsx index af8796397..00c678ef3 100644 --- a/browser/data-browser/src/components/Card.tsx +++ b/browser/data-browser/src/components/Card.tsx @@ -3,6 +3,7 @@ import { RESOURCE_PAGE_TRANSITION_TAG, getTransitionStyle, } from '../helpers/transitionName'; +import { CARD_CONTAINER } from '../helpers/containers'; type CardProps = { /** Adds a colorful border */ @@ -16,20 +17,19 @@ export const Card = styled.div.attrs(p => ({ // When we render a lot of cards it is more performant to use styles instead of classes when each card has a unique style style: getTransitionStyle(RESOURCE_PAGE_TRANSITION_TAG, p.about), }))` - background-color: ${props => props.theme.colors.bg}; - + background-color: ${p => p.theme.colors.bg}; + container: ${CARD_CONTAINER} / inline-size; border: solid 1px - ${props => - props.highlight ? props.theme.colors.main : props.theme.colors.bg2}; - box-shadow: ${props => - props.highlight - ? `0 0 0 1px ${props.theme.colors.main}, ${props.theme.boxShadow}` - : props.theme.boxShadow}; - - padding: ${props => props.theme.margin}rem; - border-radius: ${props => props.theme.radius}; - max-height: ${props => (props.small ? '10rem' : 'none')}; - overflow: ${props => (props.small ? 'hidden' : 'visible')}; + ${p => (p.highlight ? p.theme.colors.main : p.theme.colors.bg2)}; + box-shadow: ${p => + p.highlight + ? `0 0 0 1px ${p.theme.colors.main}, ${p.theme.boxShadow}` + : p.theme.boxShadow}; + + padding: ${p => p.theme.size()}; + border-radius: ${p => p.theme.radius}; + max-height: ${p => (p.small ? p.theme.size(12) : 'initial')}; + overflow: ${p => (p.small ? 'hidden' : 'visible')}; `; export interface CardRowProps { @@ -38,20 +38,19 @@ export interface CardRowProps { /** A Row in a Card. Should probably be used inside a CardInsideFull */ export const CardRow = styled.div` - --border: solid 1px ${props => props.theme.colors.bg2}; + --border: solid 1px ${p => p.theme.colors.bg2}; display: block; - border-top: ${props => (props.noBorder ? 'none' : 'var(--border)')}; - padding: ${props => props.theme.margin / 3}rem - ${props => props.theme.margin}rem; + border-top: ${p => (p.noBorder ? 'none' : 'var(--border)')}; + padding: ${p => p.theme.size(2)} ${p => p.theme.size()}; `; /** A block inside a Card which has full width */ export const CardInsideFull = styled.div` - margin-left: -${props => props.theme.margin}rem; - margin-right: -${props => props.theme.margin}rem; + margin-left: -${p => p.theme.size()}; + margin-right: -${p => p.theme.size()}; `; export const Margin = styled.div` display: block; - height: ${props => props.theme.margin}rem; + height: ${p => p.theme.size()}; `; diff --git a/browser/data-browser/src/components/Main.tsx b/browser/data-browser/src/components/Main.tsx index dbf23923c..0cfc31a36 100644 --- a/browser/data-browser/src/components/Main.tsx +++ b/browser/data-browser/src/components/Main.tsx @@ -7,6 +7,8 @@ import { } from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; import { MAIN_CONTAINER } from '../helpers/containers'; +import Parent from './Parent'; +import { useResource } from '@tomic/react'; /** Main landmark. Every page should have one of these. * If the pages shows a resource a subject can be passed that enables view transitions to work. */ @@ -14,19 +16,26 @@ export function Main({ subject, children, }: PropsWithChildren): JSX.Element { + const resource = useResource(subject); + return ( - - - - Start of main content - - - {children} - + <> + {subject && } + + + + Start of main content + + + {children} + + ); } const StyledMain = memo(styled.main` container: ${MAIN_CONTAINER} / inline-size; - ${p => transitionName(RESOURCE_PAGE_TRANSITION_TAG, p.subject)} + ${p => transitionName(RESOURCE_PAGE_TRANSITION_TAG, p.subject)}; + height: calc(100vh - ${p => p.theme.heights.breadCrumbBar}); + overflow-y: auto; `); diff --git a/browser/data-browser/src/components/Navigation.tsx b/browser/data-browser/src/components/Navigation.tsx index 22bf32012..d08cd256d 100644 --- a/browser/data-browser/src/components/Navigation.tsx +++ b/browser/data-browser/src/components/Navigation.tsx @@ -8,14 +8,13 @@ import { ButtonBar } from './Button'; import { useCurrentSubject } from '../helpers/useCurrentSubject'; import { useSettings } from '../helpers/AppSettings'; import { SideBar } from './SideBar'; -import ResourceContextMenu from './ResourceContextMenu'; import { isRunningInTauri } from '../helpers/tauri'; import { shortcuts } from './HotKeyWrapper'; -import { MenuBarDropdownTrigger } from './ResourceContextMenu/MenuBarDropdownTrigger'; import { NavBarSpacer } from './NavBarSpacer'; import { Searchbar } from './Searchbar'; import { useMediaQuery } from '../hooks/useMediaQuery'; import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; +import { NAVBAR_TRANSITION_TAG } from '../helpers/transitionName'; interface NavWrapperProps { children: React.ReactNode; @@ -134,14 +133,6 @@ function NavBar(): JSX.Element { onFocus={maybeHideButtons} onBlur={() => setShowButtons(true)} /> - - {showButtons && subject && ( - - )} ); } @@ -160,7 +151,7 @@ const NavBarBase = styled.div` display: flex; border: solid 1px ${props => props.theme.colors.bg2}; background-color: ${props => props.theme.colors.bg}; - view-transition-name: navbar; + view-transition-name: ${NAVBAR_TRANSITION_TAG}; `; /** Width of the floating navbar in rem */ diff --git a/browser/data-browser/src/components/Parent.tsx b/browser/data-browser/src/components/Parent.tsx index 9daa73593..a025e469d 100644 --- a/browser/data-browser/src/components/Parent.tsx +++ b/browser/data-browser/src/components/Parent.tsx @@ -8,24 +8,21 @@ import { server, } from '@tomic/react'; import { constructOpenURL } from '../helpers/navigation'; -import { FaSearch } from 'react-icons/fa'; import { Row } from './Row'; -import { useQueryScopeHandler } from '../hooks/useQueryScope'; -import { IconButton } from './IconButton/IconButton'; import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; import { useSettings } from '../helpers/AppSettings'; import { Button } from './Button'; +import { BREADCRUMB_BAR_TRANSITION_TAG } from '../helpers/transitionName'; +import ResourceContextMenu from './ResourceContextMenu'; +import { MenuBarDropdownTrigger } from './ResourceContextMenu/MenuBarDropdownTrigger'; type ParentProps = { resource: Resource; }; -export const PARENT_PADDING_BLOCK = '0.2rem'; - /** Breadcrumb list. Recursively renders parents. */ function Parent({ resource }: ParentProps): JSX.Element { const [parent] = useString(resource, core.properties.parent); - const { enableScope } = useQueryScopeHandler(resource.getSubject()); return ( @@ -33,17 +30,17 @@ function Parent({ resource }: ParentProps): JSX.Element { {parent ? ( ) : ( - + )} {resource.title} - - - + + + ); @@ -51,9 +48,7 @@ function Parent({ resource }: ParentProps): JSX.Element { const ParentWrapper = styled.nav` height: ${p => p.theme.heights.breadCrumbBar}; - padding-block: ${PARENT_PADDING_BLOCK}; - padding-inline: 0.5rem; - color: ${props => props.theme.colors.textLight2}; + padding-inline: ${p => p.theme.size(2)}; border-bottom: 1px solid ${props => props.theme.colors.bg2}; background-color: ${props => props.theme.colors.bg}; display: flex; @@ -61,7 +56,7 @@ const ParentWrapper = styled.nav` align-items: center; justify-content: flex-start; - view-transition-name: breadcrumb-bar; + view-transition-name: ${BREADCRUMB_BAR_TRANSITION_TAG}; `; type NestedParentProps = { @@ -111,10 +106,10 @@ function NestedParent({ subject, depth }: NestedParentProps): JSX.Element { return Set as drive; } - function handleClick(e) { + const handleClick: React.MouseEventHandler = e => { e.preventDefault(); navigate(constructOpenURL(subject)); - } + }; return ( <> @@ -170,7 +165,7 @@ const Spacer = styled.span` flex: 1; `; -const ScopedSearchButton = styled(IconButton)` +const ButtonArea = styled.div` justify-self: flex-end; `; diff --git a/browser/data-browser/src/components/ResourceContextMenu/MenuBarDropdownTrigger.tsx b/browser/data-browser/src/components/ResourceContextMenu/MenuBarDropdownTrigger.tsx index 5a86ca7f5..4498699da 100644 --- a/browser/data-browser/src/components/ResourceContextMenu/MenuBarDropdownTrigger.tsx +++ b/browser/data-browser/src/components/ResourceContextMenu/MenuBarDropdownTrigger.tsx @@ -1,24 +1,22 @@ -import { ButtonBar } from '../Button'; import { FaEllipsisV } from 'react-icons/fa'; import { DropdownTriggerRenderFunction } from '../Dropdown/DropdownTrigger'; import { shortcuts } from '../HotKeyWrapper'; +import { IconButton } from '../IconButton/IconButton'; export const MenuBarDropdownTrigger: DropdownTriggerRenderFunction = ( - { onClick, isActive, menuId }, + { onClick, menuId }, ref, ) => ( - - + ); MenuBarDropdownTrigger.displayName = 'MenuBarDropdownTrigger'; diff --git a/browser/data-browser/src/components/forms/BasicSelect.tsx b/browser/data-browser/src/components/forms/BasicSelect.tsx index b725e1385..2619c5f10 100644 --- a/browser/data-browser/src/components/forms/BasicSelect.tsx +++ b/browser/data-browser/src/components/forms/BasicSelect.tsx @@ -6,10 +6,11 @@ type Props = React.SelectHTMLAttributes; export const BasicSelect: FC> = ({ children, + className, ...props }) => { return ( - + diff --git a/browser/data-browser/src/components/forms/InputString.tsx b/browser/data-browser/src/components/forms/InputString.tsx index e6a8870a2..6694d5585 100644 --- a/browser/data-browser/src/components/forms/InputString.tsx +++ b/browser/data-browser/src/components/forms/InputString.tsx @@ -20,7 +20,7 @@ export default function InputString({ }); function handleUpdate(event: React.ChangeEvent): void { - const newval = event.target.value; + const newval = event.target.value || undefined; setValue(newval); try { diff --git a/browser/data-browser/src/helpers/containers.ts b/browser/data-browser/src/helpers/containers.ts index 0aaccb8ed..fb047a7d9 100644 --- a/browser/data-browser/src/helpers/containers.ts +++ b/browser/data-browser/src/helpers/containers.ts @@ -1,2 +1,3 @@ export const MAIN_CONTAINER = 'main'; export const ALL_PROPS_CONTAINER = 'all-props'; +export const CARD_CONTAINER = 'card'; diff --git a/browser/data-browser/src/helpers/transitionName.ts b/browser/data-browser/src/helpers/transitionName.ts index f095d98d2..f48a6d019 100644 --- a/browser/data-browser/src/helpers/transitionName.ts +++ b/browser/data-browser/src/helpers/transitionName.ts @@ -2,12 +2,14 @@ export const FILE_IMAGE_TRANSITION_TAG = 'file-image'; export const SIDEBAR_TRANSITION_TAG = 'sidebar'; export const PAGE_TITLE_TRANSITION_TAG = 'page-title'; export const RESOURCE_PAGE_TRANSITION_TAG = 'resource-page'; +export const BREADCRUMB_BAR_TRANSITION_TAG = 'breadcrumb-bar'; +export const NAVBAR_TRANSITION_TAG = 'navbar'; -const hashStringWithCYRB53 = (str, seed = 0) => { +const hashStringWithCYRB53 = (str: string, seed = 0) => { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; - for (let i = 0, ch; i < str.length; i++) { + for (let i = 0, ch: number; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); diff --git a/browser/data-browser/src/routes/DataRoute.tsx b/browser/data-browser/src/routes/DataRoute.tsx index 65104187a..1aa41f022 100644 --- a/browser/data-browser/src/routes/DataRoute.tsx +++ b/browser/data-browser/src/routes/DataRoute.tsx @@ -75,8 +75,8 @@ function Data(): JSX.Element { }; return ( - -
+
+ Usage -
- + +
); } diff --git a/browser/data-browser/src/routes/EditRoute.tsx b/browser/data-browser/src/routes/EditRoute.tsx index 4302f1209..8faaf3e05 100644 --- a/browser/data-browser/src/routes/EditRoute.tsx +++ b/browser/data-browser/src/routes/EditRoute.tsx @@ -8,7 +8,6 @@ import { ResourceForm } from '../components/forms/ResourceForm'; import { useCurrentSubject } from '../helpers/useCurrentSubject'; import { ClassDetail } from '../components/ClassDetail'; import { Title } from '../components/Title'; -import Parent from '../components/Parent'; import { Main } from '../components/Main'; import { Column, Row } from '../components/Row'; import { IconButton } from '../components/IconButton/IconButton'; @@ -38,39 +37,36 @@ export function Edit(): JSX.Element { }; return ( - <> - +
-
- {subject ? ( - - - - - - - </Row> - <ClassDetail resource={resource} /> - {/* Key is required for re-rendering when subject changes */} - <ResourceForm resource={resource} key={subject} /> - </Column> - ) : ( - <form onSubmit={handleClassSet}> - <h1>edit a resource</h1> - <InputStyled - value={subjectInput || undefined} - onChange={e => setSubjectInput(e.target.value)} - placeholder={'Enter a Resource URL...'} - /> - </form> - )} - </Main> + {subject ? ( + <Column> + <Row center gap='1ch'> + <IconButton + title={`Back to ${resource.title}`} + size='1.4em' + edgeAlign='start' + onClick={handleBackClick} + > + <FaArrowLeft /> + </IconButton> + <Title resource={resource} prefix='Edit' /> + </Row> + <ClassDetail resource={resource} /> + {/* Key is required for re-rendering when subject changes */} + <ResourceForm resource={resource} key={subject} /> + </Column> + ) : ( + <form onSubmit={handleClassSet}> + <h1>edit a resource</h1> + <InputStyled + value={subjectInput || undefined} + onChange={e => setSubjectInput(e.target.value)} + placeholder={'Enter a Resource URL...'} + /> + </form> + )} </ContainerNarrow> - </> + </Main> ); } diff --git a/browser/data-browser/src/routes/History/HistoryRoute.tsx b/browser/data-browser/src/routes/History/HistoryRoute.tsx index e6ce50758..577fe4632 100644 --- a/browser/data-browser/src/routes/History/HistoryRoute.tsx +++ b/browser/data-browser/src/routes/History/HistoryRoute.tsx @@ -15,6 +15,7 @@ import { HistoryMobileView } from './HistoryMobileView'; import { useMediaQuery } from '../../hooks/useMediaQuery'; import { Column, Row } from '../../components/Row'; import { ProgressBar } from '../../components/ProgressBar'; +import { Main } from '../../components/Main'; /** Shows an activity log of previous versions */ export function History(): JSX.Element { @@ -93,18 +94,20 @@ export function History(): JSX.Element { } return ( - <SplitView about={subject}> - <ViewComp - resource={resource} - groupedVersions={groupedVersions} - selectedVersion={selectedVersion} - isCurrentVersion={isCurrentVersion} - onNextVersion={nextVersion} - onPreviousVersion={previousVersion} - onSelectVersion={setSelectedVersion} - onVersionAccept={setResourceToCurrentVersion} - /> - </SplitView> + <Main subject={subject}> + <SplitView about={subject}> + <ViewComp + resource={resource} + groupedVersions={groupedVersions} + selectedVersion={selectedVersion} + isCurrentVersion={isCurrentVersion} + onNextVersion={nextVersion} + onPreviousVersion={previousVersion} + onSelectVersion={setSelectedVersion} + onVersionAccept={setResourceToCurrentVersion} + /> + </SplitView> + </Main> ); } diff --git a/browser/data-browser/src/routes/Routes.tsx b/browser/data-browser/src/routes/Routes.tsx index 69959ed2c..3c4cc7d40 100644 --- a/browser/data-browser/src/routes/Routes.tsx +++ b/browser/data-browser/src/routes/Routes.tsx @@ -15,7 +15,7 @@ import SettingsAgent from './SettingsAgent'; import { SettingsServer } from './SettingsServer'; import { paths } from './paths'; import ResourcePage from '../views/ResourcePage'; -import { ShareRoute } from './ShareRoute'; +import { ShareRoute } from './Share/ShareRoute'; import { Sandbox } from './Sandbox'; import { TokenRoute } from './TokenRoute'; import { ImporterPage } from '../views/ImporterPage'; diff --git a/browser/data-browser/src/routes/Share/AgentRights.tsx b/browser/data-browser/src/routes/Share/AgentRights.tsx new file mode 100644 index 000000000..c4ebe41a2 --- /dev/null +++ b/browser/data-browser/src/routes/Share/AgentRights.tsx @@ -0,0 +1,86 @@ +import { urls, useResource } from '@tomic/react'; +import { FaGlobe } from 'react-icons/fa6'; +import styled from 'styled-components'; +import { CardRow } from '../../components/Card'; +import { ResourceInline } from '../../views/ResourceInline'; +import type { MergedRight } from './useRights'; +import { PermissionRow } from './PermissionRow'; + +interface AgentRightsProps extends MergedRight { + hideInherit?: boolean; + handleSetRight?: (agent: string, write: boolean, setToTrue: boolean) => void; +} + +export function AgentRights({ + handleSetRight, + hideInherit, + agentSubject, + setIn, + read, + write, +}: AgentRightsProps): JSX.Element { + const isPublicRight = agentSubject === urls.instances.publicAgent; + const resource = useResource(agentSubject); + const disabled = !resource.isReady() || !handleSetRight; + + return ( + <CardRow> + <PermissionRow data-test={isPublicRight ? 'right-public' : null}> + <PermissionRow.TitleColumn> + {isPublicRight ? ( + <> + <FaGlobe /> Public (anyone){' '} + </> + ) : ( + <TruncatedResourceTitle subject={agentSubject} /> + )} + {!hideInherit && setIn && ( + <> + {' (via '} + <ResourceInline subject={setIn} /> + {') '} + </> + )} + </PermissionRow.TitleColumn> + <PermissionRow.ControlsColumn> + <StyledCheckbox + type='checkbox' + disabled={disabled} + onChange={e => + handleSetRight?.(agentSubject, false, e.target.checked) + } + checked={read} + title={ + read + ? 'Read access. Toggle to remove access.' + : 'No read access. Toggle to give read access.' + } + /> + <StyledCheckbox + type='checkbox' + disabled={disabled} + onChange={e => + handleSetRight?.(agentSubject, true, e.target.checked) + } + checked={write} + title={ + write + ? 'Write access. Toggle to remove access.' + : 'No write access. Toggle to give write access.' + } + /> + </PermissionRow.ControlsColumn> + </PermissionRow> + </CardRow> + ); +} + +const StyledCheckbox = styled.input` + width: 1rem; + height: 1rem; +`; + +const TruncatedResourceTitle = styled(ResourceInline)` + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/browser/data-browser/src/routes/Share/PermissionRow.tsx b/browser/data-browser/src/routes/Share/PermissionRow.tsx new file mode 100644 index 000000000..1dfc73a8e --- /dev/null +++ b/browser/data-browser/src/routes/Share/PermissionRow.tsx @@ -0,0 +1,26 @@ +import { styled } from 'styled-components'; +import { Row } from '../../components/Row'; + +export function PermissionRow({ + children, + ...props +}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) { + return ( + <Row {...props} center> + {children} + </Row> + ); +} + +PermissionRow.TitleColumn = styled.div` + overflow: hidden; + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; +`; + +PermissionRow.ControlsColumn = styled.div` + flex-basis: 6rem; + display: flex; + justify-content: space-around; +`; diff --git a/browser/data-browser/src/routes/Share/ShareRoute.tsx b/browser/data-browser/src/routes/Share/ShareRoute.tsx new file mode 100644 index 000000000..92a1f90b2 --- /dev/null +++ b/browser/data-browser/src/routes/Share/ShareRoute.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { useCanWrite, useResource } from '@tomic/react'; +import { ContainerNarrow } from '../../components/Containers'; +import { useCurrentSubject } from '../../helpers/useCurrentSubject'; +import { Card, CardInsideFull } from '../../components/Card'; +import { Button } from '../../components/Button'; +import { InviteForm } from '../../components/InviteForm'; +import toast from 'react-hot-toast'; +import { Title } from '../../components/Title'; +import { constructOpenURL } from '../../helpers/navigation'; +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'; +import { useRights } from './useRights'; +import { AgentRights } from './AgentRights'; +import { useInheritedRights } from './useInheritedRights'; +import { PermissionRow } from './PermissionRow'; +import styled from 'styled-components'; + +/** Form for managing and viewing rights for this resource */ +export function ShareRoute(): JSX.Element { + const [subject] = useCurrentSubject(); + const resource = useResource(subject); + const [canWrite] = useCanWrite(resource); + const [showInviteForm, setShowInviteForm] = useState(false); + const [err, setErr] = useState<Error | undefined>(undefined); + const navigate = useNavigate(); + const inheritedRights = useInheritedRights(resource); + + const [resourceRights, updateResourceRights] = useRights(resource, setErr); + + if (!subject) { + return <>No subject passed</>; + } + + async function handleSave() { + try { + await resource.save(); + toast.success('Share settings saved'); + navigate(constructOpenURL(subject!)); + } catch (e) { + toast.error(e.message); + } + } + + return ( + <Main subject={subject}> + <ContainerNarrow> + <Column> + <Title resource={resource} prefix='Share settings' link /> + {canWrite && !showInviteForm && ( + <span> + <Button onClick={() => setShowInviteForm(true)}> + <FaShare /> + Create Invite + </Button> + </span> + )} + {showInviteForm && <InviteForm target={resource} />} + <Card> + <Column> + <RightsHeader>Rights set here:</RightsHeader> + <CardInsideFull> + {/* This key might be a bit too much, but the component wasn't properly re-rendering before */} + {resourceRights.map(right => ( + <AgentRights + hideInherit + key={JSON.stringify(right)} + {...right} + handleSetRight={ + canWrite && resource.isReady() + ? updateResourceRights + : undefined + } + /> + ))} + </CardInsideFull> + </Column> + </Card> + {canWrite && ( + <span> + <Button + disabled={!resource.hasUnsavedChanges()} + onClick={handleSave} + > + Save + </Button> + </span> + )} + {err && <ErrorLook>{err.message}</ErrorLook>} + {inheritedRights.length > 0 && ( + <Card> + <Column> + <RightsHeader>Inherited rights:</RightsHeader> + <CardInsideFull> + {inheritedRights.map(right => ( + <AgentRights + setIn={right.setIn} + key={right.agentSubject + right.setIn} + read={right.read} + write={right.write} + agentSubject={right.agentSubject} + /> + ))} + </CardInsideFull> + </Column> + </Card> + )} + </Column> + </ContainerNarrow> + </Main> + ); +} + +function RightsHeader({ children }: React.PropsWithChildren): JSX.Element { + return ( + <PermissionRow> + <PermissionRowTitleHeader>{children}</PermissionRowTitleHeader> + <PermissionRow.ControlsColumn> + <span>Read</span> + <span>Write</span> + </PermissionRow.ControlsColumn> + </PermissionRow> + ); +} + +const PermissionRowTitleHeader = styled(PermissionRow.TitleColumn)` + font-weight: bold; +`; diff --git a/browser/data-browser/src/routes/Share/useInheritedRights.ts b/browser/data-browser/src/routes/Share/useInheritedRights.ts new file mode 100644 index 000000000..53259abf5 --- /dev/null +++ b/browser/data-browser/src/routes/Share/useInheritedRights.ts @@ -0,0 +1,45 @@ +import { type Resource, type Right, urls, RightType } from '@tomic/react'; +import { useState, useEffect } from 'react'; +import type { MergedRight } from './useRights'; + +export const useInheritedRights = (resource: Resource): MergedRight[] => { + const [inheritedRights, setInheritedRights] = useState<MergedRight[]>([]); + + useEffect(() => { + resource.getRights().then(allRights => { + const rights = allRights + .filter(r => r.setIn !== resource.subject) + // Make sure the public agent is always the top of the list + .toSorted(a => { + return a.for === urls.instances.publicAgent ? -1 : 1; + }); + + setInheritedRights(toMergedRights(rights)); + }); + }, [resource]); + + return inheritedRights; +}; + +const buildKey = (right: Right) => `${right.for}::${right.setIn}`; + +function toMergedRights(rights: Right[]): MergedRight[] { + const rightsMap = new Map<string, MergedRight>(); + + rights.forEach(right => { + const key = buildKey(right); + const existing = rightsMap.get(key) ?? { + read: false, + write: false, + agentSubject: right.for, + setIn: right.setIn, + }; + + existing.read ||= right.type === RightType.READ; + existing.write ||= right.type === RightType.WRITE; + + rightsMap.set(key, existing); + }); + + return Array.from(rightsMap.values()); +} diff --git a/browser/data-browser/src/routes/Share/useRights.ts b/browser/data-browser/src/routes/Share/useRights.ts new file mode 100644 index 000000000..2367d1a23 --- /dev/null +++ b/browser/data-browser/src/routes/Share/useRights.ts @@ -0,0 +1,86 @@ +import { type Resource, useArray, core, urls } from '@tomic/react'; +import { useMemo } from 'react'; + +export interface RightBools { + read: boolean; + write: boolean; +} + +export interface MergedRight extends RightBools { + agentSubject: string; + setIn: string; +} + +type UpdateRights = (agent: string, write: boolean, state: boolean) => void; + +export function useRights( + resource: Resource, + onError: (e: Error | undefined) => void, +): [rights: MergedRight[], updateRights: UpdateRights] { + const valueOpts = { + commit: false, + handleValidationError: onError, + }; + + const [writers, setWriters] = useArray( + resource, + core.properties.write, + valueOpts, + ); + const [readers, setReaders] = useArray( + resource, + core.properties.read, + valueOpts, + ); + + const rights: MergedRight[] = useMemo(() => { + const rightsMap = new Map<string, RightBools>(); + + // Always show the public agent + rightsMap.set(urls.instances.publicAgent, { read: false, write: false }); + + readers.map(agent => { + rightsMap.set(agent, { + read: true, + write: false, + }); + }); + + writers.map(agent => { + const old = rightsMap.get(agent) ?? { read: false, write: false }; + rightsMap.set(agent, { + ...old, + write: true, + }); + }); + + return Array.from(rightsMap.entries()) + .map(([agent, right]) => ({ + agentSubject: agent, + setIn: resource.subject, + read: right.read, + write: right.write, + })) + .sort(a => { + return a.agentSubject === urls.instances.publicAgent ? -1 : 1; + }); + }, [readers, writers]); + + function updateRights(agent: string, write: boolean, state: boolean) { + let agents = write ? writers : readers; + + if (state) { + agents = Array.from(new Set([...agents, agent])); + } else { + agents = agents.filter(s => s !== agent); + } + + if (write) { + setWriters(agents); + } else { + setReaders(agents); + } + } + + return [rights, updateRights]; +} diff --git a/browser/data-browser/src/routes/ShareRoute.tsx b/browser/data-browser/src/routes/ShareRoute.tsx deleted file mode 100644 index 5d9135e95..000000000 --- a/browser/data-browser/src/routes/ShareRoute.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Right, urls, useArray, useCanWrite, useResource } from '@tomic/react'; -import { ContainerNarrow } from '../components/Containers'; -import { useCurrentSubject } from '../helpers/useCurrentSubject'; -import { ResourceInline } from '../views/ResourceInline'; -import { Card, CardInsideFull, CardRow } from '../components/Card'; -import { FaGlobe } from 'react-icons/fa'; -import { styled } from 'styled-components'; -import { Button } from '../components/Button'; -import { InviteForm } from '../components/InviteForm'; -import toast from 'react-hot-toast'; -import { Title } from '../components/Title'; -import { constructOpenURL } from '../helpers/navigation'; -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 { - const [subject] = useCurrentSubject(); - const resource = useResource(subject); - const [canWrite] = useCanWrite(resource); - const [showInviteForm, setShowInviteForm] = useState(false); - const [err, setErr] = useState<Error | undefined>(undefined); - const navigate = useNavigate(); - - const useValueOpts = { - commit: false, - handleValidationError: setErr, - }; - - const [writers, setWriters] = useArray( - resource, - urls.properties.write, - useValueOpts, - ); - const [readers, setReaders] = useArray( - resource, - urls.properties.read, - useValueOpts, - ); - - const [inheritedRights, setInheritedRights] = useState<Right[]>([]); - - useEffect(() => { - async function getTheRights() { - const allRights = await resource.getRights(); - const inherited = allRights.filter(r => r.setIn !== subject); - - // Make sure the public agent is always the top of the list - const sorted = inherited.sort((a, _b) => { - return a.for === urls.instances.publicAgent ? -1 : 1; - }); - - setInheritedRights(sorted); - } - - getTheRights(); - }, [resource]); - - if (!subject) { - return <>No subject passed</>; - } - - function handleSetRight(agent: string, write: boolean, setToTrue: boolean) { - let agents = write ? writers : readers; - - if (setToTrue) { - // remove previous occurence - agents = agents.filter(s => s !== agent); - agents.push(agent); - } else { - agents = agents.filter(s => s !== agent); - } - - if (write) { - setWriters(agents); - } else { - setReaders(agents); - } - } - - function constructAgentProps(): AgentRight[] { - const rightsMap: Map<string, RightBools> = new Map(); - - // Always show the public agent - rightsMap.set(urls.instances.publicAgent, { read: false, write: false }); - - readers.map(agent => { - rightsMap.set(agent, { - read: true, - write: false, - }); - }); - - writers.map(agent => { - const old = rightsMap.get(agent); - rightsMap.set(agent, { - read: old ? old.read : false, - write: true, - }); - }); - - const rights: AgentRight[] = []; - - rightsMap.forEach((right, agent) => { - rights.push({ - agentSubject: agent, - read: right.read, - write: right.write, - }); - }); - - // Make sure the public agent is always the top of the list - const sorted = rights.sort(a => { - return a.agentSubject === urls.instances.publicAgent ? -1 : 1; - }); - - return sorted; - } - - async function handleSave() { - try { - await resource.save(); - toast.success('Share settings saved'); - navigate(constructOpenURL(subject!)); - } catch (e) { - toast.error(e.message); - } - } - - return ( - <Main subject={subject}> - <ContainerNarrow> - <Column> - <Title resource={resource} prefix='Share settings' link /> - {canWrite && !showInviteForm && ( - <span> - <Button onClick={() => setShowInviteForm(true)}> - <FaShare /> - Create Invite - </Button> - </span> - )} - {showInviteForm && <InviteForm target={resource} />} - <Card> - <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 => ( - <AgentRights - key={JSON.stringify(right)} - {...right} - handleSetRight={ - canWrite && resource.isReady() ? handleSetRight : undefined - } - /> - ))} - </CardInsideFull> - </Card> - {canWrite && ( - <span> - <Button - disabled={!resource.hasUnsavedChanges()} - onClick={handleSave} - > - Save - </Button> - </span> - )} - {err && <ErrorLook>{err.message}</ErrorLook>} - {inheritedRights.length > 0 && ( - <Card> - <RightsHeader text='Inherited rights:' /> - <CardInsideFull> - {inheritedRights.map(right => ( - <AgentRights - inheritedFrom={right.setIn} - key={right.for + right.type} - read={right.type === 'read'} - write={right.type === 'write'} - agentSubject={right.for} - /> - ))} - </CardInsideFull> - </Card> - )} - </Column> - </ContainerNarrow> - </Main> - ); -} - -interface RightBools { - read: boolean; - write: boolean; -} - -interface AgentRight extends RightBools { - agentSubject: string; -} - -interface AgentRightsProps extends AgentRight { - inheritedFrom?: string; - handleSetRight?: (agent: string, write: boolean, setToTrue: boolean) => void; -} - -function AgentRights({ - handleSetRight, - agentSubject, - inheritedFrom, - read, - write, -}: AgentRightsProps): JSX.Element { - const isPublicRight = agentSubject === urls.instances.publicAgent; - const resource = useResource(agentSubject); - const disabled = !resource.isReady() || !handleSetRight; - - return ( - <CardRow> - <div - style={{ display: 'flex' }} - data-test={isPublicRight ? 'right-public' : null} - > - <div style={{ flex: 1 }}> - {isPublicRight ? ( - <> - <FaGlobe /> Public (anyone){' '} - </> - ) : ( - <ResourceInline subject={agentSubject} /> - )} - {inheritedFrom && ( - <> - {' (via '} - <ResourceInline subject={inheritedFrom} /> - {') '} - </> - )} - </div> - <div style={{ alignSelf: 'flex-end' }}> - <StyledCheckbox - type='checkbox' - disabled={disabled} - onChange={e => - handleSetRight && - handleSetRight(agentSubject, false, e.target.checked) - } - checked={read} - title={ - read - ? 'Read access. Toggle to remove access.' - : 'No read access. Toggle to give read access.' - } - /> - <StyledCheckbox - type='checkbox' - disabled={disabled} - onChange={e => - handleSetRight && - handleSetRight(agentSubject, true, e.target.checked) - } - checked={write} - title={ - write - ? 'Write access. Toggle to remove access.' - : 'No write access. Toggle to give write access.' - } - /> - </div> - </div> - </CardRow> - ); -} - -const StyledCheckbox = styled.input` - width: 2rem; -`; - -function RightsHeader({ text }: { text: string }): JSX.Element { - return ( - <div - style={{ - display: 'flex', - flexDirection: 'row', - flex: 1, - marginBottom: '1rem', - }} - > - <div style={{ flex: 1, fontWeight: 'bold' }}>{text}</div> - <div style={{ alignSelf: 'flex-end', justifyContent: 'center' }}> - <span>read </span> - <span>write</span> - </div> - </div> - ); -} diff --git a/browser/data-browser/src/styling.tsx b/browser/data-browser/src/styling.tsx index bac0b08b7..2e617743e 100644 --- a/browser/data-browser/src/styling.tsx +++ b/browser/data-browser/src/styling.tsx @@ -46,6 +46,36 @@ export const animationDuration = 100; const breadCrumbBarHeight = '2.2rem'; const floatingSearchBarPadding = '4.2rem'; +function size(index = 3): string { + const sizes = [ + size.raw(0.25), + size.raw(0.5), + size.raw(1), + size.raw(1.25), + size.raw(1.5), + size.raw(1.75), + size.raw(2), + size.raw(3), + size.raw(4), + size.raw(5), + size.raw(7.5), + size.raw(10), + size.raw(15), + size.raw(20), + size.raw(30), + ]; + + const sizeStr = sizes[index - 1]; + + if (sizeStr === undefined) { + throw new Error(`Size index ${index} out of bounds`); + } + + return sizeStr; +} + +size.raw = (multiplier: number) => `${multiplier}rem`; + /** Construct a StyledComponents theme object */ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => { const main = darkMode ? lighten(0.2, mainIn) : mainIn; @@ -80,6 +110,7 @@ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => { floatingSearchBarPadding: floatingSearchBarPadding, fullPage: `calc(100% - ${breadCrumbBarHeight})`, }, + size, colors: { main, mainLight: darkMode ? lighten(0.08)(main) : lighten(0.08)(main), @@ -118,7 +149,10 @@ declare module 'styled-components' { boxShadow: string; boxShadowIntense: string; boxShadowSoft: string; - /** Base margin */ + /** + * @deprecated + * use size() instead + */ margin: number; /** Width of the container, in rem */ containerWidth: number; @@ -134,6 +168,29 @@ declare module 'styled-components' { fullPage: string; floatingSearchBarPadding: string; }; + + /** + * Function that returns a size in rem for the given index. + * Based on the following ratio: + * 1) size.raw(0.25), + * 2) size.raw(0.5), + * 3) size.raw(1), + * 4) size.raw(1.25), + * 5) size.raw(1.5), + * 6) size.raw(1.75), + * 7) size.raw(2), + * 8) size.raw(3), + * 9) size.raw(4), + * 10) size.raw(5), + * 11) size.raw(7.5), + * 12) size.raw(10), + * 13) size.raw(15), + * 14) size.raw(20), + * 15) size.raw(30), + * + * When given no index it returns the default size (3) + */ + size: typeof size; colors: { /** Main accent color, used for links */ main: string; @@ -208,7 +265,7 @@ export const GlobalStyle = createGlobalStyle` overflow-wrap: anywhere; margin: 0; /** Pretty dark mode transition */ - transition: background .2s ease, border-color .2s ease, color .2s ease; + transition: background-color .2s ease, border-color .2s ease, color .2s ease; font-size: 0.95rem; } @@ -230,7 +287,7 @@ export const GlobalStyle = createGlobalStyle` } h1,h2,h3,h4,h5,h6 { - margin-bottom: ${props => props.theme.margin}rem; + margin-bottom: ${props => props.theme.size()}; font-weight: bold; font-family: ${p => p.theme.fontFamilyHeader}; line-height: 1em; @@ -244,18 +301,18 @@ export const GlobalStyle = createGlobalStyle` p { margin-top: 0; - margin-bottom: ${props => props.theme.margin}rem; + margin-bottom: ${props => props.theme.size()}; } ul { margin-top: 0; - margin-bottom: ${props => props.theme.margin}rem; + margin-bottom: ${props => props.theme.size()}; padding: 0; li { list-style-type: disc; - margin-left: ${props => props.theme.margin * 2}rem; - margin-bottom: ${props => props.theme.margin / 2}rem; + margin-left: ${props => props.theme.size(7)}; + margin-bottom: ${props => props.theme.size(7)}; } } diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx index 4410c9eb6..c084c13c8 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx @@ -14,6 +14,7 @@ import { transitionName, } from '../../../helpers/transitionName'; import { NewClassInstanceButton } from './NewClassInstanceButton'; +import { CARD_CONTAINER } from '../../../helpers/containers'; interface ClassCardReadProps { subject: string; @@ -58,7 +59,7 @@ export function ClassCardRead({ subject }: ClassCardReadProps): JSX.Element { } const StyledCard = styled(Card)<ViewTransitionProps>` - padding-bottom: ${p => p.theme.margin}rem; + padding-bottom: ${p => p.theme.size()}; ${props => transitionName(RESOURCE_PAGE_TRANSITION_TAG, props.subject)}; `; @@ -74,6 +75,4 @@ const StyledH4 = styled.h4` margin-bottom: 0px; `; -const StyledTable = styled.table` - border-collapse: collapse; -`; +const StyledTable = styled.div``; diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 7aed1f94c..062293236 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -54,7 +54,7 @@ export function OntologyPage({ resource }: ResourcePageProps) { <OntologySidebar ontology={resource} /> </SidebarSlot> <ListSlot> - <Column> + <Column style={{ paddingBottom: '3rem' }}> <OntologyDescription edit={editMode} resource={resource} /> <h2>Classes</h2> <StyledUl> @@ -106,6 +106,15 @@ export function OntologyPage({ resource }: ResourcePageProps) { ); } +const SidebarSlot = styled.div` + grid-area: sidebar; +`; + +const ListSlot = styled.div` + grid-area: list; + padding: ${p => p.theme.size()}; +`; + const FullPageWrapper = styled.div<{ edit: boolean }>` --ontology-graph-position: sticky; --ontology-graph-ratio: 9 / 16; @@ -113,9 +122,9 @@ const FullPageWrapper = styled.div<{ edit: boolean }>` display: grid; grid-template-areas: ${p => p.edit - ? `'sidebar title title' 'sidebar list list'` - : `'sidebar title graph' 'sidebar list graph'`}; - grid-template-columns: minmax(auto, 13rem) 3fr 2fr; + ? `'title title sidebar' 'list list sidebar'` + : `'title graph sidebar' 'list graph sidebar'`}; + grid-template-columns: 3fr 2fr minmax(auto, 13rem); grid-template-rows: 4rem auto; width: 100%; min-height: ${p => p.theme.heights.fullPage}; @@ -123,10 +132,10 @@ const FullPageWrapper = styled.div<{ edit: boolean }>` @container (max-width: 950px) { grid-template-areas: ${p => p.edit - ? `'sidebar title' 'sidebar list' 'sidebar list'` - : `'sidebar title' 'sidebar graph' 'sidebar list'`}; + ? `'title sidebar' 'list sidebar' 'list sidebar'` + : `'title sidebar' 'graph sidebar' 'list sidebar'`}; - grid-template-columns: 1fr 5fr; + grid-template-columns: 5fr minmax(auto, 13rem); grid-template-rows: 4rem auto auto; --ontology-graph-position: sticky; --ontology-graph-ratio: 16/9; @@ -135,29 +144,27 @@ const FullPageWrapper = styled.div<{ edit: boolean }>` @container (max-width: 600px) { grid-template-areas: ${p => p.edit ? `'title' 'list' 'list'` : `'title' 'graph' 'list'`}; - grid-template-columns: 100%; + grid-template-columns: 100vw; + + ${SidebarSlot} { + display: none; + } } - padding-bottom: 3rem; + ${ListSlot} { + width: ${p => (p.edit ? 'min(100%, 80rem)' : 'unset')}; + margin: ${p => (p.edit ? '0 auto' : 'unset')}; + } `; const TitleSlot = styled.div` grid-area: title; - padding: ${p => p.theme.margin}rem; -`; - -const SidebarSlot = styled.div` - grid-area: sidebar; -`; - -const ListSlot = styled.div` - grid-area: list; - padding: ${p => p.theme.margin}rem; + padding: ${p => p.theme.size()}; `; const GraphSlot = styled.div` grid-area: graph; - padding: ${p => p.theme.margin}rem; + padding: ${p => p.theme.size()}; height: 100%; `; diff --git a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx index fe5ab2691..bdc90e4a5 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx @@ -93,7 +93,7 @@ const Wrapper = styled.div` flex-direction: column; background-color: ${p => p.theme.colors.bg}; height: 100vh; - border-right: 1px solid ${p => p.theme.colors.bg2}; + border-left: 1px solid ${p => p.theme.colors.bg2}; min-width: 10rem; `; diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineRead.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineRead.tsx index 02871455c..dda685550 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineRead.tsx @@ -4,6 +4,7 @@ import { styled } from 'styled-components'; import Markdown from '../../../components/datatypes/Markdown'; import { InlineDatatype } from '../InlineDatatype'; import { ErrorLook } from '../../../components/ErrorLook'; +import { CARD_CONTAINER } from '../../../helpers/containers'; interface PropertyLineReadProps { subject: string; @@ -24,20 +25,49 @@ export function PropertyLineRead({ } return ( - <tr> - <StyledTd>{resource.title}</StyledTd> - <StyledTd> + <SubGrid> + <PropTitle>{resource.title}</PropTitle> + <DatatypeSlot> <InlineDatatype resource={resource} /> - </StyledTd> - <StyledTd> + </DatatypeSlot> + <MarkdownWrapper> <Markdown text={description ?? ''} /> - </StyledTd> - </tr> + </MarkdownWrapper> + </SubGrid> ); } -const StyledTd = styled.td` - padding-inline: 0.5rem; - padding-block: 0.4rem; - vertical-align: top; +const SubGrid = styled.div` + display: grid; + grid-template-columns: 1fr auto; + gap: 1rem; + padding: ${p => p.theme.size()}; + border-radius: ${p => p.theme.radius}; + + @container ${CARD_CONTAINER} (inline-size < 400px) { + grid-template-columns: 1fr; + } + + &:nth-child(odd) { + background-color: ${p => p.theme.colors.bg1}; + } +`; + +const MarkdownWrapper = styled.span` + @container ${CARD_CONTAINER} (inline-size > 400px) { + grid-column: 1 / 3; + } + + color: ${({ theme }) => theme.colors.textLight}; + padding-bottom: 0.5rem; +`; + +const PropTitle = styled.span` + font-weight: bold; +`; + +const DatatypeSlot = styled.span` + @container ${CARD_CONTAINER} (inline-size > 400px) { + justify-self: end; + } `; diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx index 0ebbb3589..82f75fedd 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx @@ -64,21 +64,23 @@ export function PropertyLineWrite({ resource={resource} property={descriptionProp} /> - <PropertyDatatypePicker disabled={disabled} resource={resource} /> - <IconButton - title={`Configure ${resource.title}`} - color='textLight' - onClick={show} - > - <FaSlidersH /> - </IconButton> - <IconButton - title='remove' - color='textLight' - onClick={() => onRemove(subject)} - > - <FaTimes /> - </IconButton> + <Row center> + <PropertyDatatypePicker disabled={disabled} resource={resource} /> + <IconButton + title={`Configure ${resource.title}`} + color='textLight' + onClick={show} + > + <FaSlidersH /> + </IconButton> + <IconButton + title='remove' + color='textLight' + onClick={() => onRemove(subject)} + > + <FaTimes /> + </IconButton> + </Row> </Row> <PropertyWriteDialog resource={resource} {...dialogProps} close={hide} /> </ListItem> diff --git a/browser/data-browser/src/views/OntologyPage/Property/filterAllowsOnly.ts b/browser/data-browser/src/views/OntologyPage/Property/filterAllowsOnly.ts index 52ae065aa..812caa2ea 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/filterAllowsOnly.ts +++ b/browser/data-browser/src/views/OntologyPage/Property/filterAllowsOnly.ts @@ -14,7 +14,7 @@ export async function filterAllowsOnly( const filteredTags: string[] = []; for (const line of allowsOnly) { - const lineResource = await store.getResourceAsync(line); + const lineResource = await store.getResource(line); if (lineResource.hasClasses(isA)) { filteredTags.push(line); diff --git a/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx b/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx index 6d889b307..2e041340a 100644 --- a/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx +++ b/browser/data-browser/src/views/OntologyPage/PropertyDatatypePicker.tsx @@ -7,6 +7,7 @@ import { useString, } from '@tomic/react'; import { AtomicSelectInput } from '../../components/forms/AtomicSelectInput'; +import styled from 'styled-components'; interface PropertyDatatypePickerProps { resource: Resource; disabled?: boolean; @@ -44,7 +45,7 @@ export function PropertyDatatypePicker({ }; return ( - <AtomicSelectInput + <StyledAtomicSelectInput commit disabled={disabled} resource={resource} @@ -54,3 +55,7 @@ export function PropertyDatatypePicker({ /> ); } + +const StyledAtomicSelectInput = styled(AtomicSelectInput)` + min-width: 18ch; +`; diff --git a/browser/data-browser/src/views/ResourcePage.tsx b/browser/data-browser/src/views/ResourcePage.tsx index b81671301..afc517a2a 100644 --- a/browser/data-browser/src/views/ResourcePage.tsx +++ b/browser/data-browser/src/views/ResourcePage.tsx @@ -2,11 +2,12 @@ import { useEffect } from 'react'; import { useString, useResource, - properties, Resource, - urls, type OptionalClass, dataBrowser, + collections, + server, + core, } from '@tomic/react'; import { ContainerNarrow } from '../components/Containers'; @@ -25,7 +26,6 @@ import { ChatRoomPage } from './ChatRoomPage'; import { MessagePage } from './MessagePage'; import { BookmarkPage } from './BookmarkPage/BookmarkPage'; import { ImporterPage } from './ImporterPage.jsx'; -import Parent from '../components/Parent'; import { FolderPage } from './FolderPage'; import { ArticlePage } from './Article'; import { TablePage } from './TablePage'; @@ -49,7 +49,7 @@ type Props = { */ function ResourcePage({ subject }: Props): JSX.Element { const resource = useResource(subject); - const [klass] = useString(resource, properties.isA); + const [klass] = useString(resource, core.properties.isA); // The body can have an inert attribute when the user navigated from an open dialog. // we remove it to make the page becomes interavtive again. @@ -79,50 +79,47 @@ function ResourcePage({ subject }: Props): JSX.Element { const ReturnComponent = selectComponent(klass!); return ( - <> - <Parent resource={resource} /> - <Main subject={subject}> - <ErrorBoundary> - <ReturnComponent resource={resource} /> - </ErrorBoundary> - </Main> - </> + <Main subject={subject}> + <ErrorBoundary> + <ReturnComponent resource={resource} /> + </ErrorBoundary> + </Main> ); } function selectComponent(klass: string) { switch (klass) { - case urls.classes.collection: + case collections.classes.collection: return Collection; - case urls.classes.endpoint: + case server.classes.endpoint: return EndpointPage; - case urls.classes.drive: + case server.classes.drive: return DrivePage; - case urls.classes.redirect: + case server.classes.redirect: return RedirectPage; - case urls.classes.invite: + case server.classes.invite: return InvitePage; - case urls.classes.document: + case dataBrowser.classes.document: return DocumentPage; - case urls.classes.class: + case core.classes.class: return ClassPage; - case urls.classes.file: + case server.classes.file: return FilePage; - case urls.classes.chatRoom: + case dataBrowser.classes.chatroom: return ChatRoomPage; - case urls.classes.message: + case dataBrowser.classes.message: return MessagePage; - case urls.classes.bookmark: + case dataBrowser.classes.bookmark: return BookmarkPage; - case urls.classes.importer: + case dataBrowser.classes.importer: return ImporterPage; - case urls.classes.folder: + case dataBrowser.classes.folder: return FolderPage; - case urls.classes.article: + case dataBrowser.classes.article: return ArticlePage; - case urls.classes.table: + case dataBrowser.classes.table: return TablePage; - case urls.classes.ontology: + case core.classes.ontology: return OntologyPage; case dataBrowser.classes.tag: return TagPage; diff --git a/browser/e2e/tests/search.spec.ts b/browser/e2e/tests/search.spec.ts index b91eb6876..dc18a8d79 100644 --- a/browser/e2e/tests/search.spec.ts +++ b/browser/e2e/tests/search.spec.ts @@ -10,6 +10,7 @@ import { editTitle, setTitle, sideBarNewResourceTestId, + contextMenuClick, } from './test-utils'; test.describe('search', async () => { test.beforeEach(before); @@ -57,7 +58,7 @@ test.describe('search', async () => { // Set search scope to 'Cake folder' await page.waitForTimeout(REBUILD_INDEX_TIME); await page.reload(); - await page.locator('button[title="Search in Cake Folder"]').click(); + await contextMenuClick('scope', page); // Search for 'Avocado' await page.locator('[data-test="address-bar"]').type('Avocado'); // I don't like the `.first` here, but for some reason there is one frame where diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index e49b0ef31..70e83a569 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -770,7 +770,7 @@ export class Resource<C extends OptionalClass = any> { } /** Type of Rights (e.g. read or write) */ -enum RightType { +export enum RightType { /** Open a resource or its children */ READ = 'read', /** Edit or delete a resource or its children */ From f4e3b15a539b6e59fdae21afcc61eb7483768ab6 Mon Sep 17 00:00:00 2001 From: Polle Pas <polleps@gmail.com> Date: Tue, 9 Jul 2024 16:37:56 +0200 Subject: [PATCH 11/13] Fix type/lint errors --- browser/.eslintrc.cjs | 1 + browser/data-browser/src/views/CodeUsage/PropSelector.tsx | 6 +----- .../src/views/OntologyPage/Class/ClassCardRead.tsx | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/browser/.eslintrc.cjs b/browser/.eslintrc.cjs index 85eda6450..018dad245 100644 --- a/browser/.eslintrc.cjs +++ b/browser/.eslintrc.cjs @@ -59,6 +59,7 @@ module.exports = { // "import/extensions": ["error", "ignorePackages"], "import/no-unresolved": "off", 'import/no-dynamic-require': 'off', // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md + 'import/no-named-as-default': 'off', 'no-inner-declarations': 'off', // https://eslint.org/docs/rules/no-inner-declarations// New rules 'class-methods-use-this': 'off', //Allow underscores https://stackoverflow.com/questions/57802057/eslint-configuring-no-unused-vars-for-typescript diff --git a/browser/data-browser/src/views/CodeUsage/PropSelector.tsx b/browser/data-browser/src/views/CodeUsage/PropSelector.tsx index 1c33eee13..76c85e52d 100644 --- a/browser/data-browser/src/views/CodeUsage/PropSelector.tsx +++ b/browser/data-browser/src/views/CodeUsage/PropSelector.tsx @@ -22,11 +22,7 @@ export function PropSelector({ const props = useResources(allProps); return ( - <BasicSelect - onChange={e => onPropSelect(e.target.value)} - placeholder='Select a property' - defaultValue={''} - > + <BasicSelect onChange={e => onPropSelect(e.target.value)} defaultValue={''}> <option value=''>None</option> <hr /> {Array.from(props.entries()).map(([prop, propResource]) => ( diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx index c084c13c8..6b7f6974a 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx @@ -14,7 +14,6 @@ import { transitionName, } from '../../../helpers/transitionName'; import { NewClassInstanceButton } from './NewClassInstanceButton'; -import { CARD_CONTAINER } from '../../../helpers/containers'; interface ClassCardReadProps { subject: string; From ee7434d36c899a506887fba6377df0f9ce4f3e95 Mon Sep 17 00:00:00 2001 From: Polle Pas <polleps@gmail.com> Date: Mon, 15 Jul 2024 14:56:44 +0200 Subject: [PATCH 12/13] #906 Reset resource after cancel edit --- browser/CHANGELOG.md | 2 + .../src/components/EditableTitle.tsx | 12 ++-- .../src/components/IconButton/IconButton.tsx | 4 +- .../ResourceSideBar/ResourceSideBar.tsx | 11 ++- .../ResourceSideBar/SidebarItemTitle.tsx | 3 + .../src/components/UnsavedIndicator.tsx | 31 ++++++++ .../src/components/forms/ResourceForm.tsx | 49 +++++++------ .../forms/{ => ValueForm}/ValueForm.tsx | 72 ++++--------------- .../forms/ValueForm/ValueFormEdit.tsx | 66 +++++++++++++++++ .../src/components/forms/ValueForm/index.ts | 1 + browser/data-browser/src/routes/EditRoute.tsx | 29 +++++--- .../src/views/ResourcePageDefault.tsx | 29 ++++---- browser/e2e/tests/e2e.spec.ts | 2 +- browser/lib/src/resource.ts | 7 ++ browser/lib/src/store.ts | 4 +- 15 files changed, 205 insertions(+), 117 deletions(-) create mode 100644 browser/data-browser/src/components/UnsavedIndicator.tsx rename browser/data-browser/src/components/forms/{ => ValueForm}/ValueForm.tsx (53%) create mode 100644 browser/data-browser/src/components/forms/ValueForm/ValueFormEdit.tsx create mode 100644 browser/data-browser/src/components/forms/ValueForm/index.ts diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 49b5d4e9d..9f78c7bb7 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -19,10 +19,12 @@ This changelog covers all five packages, as they are (for now) updated as a whol - Redesigned the ontology page. - Moved the resource context menu to the top of the page. - [#861](https://github.com/atomicdata-dev/atomic-server/issues/861) Fix long usernames overflowing on the share page. +- [#906](https://github.com/atomicdata-dev/atomic-server/issues/906) Reset changes after clicking the cancel button in a form or navigating away. ### @tomic/lib - Added `LocalChange` event to `Resource`. +- Added `resource.refresh()` method. ### @tomic/react diff --git a/browser/data-browser/src/components/EditableTitle.tsx b/browser/data-browser/src/components/EditableTitle.tsx index fc5e66694..a48ffc26a 100644 --- a/browser/data-browser/src/components/EditableTitle.tsx +++ b/browser/data-browser/src/components/EditableTitle.tsx @@ -8,6 +8,7 @@ import { transitionName, } from '../helpers/transitionName'; import { ViewTransitionProps } from '../helpers/ViewTransitionProps'; +import { UnsavedIndicator } from './UnsavedIndicator'; export interface EditableTitleProps { resource: Resource; @@ -80,15 +81,18 @@ export function EditableTitle({ disabled={!canEdit} id={id} canEdit={!!canEdit} - title={canEdit ? 'Edit title' : 'View title'} + title={canEdit ? 'Click to edit title' : ''} data-test='editable-title' onClick={handleClick} subtle={!!canEdit && !text} - subject={resource.getSubject()} + subject={resource.subject} className={className} > <> - {text || placeholder} + <span> + {text || placeholder} + <UnsavedIndicator resource={resource} /> + </span> {canEdit && <Icon />} </> @@ -109,7 +113,7 @@ const Title = styled.h1` ${TitleShared} display: flex; align-items: center; - gap: ${p => p.theme.margin}rem; + gap: ${p => p.theme.size()}; cursor: ${props => (props.canEdit ? 'pointer' : 'initial')}; opacity: ${props => (props.subtle ? 0.5 : 1)}; diff --git a/browser/data-browser/src/components/IconButton/IconButton.tsx b/browser/data-browser/src/components/IconButton/IconButton.tsx index 7b1079229..2f7d56026 100644 --- a/browser/data-browser/src/components/IconButton/IconButton.tsx +++ b/browser/data-browser/src/components/IconButton/IconButton.tsx @@ -93,9 +93,7 @@ const IconButtonBase = styled.button` border: none; user-select: none; padding: var(--button-padding); - width: calc(${p => p.size} + var(--button-padding) * 2); - height: calc(${p => p.size} + var(--button-padding) * 2); - + aspect-ratio: 1/1; margin-inline-start: ${p => p.edgeAlign === 'start' ? 'calc(var(--button-padding) * -1)' : '0'}; diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx index 1efeb8f28..9dda134f5 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx @@ -7,6 +7,7 @@ import { useCanWrite, core, dataBrowser, + unknownSubject, } from '@tomic/react'; import { useCurrentSubject } from '../../../helpers/useCurrentSubject'; import { SideBarItem } from '../SideBarItem'; @@ -33,12 +34,12 @@ interface ResourceSideBarProps { } /** Renders a Resource as a nav item for in the sidebar. */ -export function ResourceSideBar({ +export const ResourceSideBar: React.FC = ({ subject, renderedHierargy, ancestry, onClick, -}: ResourceSideBarProps): JSX.Element { +}) => { if (renderedHierargy.length === 0) { throw new Error('renderedHierargy should not be empty'); } @@ -103,6 +104,10 @@ export function ResourceSideBar({ } }, [ancestry]); + if (!subject || subject === unknownSubject) { + return null; + } + if (resource.loading) { return ( ); -} +}; const Wrapper = styled.div<{ highlight: boolean }>` background-color: ${p => diff --git a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx index 8602d2139..bb71de7ba 100644 --- a/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx +++ b/browser/data-browser/src/components/SideBar/ResourceSideBar/SidebarItemTitle.tsx @@ -14,6 +14,7 @@ import { import { useSettings } from '../../../helpers/AppSettings'; import { IconButton } from '../../IconButton/IconButton'; import { FaGripVertical } from 'react-icons/fa6'; +import { UnsavedIndicator } from '../../UnsavedIndicator'; interface SidebarItemTitleProps { subject: string; @@ -70,6 +71,7 @@ export const SidebarItemTitle = forwardRef< {resource.title} + @@ -90,6 +92,7 @@ export const SidebarItemTitle = forwardRef< {resource.title} + diff --git a/browser/data-browser/src/components/UnsavedIndicator.tsx b/browser/data-browser/src/components/UnsavedIndicator.tsx new file mode 100644 index 000000000..9bb2a8953 --- /dev/null +++ b/browser/data-browser/src/components/UnsavedIndicator.tsx @@ -0,0 +1,31 @@ +import { ResourceEvents, type Resource } from '@tomic/react'; +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +interface UnsavedIndicatorProps { + resource: Resource; +} + +export const UnsavedIndicator: React.FC = ({ + resource, +}) => { + const [hasChanges, setHasChanges] = useState(resource.hasUnsavedChanges()); + + useEffect(() => { + setHasChanges(resource.hasUnsavedChanges()); + + return resource.on(ResourceEvents.LocalChange, () => { + setHasChanges(resource.hasUnsavedChanges()); + }); + }, [resource]); + + if (!hasChanges) { + return null; + } + + return *; +}; + +const Indicator = styled.span` + color: ${p => p.theme.colors.warning}; +`; diff --git a/browser/data-browser/src/components/forms/ResourceForm.tsx b/browser/data-browser/src/components/forms/ResourceForm.tsx index bdbdbe85e..d890c27da 100644 --- a/browser/data-browser/src/components/forms/ResourceForm.tsx +++ b/browser/data-browser/src/components/forms/ResourceForm.tsx @@ -3,15 +3,13 @@ import { useNavigate } from 'react-router-dom'; import { useArray, useResource, - useString, Resource, - classes, - properties, - urls, useDebounce, useCanWrite, Client, useStore, + core, + commits, } from '@tomic/react'; import { FaCaretDown, FaCaretRight } from 'react-icons/fa'; @@ -44,14 +42,15 @@ export interface ResourceFormProps { variant?: ResourceFormVariant; onSave?: () => void; + onCancel?: () => void; } -const nonEssentialProps = [ - properties.isA, - properties.parent, - properties.read, - properties.write, - properties.commit.lastCommit, +const nonEssentialProps: string[] = [ + core.properties.isA, + core.properties.parent, + core.properties.write, + core.properties.read, + commits.properties.lastCommit, ]; /** Form for editing and creating a Resource */ @@ -60,8 +59,9 @@ export function ResourceForm({ resource, variant, onSave, + onCancel, }: ResourceFormProps): JSX.Element { - const [isAArray] = useArray(resource, properties.isA); + const [isAArray] = useArray(resource, core.properties.isA); if (classSubject === undefined && isAArray?.length > 0) { // This is not entirely accurate, as Atomic Data supports having multiple @@ -70,9 +70,8 @@ export function ResourceForm({ } const klass = useResource(classSubject); - const [requires] = useArray(klass, properties.requires); - const [recommends] = useArray(klass, properties.recommends); - const [klassIsa] = useString(klass, properties.isA); + const [requires] = useArray(klass, core.properties.requires); + const [recommends] = useArray(klass, core.properties.recommends); const [newPropErr, setNewPropErr] = useState(undefined); const navigate = useNavigate(); /** A list of custom properties, set by the User while editing this form */ @@ -126,7 +125,7 @@ export function ResourceForm({ return <>Loading class...; } - if (klassIsa && klassIsa !== classes.class) { + if (!klass.hasClasses(core.classes.class)) { return ( {classSubject} is not a Class. Only resources with valid classes can be @@ -169,7 +168,7 @@ export function ResourceForm({ } return ( -
+ {classSubject && klass.error && ( @@ -234,25 +233,25 @@ export function ResourceForm({ handleAddProp(set); }} error={newPropErr} - isA={urls.classes.property} + isA={core.classes.property} /> {newPropErr && {newPropErr.message}} - - - - - + {nonEssentialProps.map(prop => ( + + ))} {variant !== ResourceFormVariant.Dialog && ( <> {err && {err.message}} + {onCancel && ( + + )} - - - - + setEditMode(false)} + /> ); } diff --git a/browser/data-browser/src/components/forms/ValueForm/ValueFormEdit.tsx b/browser/data-browser/src/components/forms/ValueForm/ValueFormEdit.tsx new file mode 100644 index 000000000..4c406225b --- /dev/null +++ b/browser/data-browser/src/components/forms/ValueForm/ValueFormEdit.tsx @@ -0,0 +1,66 @@ +import toast from 'react-hot-toast'; +import type { Property, Resource } from '@tomic/react'; +import { FaFloppyDisk } from 'react-icons/fa6'; +import { Button } from '../../Button'; +import { Column, Row } from '../../Row'; +import { ErrMessage } from '../InputStyles'; +import InputSwitcher from '../InputSwitcher'; +import { useEffect, useState } from 'react'; + +interface ValueFormEditProps { + resource: Resource; + property: Property; + onClose: () => void; +} + +export function ValueFormEdit({ + resource, + property, + onClose, +}: ValueFormEditProps): React.JSX.Element { + const [err, setErr] = useState(undefined); + + const save = async () => { + try { + await resource.save(); + onClose(); + toast.success('Resource saved'); + } catch (e) { + setErr(e); + toast.error('Could not save resource...'); + } + }; + + const cancel = () => { + setErr(undefined); + onClose(); + }; + + useEffect(() => { + // Refresh the data when the edit form closes. + return () => { + resource.refresh(); + }; + }, []); + + return ( + + + {err && {err.message}} + + + + + + ); +} diff --git a/browser/data-browser/src/components/forms/ValueForm/index.ts b/browser/data-browser/src/components/forms/ValueForm/index.ts new file mode 100644 index 000000000..d7f4a5088 --- /dev/null +++ b/browser/data-browser/src/components/forms/ValueForm/index.ts @@ -0,0 +1 @@ +export { ValueForm } from './ValueForm'; diff --git a/browser/data-browser/src/routes/EditRoute.tsx b/browser/data-browser/src/routes/EditRoute.tsx index 8faaf3e05..0d8337aba 100644 --- a/browser/data-browser/src/routes/EditRoute.tsx +++ b/browser/data-browser/src/routes/EditRoute.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router'; +import { useCallback, useEffect, useState } from 'react'; import { useResource } from '@tomic/react'; import { constructOpenURL, newURL } from '../helpers/navigation'; import { ContainerNarrow } from '../components/Containers'; @@ -12,6 +11,7 @@ import { Main } from '../components/Main'; import { Column, Row } from '../components/Row'; import { IconButton } from '../components/IconButton/IconButton'; import { FaArrowLeft } from 'react-icons/fa'; +import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition'; /** Form for instantiating a new Resource from some Class */ export function Edit(): JSX.Element { @@ -20,9 +20,9 @@ export function Edit(): JSX.Element { const [subjectInput, setSubjectInput] = useState( undefined, ); - const navigate = useNavigate(); + const navigate = useNavigateWithTransition(); - function handleClassSet(e) { + const handleClassSet: React.FormEventHandler = e => { e.preventDefault(); if (!subjectInput) { @@ -30,11 +30,18 @@ export function Edit(): JSX.Element { } navigate(newURL(subjectInput)); - } + }; - const handleBackClick = () => { + const cancelEdit = useCallback(() => { navigate(constructOpenURL(subject ?? '')); - }; + }, [subject, navigate]); + + useEffect( + () => () => { + resource.refresh(); + }, + [], + ); return (
@@ -46,7 +53,7 @@ export function Edit(): JSX.Element { title={`Back to ${resource.title}`} size='1.4em' edgeAlign='start' - onClick={handleBackClick} + onClick={cancelEdit} > @@ -54,7 +61,11 @@ export function Edit(): JSX.Element { {/* Key is required for re-rendering when subject changes */} - + ) : ( diff --git a/browser/data-browser/src/views/ResourcePageDefault.tsx b/browser/data-browser/src/views/ResourcePageDefault.tsx index 3e602312a..d63aa0678 100644 --- a/browser/data-browser/src/views/ResourcePageDefault.tsx +++ b/browser/data-browser/src/views/ResourcePageDefault.tsx @@ -1,8 +1,8 @@ -import { useString, properties } from '@tomic/react'; +import { useString, core, server, commits } from '@tomic/react'; import AllProps from '../components/AllProps'; import { ClassDetail } from '../components/ClassDetail'; import { ContainerNarrow } from '../components/Containers'; -import { ValueForm } from '../components/forms/ValueForm'; +import { ValueForm } from '../components/forms/ValueForm/ValueForm'; import { ResourcePageProps } from './ResourcePage'; import { CommitDetail } from '../components/CommitDetail'; import { Details } from '../components/Detail'; @@ -15,20 +15,20 @@ import { EditableTitle } from '../components/EditableTitle'; */ export const defaultHiddenProps = [ // Shown as title - properties.name, - properties.shortname, - properties.file.filename, + core.properties.name, + core.properties.shortname, + server.properties.filename, // Shown separately - properties.description, + core.properties.description, // Content should indicate Class in custom views (e.g. document looks like a document) - properties.isA, + core.properties.isA, // Shown in navigation - properties.parent, + core.properties.parent, // Shown in rights / share menu - properties.write, - properties.read, + core.properties.write, + core.properties.read, // Shown in CommitDetail - properties.commit.lastCommit, + commits.properties.lastCommit, ]; /** @@ -38,7 +38,7 @@ export const defaultHiddenProps = [ export function ResourcePageDefault({ resource, }: ResourcePageProps): JSX.Element { - const [lastCommit] = useString(resource, properties.commit.lastCommit); + const [lastCommit] = useString(resource, commits.properties.lastCommit); return ( @@ -47,7 +47,10 @@ export function ResourcePageDefault({
- + { // get current url, append the localID await page.goto(parentSubject + '/' + localID); - await expect(page.locator(`h1:text("${name}")`)).toBeVisible(); + await expect(page.getByRole('heading', { name })).toBeVisible(); }); test('dialog', async ({ page }) => { diff --git a/browser/lib/src/resource.ts b/browser/lib/src/resource.ts index 70e83a569..4c8f1dd3e 100644 --- a/browser/lib/src/resource.ts +++ b/browser/lib/src/resource.ts @@ -756,6 +756,13 @@ export class Resource { this._subject = subject; } + /** Refetches the resource from the server. Will reset all changes to the latest saved version */ + public async refresh(): Promise { + await this.store.fetchResourceFromServer(this.subject, { + noWebSocket: true, + }); + } + 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 1903ed6d1..59c6ceef1 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -326,7 +326,7 @@ export class Store { if (opts.setLoading) { const newR = new Resource(subject); newR.loading = true; - this.addResources(newR); + this.addResources(newR, { skipCommitCompare: true }); } // Use WebSocket if available, else use HTTP(S) @@ -357,7 +357,7 @@ export class Store { }, ); - this.addResources(createdResources); + this.addResources(createdResources, { skipCommitCompare: true }); } return this.resources.get(subject)!; From 5a216d7d0230f4c078a444b17a24f5327752cc0e Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 15 Jul 2024 15:56:44 +0200 Subject: [PATCH 13/13] Update crossfetch for better node compatibility --- browser/lib/package.json | 2 +- browser/pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/browser/lib/package.json b/browser/lib/package.json index f3021715b..79ba805c0 100644 --- a/browser/lib/package.json +++ b/browser/lib/package.json @@ -5,7 +5,7 @@ "@noble/ed25519": "1.6.0", "@noble/hashes": "^0.5.7", "base64-arraybuffer": "^1.0.2", - "cross-fetch": "^3.1.4", + "cross-fetch": "^4.0.0", "fast-json-stable-stringify": "^2.1.0", "ulidx": "^2.3.0" }, diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml index 52a61013a..eb33161a7 100644 --- a/browser/pnpm-lock.yaml +++ b/browser/pnpm-lock.yaml @@ -320,8 +320,8 @@ importers: specifier: ^1.0.2 version: 1.0.2 cross-fetch: - specifier: ^3.1.4 - version: 3.1.8 + specifier: ^4.0.0 + version: 4.0.0 fast-json-stable-stringify: specifier: ^2.1.0 version: 2.1.0 @@ -5065,8 +5065,8 @@ packages: resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==} engines: {node: '>=12.0.0'} - cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} @@ -15943,7 +15943,7 @@ snapshots: dependencies: luxon: 3.4.4 - cross-fetch@3.1.8: + cross-fetch@4.0.0: dependencies: node-fetch: 2.6.12 transitivePeerDependencies: