From 749f14d425eb060326a1e280559e746f9fa15224 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Thu, 21 Nov 2024 12:32:22 +0100 Subject: [PATCH] Ontology Improvements #1008 --- browser/CHANGELOG.md | 7 + browser/cli/readme.md | 12 +- .../src/lib/views/MenuItem/MenuItem.svelte | 3 +- .../src/chunks/GraphViewer/OntologyGraph.tsx | 2 +- .../src/chunks/GraphViewer/buildGraph.ts | 1 + .../src/components/ClassSelectorDialog.tsx | 2 + .../src/components/Dialog/index.tsx | 4 +- .../src/components/Dropdown/index.tsx | 39 ++-- .../src/components/OutlinedSection.tsx | 3 +- .../components/ResourceContextMenu/index.tsx | 12 +- .../components/forms/SearchBox/ResultLine.tsx | 101 ++++++--- .../components/forms/SearchBox/SearchBox.tsx | 7 +- .../forms/SearchBox/SearchBoxWindow.tsx | 192 ++++++++++++++---- browser/data-browser/src/globalCssVars.ts | 3 + .../OntologyPage => helpers}/toAnchorId.ts | 0 browser/data-browser/src/styling.tsx | 12 +- .../generators/BasicCodeGenerator.ts | 28 +-- .../generators/TableCodeGenerator.ts | 130 ++++++------ .../OntologyPage/Class/AddPropertyButton.tsx | 22 +- .../OntologyPage/Class/ClassCardRead.tsx | 2 +- .../OntologyPage/Class/ClassCardWrite.tsx | 9 +- .../OntologyPage/CreateInstanceButton.tsx | 20 +- .../src/views/OntologyPage/DashedButton.tsx | 24 +++ .../src/views/OntologyPage/InfoTitle.tsx | 41 ++++ .../src/views/OntologyPage/InlineDatatype.tsx | 2 +- .../src/views/OntologyPage/NewClassButton.tsx | 31 +-- .../views/OntologyPage/NewPropertyButton.tsx | 128 ++++++++++++ .../src/views/OntologyPage/OntologyPage.tsx | 47 ++++- .../views/OntologyPage/OntologySidebar.tsx | 2 +- .../OntologyPage/Property/EnumFormPart.tsx | 2 +- .../Property/PropertyCardRead.tsx | 2 +- .../Property/PropertyCardWrite.tsx | 9 +- .../Property/PropertyFormCommon.tsx | 4 +- .../Property/PropertyLineWrite.tsx | 11 +- .../Property/PropertyWriteDialog.tsx | 4 +- .../{newClass.ts => ontologyUtils.ts} | 25 ++- browser/e2e/tests/e2e.spec.ts | 2 +- browser/e2e/tests/ontology.spec.ts | 18 +- browser/e2e/tests/test-utils.ts | 2 +- lib/defaults/ontologies.json | 6 +- 40 files changed, 708 insertions(+), 263 deletions(-) create mode 100644 browser/data-browser/src/globalCssVars.ts rename browser/data-browser/src/{views/OntologyPage => helpers}/toAnchorId.ts (100%) create mode 100644 browser/data-browser/src/views/OntologyPage/DashedButton.tsx create mode 100644 browser/data-browser/src/views/OntologyPage/InfoTitle.tsx create mode 100644 browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx rename browser/data-browser/src/views/OntologyPage/{newClass.ts => ontologyUtils.ts} (60%) diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index faff71ffc..8c8401539 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -11,12 +11,19 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#992](https://github.com/atomicdata-dev/atomic-server/issues/992) Fix Searchbox overflowing when displaying long names. - [#999](https://github.com/atomicdata-dev/atomic-server/issues/999) Fix parseMetaTags character escape issue. - [#1014](https://github.com/atomicdata-dev/atomic-server/issues/1014) Fix date input always showing required error even when filled in. +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1005) Showcase standard properties in the resource selector +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add 'New Property' button to the property list in the ontology editor. +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Fix disabled resource selectors still get highlighted on hover. +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add 'open' option to classes and properties in the ontology edit view. +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Updated the look of the resource selector and made it more responsive. +- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add info dropdowns to different sections of the ontology editor for more information about the section. ### @tomic/lib - `resource.props` is now writeable: `resource.props.name = 'New Name'`. - Added `store.preloadResourceTree()` method, see docs for more info. - Fix generated ontologies not working in a Next.js server context. +- SEMI BREAKING CHANGE: When using generated types by cli, @tomic/lib now requires them to be generated by @tomic/cli v0.41.0 or above. ### @tomic/cli diff --git a/browser/cli/readme.md b/browser/cli/readme.md index 51e01135a..ca8f9abe2 100644 --- a/browser/cli/readme.md +++ b/browser/cli/readme.md @@ -1,15 +1,21 @@ _Check out [the docs here](https://docs.atomicdata.dev/js-cli)._ -`@tomic/cli` is an NPM tool that helps the developer with creating a front-end for their atomic data project by providing typesafety on resources. +`@tomic/cli` is a command line tool that helps the developer with creating a front-end for their atomic data project by providing typesafety on resources. In atomic data you can create [ontologies](https://atomicdata.dev/class/ontology) that describe your business model. You can use `@tomic/cli` to generate Typscript types for these ontologies in your front-end. ```typescript -import { Post } from './ontolgies/blog'; // <--- generated +import { type Post } from './ontolgies/blog'; // <--- generated -const myBlogpost = await store.getResourceAsync( +const myBlogpost = await store.getResource( 'https://myblog.com/atomic-is-awesome', ); const comments = myBlogpost.props.comments; // string[] automatically inferred! + +myBlogpost.props.name = 'New title'; +myBlogpost.save(); ``` + +_Check out [the docs here](https://docs.atomicdata.dev/js-cli)._ + diff --git a/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte b/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte index 1158cb9e1..5b454c6bf 100644 --- a/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte +++ b/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte @@ -49,7 +49,7 @@ if (!button || !popover) return; // Check if the anchor position api is supported. If so we don't need to calculate the position. - if (getComputedStyle(popover).getPropertyValue('--supports-position-anchor') !== 'false') { + if (CSS.supports('anchor-name', '--something')) { return; } @@ -142,7 +142,6 @@ position-area: bottom center; @supports not (anchor-name: --something) { - --supports-position-anchor: false; position: fixed; top: var(--top); left: var(--left); diff --git a/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx index 270b6a14d..37183124e 100644 --- a/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx +++ b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx @@ -12,7 +12,7 @@ import { buildGraph } from './buildGraph'; import { FloatingEdge } from './FloatingEdge'; import { useGraph } from './useGraph'; import { useEffectOnce } from '../../hooks/useEffectOnce'; -import { toAnchorId } from '../../views/OntologyPage/toAnchorId'; +import { toAnchorId } from '../../helpers/toAnchorId'; const edgeTypes = { floating: FloatingEdge, diff --git a/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts index 69dbd6596..919fe44d5 100644 --- a/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts +++ b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts @@ -183,6 +183,7 @@ export function applyNodeStyling( borderColor: theme.colors.bg2, color: theme.colors.text, borderStyle: node.data.external ? 'dashed' : 'solid', + opacity: node.data.external ? 0.7 : 1, }, })); } diff --git a/browser/data-browser/src/components/ClassSelectorDialog.tsx b/browser/data-browser/src/components/ClassSelectorDialog.tsx index 80ad813c8..7509d6b6c 100644 --- a/browser/data-browser/src/components/ClassSelectorDialog.tsx +++ b/browser/data-browser/src/components/ClassSelectorDialog.tsx @@ -89,6 +89,8 @@ const OntologySection = ({ subject, onClassSelect }: OntologySectionProps) => { {resource.props.classes?.map(s => ( ))} + {!resource.props.classes || + (resource.props.classes.length === 0 && No classes)} ); diff --git a/browser/data-browser/src/components/Dialog/index.tsx b/browser/data-browser/src/components/Dialog/index.tsx index 9e70549b3..ef5bdb725 100644 --- a/browser/data-browser/src/components/Dialog/index.tsx +++ b/browser/data-browser/src/components/Dialog/index.tsx @@ -17,6 +17,7 @@ import { useDialog } from './useDialog'; import { useControlLock } from '../../hooks/useControlLock'; import { useDialogGlobalContext } from './DialogGlobalContextProvider'; import { DIALOG_CONTENT_CONTAINER } from '../../helpers/containers'; +import { CurrentBackgroundColor } from '../../globalCssVars'; export interface InternalDialogProps { show: boolean; @@ -268,6 +269,7 @@ const fadeInBackground = keyframes` `; const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>` + ${CurrentBackgroundColor.define(p => p.theme.colors.bg)} --dialog-width: min(90vw, ${p => p.$width ?? '60ch'}); ${VAR_DIALOG_INNER_WIDTH}: calc( @@ -280,7 +282,7 @@ const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>` z-index: ${p => p.theme.zIndex.dialog}; padding: ${p => p.theme.size()}; color: ${props => props.theme.colors.text}; - background-color: ${props => props.theme.colors.bg}; + background-color: ${CurrentBackgroundColor.var()}; border-radius: ${props => props.theme.radius}; border: solid 1px ${props => props.theme.colors.bg2}; inline-size: var(--dialog-width); diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx index d7e81ec1a..005b19c24 100644 --- a/browser/data-browser/src/components/Dropdown/index.tsx +++ b/browser/data-browser/src/components/Dropdown/index.tsx @@ -378,6 +378,7 @@ export function MenuItem({ const StyledShortcut = styled(Shortcut)` margin-left: 0.3rem; + color: ${p => p.theme.colors.textLight}; `; const StyledLabel = styled.span` @@ -389,44 +390,46 @@ interface MenuItemStyledProps { } const MenuItemStyled = styled(Button)` + --menu-item-bg: ${p => + p.selected ? p.theme.colors.mainSelectedBg : p.theme.colors.bg}; + --menu-item-fg: ${p => + p.selected ? p.theme.colors.mainSelectedFg : p.theme.colors.text}; align-items: center; display: flex; gap: 0.5rem; width: 100%; text-align: left; - color: ${p => p.theme.colors.text}; + color: var(--menu-item-fg); padding: 0.4rem 1rem; height: auto; - background-color: ${p => - p.selected ? p.theme.colors.bg1 : p.theme.colors.bg}; - text-decoration: ${p => (p.selected ? 'underline' : 'none')}; + background-color: var(--menu-item-bg); + outline: none; & svg { - color: ${p => p.theme.colors.textLight}; + color: var(--menu-item-fg); } + &:hover { - background-color: ${p => p.theme.colors.bg1}; + --menu-item-bg: ${p => p.theme.colors.mainSelectedBg}; + --menu-item-fg: ${p => p.theme.colors.mainSelectedFg}; + + @media (prefers-contrast: more) { + --menu-item-bg: ${p => (p.theme.darkMode ? 'white' : 'black')}; + --menu-item-fg: ${p => (p.theme.darkMode ? 'black' : 'white')}; + } } &:active { - background-color: ${p => p.theme.colors.bg2}; + filter: brightness(0.9); } &:disabled { color: ${p => p.theme.colors.textLight2}; cursor: default; background-color: ${p => p.theme.colors.bg}; - &:hover { - cursor: 'default'; - } - & svg { color: ${p => p.theme.colors.textLight2}; } } - - svg { - color: ${p => p.theme.colors.textLight}; - } `; const ItemDivider = styled.div` @@ -450,9 +453,13 @@ const Menu = styled.div` width: auto; box-shadow: ${p => p.theme.boxShadowSoft}; opacity: ${p => (p.isActive ? 1 : 0)}; + ${transition('opacity')}; @starting-style { opacity: 0; } - ${transition('opacity')}; + + @media (prefers-contrast: more) { + border: solid 1px ${p => p.theme.colors.bg2}; + } `; diff --git a/browser/data-browser/src/components/OutlinedSection.tsx b/browser/data-browser/src/components/OutlinedSection.tsx index cd2adc8c8..2ab5b4a01 100644 --- a/browser/data-browser/src/components/OutlinedSection.tsx +++ b/browser/data-browser/src/components/OutlinedSection.tsx @@ -1,6 +1,7 @@ import { PropsWithChildren } from 'react'; import { Row } from './Row'; import { styled } from 'styled-components'; +import { CurrentBackgroundColor } from '../globalCssVars'; interface OutlinedSectionProps { title: string; @@ -39,7 +40,7 @@ const Heading = styled.h2` color: ${p => p.theme.colors.textLight}; font-weight: normal; margin: 0; - background-color: ${p => p.theme.colors.bgBody}; + background-color: ${CurrentBackgroundColor.var()}; position: absolute; top: -0.5rem; left: ${p => p.theme.size()}; diff --git a/browser/data-browser/src/components/ResourceContextMenu/index.tsx b/browser/data-browser/src/components/ResourceContextMenu/index.tsx index 2900db053..720f85626 100644 --- a/browser/data-browser/src/components/ResourceContextMenu/index.tsx +++ b/browser/data-browser/src/components/ResourceContextMenu/index.tsx @@ -25,6 +25,7 @@ import { FaShare, FaTrash, FaPlus, + FaArrowUpRightFromSquare, } from 'react-icons/fa6'; import { useQueryScopeHandler } from '../../hooks/useQueryScope'; import { @@ -50,6 +51,7 @@ export enum ContextMenuOptions { UseInCode = 'useInCode', NewChild = 'newChild', Export = 'export', + Open = 'open', } export interface ResourceContextMenuProps { @@ -64,6 +66,7 @@ export interface ResourceContextMenuProps { /** Callback that is called after the resource was deleted */ onAfterDelete?: () => void; title?: string; + external?: boolean; } /** Dropdown menu that opens a bunch of actions for some resource */ @@ -74,6 +77,7 @@ function ResourceContextMenu({ simple, isMainMenu, title, + external, bindActive, onAfterDelete, }: ResourceContextMenuProps) { @@ -132,10 +136,16 @@ function ResourceContextMenu({ }, DIVIDER, ), + ...addIf(!!external, { + id: ContextMenuOptions.Open, + label: 'Open', + helper: 'Open the resource', + onClick: () => navigate(constructOpenURL(subject)), + icon: , + }), ...addIf( canWrite, { - // disabled: !canWrite || location.pathname.startsWith(paths.edit), id: ContextMenuOptions.Edit, label: 'Edit', helper: 'Open the edit form.', diff --git a/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx b/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx index 52a5f82f0..794845030 100644 --- a/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx @@ -1,13 +1,15 @@ import { + core, dataBrowser, - urls, useResource, useString, type Resource, } from '@tomic/react'; import React, { useEffect, useRef } from 'react'; import { styled, css } from 'styled-components'; -import { getIconForClass } from '../../../helpers/iconMap'; +import { dataTypeIconMap, getIconForClass } from '../../../helpers/iconMap'; +import { FaAtom } from 'react-icons/fa6'; +import { Row } from '../../Row'; interface ResultLineProps { selected: boolean; @@ -26,19 +28,25 @@ export function ResultLine({ onClick, }: React.PropsWithChildren): JSX.Element { const ref = useRef(null); + // We need to track mouse hover state but we don't want to re-render the component on every mouse move. + const hasMouseHover = useRef(false); useEffect(() => { - if (selected) { + if (selected && !hasMouseHover.current) { ref.current?.scrollIntoView({ block: 'nearest' }); } - }, [selected]); + }, [selected, hasMouseHover]); return ( onMouseOver()} + onMouseMove={() => { + hasMouseHover.current = true; + onMouseOver(); + }} + onMouseLeave={() => (hasMouseHover.current = false)} onClick={onClick} > {children} @@ -51,13 +59,20 @@ export function ResourceResultLine({ ...props }: ResourceResultLineProps): JSX.Element { const resource = useResource(subject); - const [description] = useString(resource, urls.properties.description); + const [description] = useString(resource, core.properties.description); return ( - - {resource.title} - {description && - {description.slice(0, 70)}} + + + {resource.title} + + {description && ( + + {description.slice(0, 70).trim()} + {description.length > 70 ? '...' : ''} + + )} ); } @@ -67,12 +82,15 @@ type IconProps = { }; function Icon({ resource }: IconProps): React.ReactElement { - const IconComp = getIconForClass(resource.getClasses()[0] ?? ''); + let IconComp = getIconForClass(resource.getClasses()[0] ?? ''); if (resource.hasClasses(dataBrowser.classes.tag)) { const emoji = resource.get(dataBrowser.properties.emoji); return emoji ? {emoji} : ; + } else if (resource.hasClasses(core.classes.property)) { + IconComp = + dataTypeIconMap.get(resource.get(core.properties.datatype)) ?? FaAtom; } return ; @@ -81,36 +99,63 @@ function Icon({ resource }: IconProps): React.ReactElement { const Description = styled.span` white-space: nowrap; color: ${({ theme }) => theme.colors.textLight}; + grid-column: 2/2; + overflow: hidden; + text-overflow: ellipsis; `; -export const ListItem = styled.li<{ selected: boolean }>` +export const ListItem = styled.li<{ selected: boolean; gridColumn?: string }>` + --list-item-bg: none; + --list-item-color: currentColor; + --list-item-svg-color: ${({ theme }) => theme.colors.textLight}; + + background-color: var(--list-item-bg); + color: var(--list-item-color); + grid-column: 1/3; padding: 0.5rem; list-style: none; margin: 0; - padding-left: ${({ theme }) => theme.margin}rem; - border-bottom: 1px solid ${({ theme }) => theme.colors.bg2}; - min-width: 100%; - width: 100%; - text-overflow: ellipsis; + padding-left: ${({ theme }) => theme.size()}; + width: 100cqw; white-space: nowrap; - overflow: hidden; - display: flex; - align-items: center; - gap: 0.7ch; + display: grid; + grid-template-columns: subgrid; + grid-template-rows: 1fr; cursor: pointer; + &:has(+ div) { + padding-bottom: 1rem; + } - ${p => - p.selected && - css` - box-shadow: inset 0 0 0px 1px ${({ theme }) => theme.colors.main}; - color: ${({ theme }) => theme.colors.main}; - `} + div + & { + padding-top: 1rem; + } svg { - color: ${({ selected, theme }) => - selected ? theme.colors.main : theme.colors.textLight}; + color: var(--list-item-svg-color); min-width: 1rem; height: 1rem; } + + ${({ selected, theme }) => + selected && + css` + --list-item-bg: ${theme.colors.mainSelectedBg}; + --list-item-color: ${theme.colors.mainSelectedFg}; + --list-item-svg-color: var(--list-item-color); + + @media (prefers-contrast: more) { + --list-item-bg: ${theme.darkMode ? 'white' : 'black'}; + --list-item-color: ${theme.darkMode ? 'black' : 'white'}; + } + `} + + @container (max-width: 520px) { + grid-column: 1/2; + } +`; + +const Name = styled.span` + overflow: hidden; + text-overflow: ellipsis; `; diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx index 7bc58a09e..cddd33106 100644 --- a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx @@ -279,14 +279,15 @@ const TriggerButtonWrapper = styled.div<{ background-color: ${p => SB_BACKGROUND.var(p.theme.colors.bg)}; - &:disabled { + &:has(:disabled) { background-color: ${props => props.theme.colors.bg1}; + opacity: 0.7; } &:has(${TriggerButton}:hover(), ${TriggerButton}:focus-visible) { } - &:hover, - &:focus-visible { + + &:not(:has(:disabled)):where(:hover, :focus-visible) { border-color: transparent; box-shadow: 0 0 0 2px ${SB_HIGHLIGHT.var()}; ${p => diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx index f06c900e2..1edf22dd0 100644 --- a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx +++ b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx @@ -1,4 +1,4 @@ -import { core, useResources, useServerSearch } from '@tomic/react'; +import { core, dataBrowser, useResources, useServerSearch } from '@tomic/react'; import { ClipboardEventHandler, KeyboardEventHandler, @@ -23,6 +23,31 @@ import { useSettings } from '../../../helpers/AppSettings'; import { QuickScore } from 'quick-score'; import { useTitlePropOfClass } from '../ResourceSelector/useTitlePropOfClass'; import { stringToSlug } from '../../../helpers/stringToSlug'; +import { addIf } from '../../../helpers/addIf'; +import { Row } from '../../Row'; + +/** + * Options shown at the top of the results when the `isA` prop matches a key in this object. + */ +const STANDARD_OPTIONS: Record = { + [core.classes.property]: [ + core.properties.name, + core.properties.description, + core.properties.shortname, + dataBrowser.properties.image, + ], +}; + +enum OptionType { + CreateOption, + StandardOption, + Result, +} + +type Option = { + type: OptionType; + data: string; +}; const BOX_HEIGHT_REM = 20; @@ -39,6 +64,10 @@ interface SearchBoxWindowProps { onCreateItem?: (name: string, isA?: string) => void; } +/** + * The window that opens when the searchbox is focussed. + * It handles searching, both locally and on the server. + */ export function SearchBoxWindow({ searchValue, onChange, @@ -53,7 +82,7 @@ export function SearchBoxWindow({ }: SearchBoxWindowProps): JSX.Element { const { drive } = useSettings(); - const [realIndex, setIndex] = useState(undefined); + const [index, setIndex] = useState(undefined); const [results, setResults] = useState([]); const [searchError, setSearchError] = useState(); const [valueIsURL, setValueIsURL] = useState(false); @@ -67,12 +96,31 @@ export function SearchBoxWindow({ const showCreateOption = onCreateItem && searchValue && !valueIsURL && !allowsOnly; - const offset = showCreateOption ? 1 : 0; + const standardOptions = useMemo(() => { + if (!searchValue && isA && isA in STANDARD_OPTIONS) { + return STANDARD_OPTIONS[isA]; + } + + return []; + }, [isA, searchValue]); + + const options: Option[] = useMemo( + () => [ + ...addIf(!!showCreateOption, { + type: OptionType.CreateOption, + data: '', + }), + ...standardOptions.map(option => ({ + type: OptionType.StandardOption, + data: option, + })), + ...results.map(result => ({ type: OptionType.Result, data: result })), + ], + [showCreateOption, standardOptions, results], + ); const selectedIndex = - realIndex !== undefined - ? loopingIndex(realIndex, results.length + offset) - : undefined; + index !== undefined ? loopingIndex(index, options.length) : undefined; const handleKeyDown: KeyboardEventHandler = e => { if (e.key === 'Enter') { @@ -139,20 +187,24 @@ export function SearchBoxWindow({ onCreateItem(name, isA); }; - const pickSelectedItem = () => { - if (selectedIndex === undefined) { + const pickSelectedItem = (override?: number) => { + if (selectedIndex === undefined && override === undefined) { onSelect(searchValue); return; } - if (selectedIndex === 0 && showCreateOption) { + const selected = options[override! ?? selectedIndex!]; + + // The selected option is a "Create ..." option. + if (selected.type === OptionType.CreateOption) { createItem(searchValue); return; } - onSelect(results[selectedIndex - offset]); + // The selected option is a standard option or a search result. + onSelect(selected.data); }; const handleResults = useCallback((res: string[], error?: Error) => { @@ -206,7 +258,12 @@ export function SearchBoxWindow({ } return ( - + setIndex(undefined)} + > Start Searching )} -
    - {showCreateOption ? ( - handleMouseMove(0)} - onClick={() => createItem(searchValue)} - > - {titleProp ? ( - <> - Create{' '} - {searchValue} - - ) : ( - `Create new ${classTitle ?? 'resource'}` - )} - - ) : null} - {results.map((result, i) => ( - handleMouseMove(i + offset)} - onClick={pickSelectedItem} - /> - ))} -
+ + {options.map((option, i) => { + let line = <>; + + if (option.type === OptionType.CreateOption) { + line = ( + handleMouseMove(0)} + onClick={() => createItem(searchValue)} + > + {titleProp ? ( + + Create{' '} + {searchValue} + + ) : ( + `Create new ${classTitle ?? 'resource'}` + )} + + ); + } else { + line = ( + handleMouseMove(i)} + onClick={() => { + // On mobile the item is not selected by hover so we need to force pickSelectedItem to pick this one. + pickSelectedItem(i); + }} + /> + ); + } + + const showDivider = + options[i + 1] !== undefined && + option.type !== options[i + 1].type; + + return ( + <> + {line} + {showDivider && } + + ); + })} + {!!searchValue && results.length === 0 && ( No Results )} @@ -301,7 +380,11 @@ const ServerSearchUnit = ({ filters: { ...(isA ? { [core.properties.isA]: isA } : {}), }, - parents: scopes ?? [drive, 'https://atomicdata.dev'], + parents: scopes ?? [ + drive, + // We don't want to show atomicdata.dev results when there are standard defined options. + ...addIf(!!isA && !(isA in STANDARD_OPTIONS), 'https://atomicdata.dev'), + ], // If a classtype is given we want to prefill the searchbox with data. allowEmptyQuery: !!isA, }), @@ -384,13 +467,26 @@ const Input = styled.input` `; const ResultBox = styled.div` + container: searchbox / inline-size; flex: 1; border: solid 1px ${p => p.theme.colors.bg2}; - height: calc(100% - 2rem); overflow: hidden; `; +const List = styled.ul` + display: grid; + grid-template-columns: 20ch auto; + column-gap: 1ch; + overflow: hidden; + width: calc(100cqw); + margin-bottom: 0; + + @container (max-width: 520px) { + grid-template-columns: 1fr; + } +`; + const Wrapper = styled.div<{ $above: boolean }>` display: flex; @@ -401,6 +497,9 @@ const Wrapper = styled.div<{ $above: boolean }>` height: ${BOX_HEIGHT_REM}rem; position: absolute; width: var(--radix-popover-trigger-width); + left: 0; + animation: ${fadeIn} 0.2s ease-in-out; + ${({ $above, theme }) => $above ? css` @@ -433,9 +532,6 @@ const Wrapper = styled.div<{ $above: boolean }>` border-bottom-right-radius: ${p => p.theme.radius}; } `} - left: 0; - - animation: ${fadeIn} 0.2s ease-in-out; `; const CenteredMessage = styled.div` display: grid; @@ -454,3 +550,13 @@ const CreateLineInputText = styled.span` color: ${p => p.theme.colors.textLight}; font-style: italic; `; + +const Divider = styled.div` + border-top: 1px solid ${props => props.theme.colors.bg2}; + /* margin-block: 0.5rem; */ + grid-column: 1/3; + + @container (max-width: 520px) { + grid-column: 1/2; + } +`; diff --git a/browser/data-browser/src/globalCssVars.ts b/browser/data-browser/src/globalCssVars.ts new file mode 100644 index 000000000..fff29c1ed --- /dev/null +++ b/browser/data-browser/src/globalCssVars.ts @@ -0,0 +1,3 @@ +import { CSSVar } from './helpers/CSSVar'; + +export const CurrentBackgroundColor = new CSSVar('current-background-color'); diff --git a/browser/data-browser/src/views/OntologyPage/toAnchorId.ts b/browser/data-browser/src/helpers/toAnchorId.ts similarity index 100% rename from browser/data-browser/src/views/OntologyPage/toAnchorId.ts rename to browser/data-browser/src/helpers/toAnchorId.ts diff --git a/browser/data-browser/src/styling.tsx b/browser/data-browser/src/styling.tsx index 6b1e01ce6..94f9e0f72 100644 --- a/browser/data-browser/src/styling.tsx +++ b/browser/data-browser/src/styling.tsx @@ -3,10 +3,11 @@ import { DefaultTheme, ThemeProvider, } from 'styled-components'; -import { darken, lighten } from 'polished'; +import { darken, lighten, setLightness } from 'polished'; import './reset.css'; import { useContext } from 'react'; import { SettingsContext } from './helpers/AppSettings'; +import { CurrentBackgroundColor } from './globalCssVars'; interface ThemeWrapperProps { children: React.ReactNode; @@ -118,6 +119,8 @@ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => { bg: bg, // Use pitch black for dark mode bgBody: darkMode ? bg : darken(0.02)(bg), + mainSelectedBg: setLightness(darkMode ? 0.05 : 0.97, main), + mainSelectedFg: setLightness(darkMode ? 0.7 : 0.25, main), bg1: darkMode ? lighten(0.1)(bg) : darken(0.05)(bg), bg2: darkMode ? lighten(0.3)(bg) : darken(0.2)(bg), text, @@ -198,6 +201,10 @@ declare module 'styled-components' { mainLight: string; /** Slightly darker version of Main accent color */ mainDark: string; + /** Background color of selected items */ + mainSelectedBg: string; + /** Foreground color of selected items */ + mainSelectedFg: string; /** The background color of the body, which is subtly different from bg */ bgBody: string; /** Most common background color */ @@ -257,7 +264,8 @@ export const GlobalStyle = createGlobalStyle` } body { - background-color: ${props => props.theme.colors.bgBody}; + ${CurrentBackgroundColor.define(p => p.theme.colors.bgBody)} + background-color: ${CurrentBackgroundColor.var()}; color: ${props => props.theme.colors.text}; font-family: ${props => props.theme.fontFamily}; line-height: 1.5em; diff --git a/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts b/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts index e30f39072..94e2bbd3b 100644 --- a/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts +++ b/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts @@ -136,10 +136,10 @@ ${propUsage} return [ ` -
{$resource.title}
`, +
{resource.title}
`, ]; } @@ -152,21 +152,17 @@ ${propUsage} name: 'getResource', file: '@tomic/svelte', }, - { - name: 'getValue', - file: '@tomic/svelte', - }, propImport, ); return [ ` -
{$value}
`, +
{value}
`, ]; } @@ -179,10 +175,10 @@ ${imports} return [ ` -
{$resource.title}
`, +
{resource.title}
`, ]; } @@ -200,24 +196,20 @@ ${imports} name: 'getResource', file: '@tomic/svelte', }, - { - name: 'getValue', - file: '@tomic/svelte', - }, resourceShorthand ? undefined : propImport, ); const hookPart = resourceShorthand ? '' - : `\n let value = getValue(resource, ${propSubjectRef});`; + : `\n let value = $derived(resource.get(${propSubjectRef}));`; return [ ` -
{${resourceShorthand ?? '$value'}}
`, +
{${resourceShorthand ?? 'value'}}
`, ]; } } diff --git a/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts b/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts index d013493e2..58319ac1a 100644 --- a/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts +++ b/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts @@ -99,7 +99,7 @@ for await (const rowSubject of table) { const valueLine = resourceShorthand ? ` console.log(\`\${row.title}: \${${resourceShorthand}}\`);` : ` const value = row.get(${propSubjectRef}); - console.log(value);`; + console.log(\`\${row.title}: \${value}\`);`; return [ `${imports} @@ -113,7 +113,7 @@ const table = new CollectionBuilder(store) // Iterate over the collection, fetch the children and log a value // Check the docs on how to use collection for other use cases like pagenation for await (const rowSubject of table) { - const row = await store.getResource${genericName}(rowSubject); + const row = await store.getResource${genericName ?? ''}(rowSubject); ${valueLine} }`, ]; @@ -298,7 +298,7 @@ const Component = () => { }; const Row = ({ subject }: { subject: string }) => { - const row = useResource${genericName}(subject); + const row = useResource${genericName ?? ''}(subject); ${propUsage} };`, ]; @@ -310,20 +310,24 @@ ${propUsage} `// Component.svelte @@ -334,19 +338,19 @@ ${propUsage} {/each} - -`, + +`, // Item code `// Item.svelte -{$resource.title}`, +{resource.title}`, ]; } @@ -359,10 +363,6 @@ ${propUsage} name: 'getResource', file: '@tomic/svelte', }, - { - name: 'getValue', - file: '@tomic/svelte', - }, propImport, ); @@ -371,20 +371,24 @@ ${propUsage} `// Component.svelte @@ -395,19 +399,19 @@ ${propUsage} {/each} - -`, + +`, // Item code `// Item.svelte -{$resource.title}: {$value}`, +{resource.title}: {value}`, ]; } @@ -417,20 +421,24 @@ ${imports} `// Component.svelte @@ -441,19 +449,23 @@ ${imports} {/each} - -`, + +`, // Item code `// Item.svelte -{$resource.title}`, +{resource.title}`, ]; } @@ -471,36 +483,36 @@ ${imports} name: 'getResource', file: '@tomic/svelte', }, - { - name: 'getValue', - file: '@tomic/svelte', - }, resourceShorthand ? undefined : propImport, ); const hookPart = resourceShorthand ? '' - : `\n let value = getValue(resource, ${propSubjectRef});`; + : `\n let value = $derived(resource.get(${propSubjectRef}));`; return [ // Component code `// Component.svelte @@ -511,18 +523,22 @@ ${imports} {/each} - -`, + +`, // Item code `// Item.svelte -{$resource.title}: {${resourceShorthand ?? '$value'}}`, +{resource.title}: {${resourceShorthand ?? 'value'}}`, ]; } } diff --git a/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx b/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx index b8d3e40e7..c21d50bdc 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx @@ -1,4 +1,4 @@ -import { Datatype, Resource, Store, core, useStore } from '@tomic/react'; +import { Resource, core, useStore } from '@tomic/react'; import { useRef, useState } from 'react'; import { styled } from 'styled-components'; import { transition } from '../../../helpers/transition'; @@ -6,6 +6,7 @@ import { FaPlus } from 'react-icons/fa'; import { SearchBox } from '../../../components/forms/SearchBox'; import { focusOffsetElement } from '../../../helpers/focusOffsetElement'; import { useOntologyContext } from '../OntologyContext'; +import { newProperty } from '../ontologyUtils'; interface AddPropertyButtonProps { creator: Resource; @@ -14,25 +15,6 @@ interface AddPropertyButtonProps { const BUTTON_WIDTH = 'calc(100% - 5.6rem + 4px)'; //Width is 100% - (2 * 1.8rem for button width) + (2rem for gaps) + (4px for borders) -async function newProperty(shortname: string, parent: Resource, store: Store) { - const subject = `${parent.subject}/property/${shortname}`; - - const resource = await store.newResource({ - subject, - parent: parent.subject, - isA: core.classes.property, - propVals: { - [core.properties.shortname]: shortname, - [core.properties.description]: 'a property', - [core.properties.datatype]: Datatype.STRING, - }, - }); - - await resource.save(); - - return subject; -} - export function AddPropertyButton({ creator, type, diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx index 6b7f6974a..263858ea7 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx @@ -7,7 +7,7 @@ import { FaCube } from 'react-icons/fa'; import { Column, Row } from '../../../components/Row'; import Markdown from '../../../components/datatypes/Markdown'; import { AtomicLink } from '../../../components/AtomicLink'; -import { toAnchorId } from '../toAnchorId'; +import { toAnchorId } from '../../../helpers/toAnchorId'; import { ViewTransitionProps } from '../../../helpers/ViewTransitionProps'; import { RESOURCE_PAGE_TRANSITION_TAG, diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx index b78769bcd..fb9fe7a5b 100644 --- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx +++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx @@ -10,7 +10,7 @@ import InputSwitcher from '../../../components/forms/InputSwitcher'; import ResourceContextMenu, { ContextMenuOptions, } from '../../../components/ResourceContextMenu'; -import { toAnchorId } from '../toAnchorId'; +import { toAnchorId } from '../../../helpers/toAnchorId'; import { AddPropertyButton } from './AddPropertyButton'; import { ErrorChipInput } from '../../../components/forms/ErrorChip'; import { useOntologyContext } from '../OntologyContext'; @@ -19,7 +19,11 @@ interface ClassCardWriteProps { subject: string; } -const contextOptions = [ContextMenuOptions.Delete, ContextMenuOptions.History]; +const contextOptions = [ + ContextMenuOptions.Open, + ContextMenuOptions.Delete, + ContextMenuOptions.History, +]; export function ClassCardWrite({ subject }: ClassCardWriteProps): JSX.Element { const resource = useResource(subject); @@ -61,6 +65,7 @@ export function ClassCardWrite({ subject }: ClassCardWriteProps): JSX.Element { /> (); + const [createdInstanceSubject, setCreatedInstanceSubject] = + useState(); + const [dialogProps, show, close, isOpen] = useDialog({ - onSuccess: () => { - setClassSubject(undefined); - ontology.save(); + onSuccess: async () => { + ontology.push(core.properties.instances, [createdInstanceSubject], true); + await ontology.save(); + + requestAnimationFrame(() => { + document + .querySelector(`[about="${createdInstanceSubject}"]`) + ?.scrollIntoView({ behavior: 'smooth' }); + + setCreatedInstanceSubject(undefined); + setClassSubject(undefined); + }); }, }); @@ -31,7 +43,7 @@ export function CreateInstanceButton({ ontology }: CreateInstanceButtonProps) { }; const handleSaveClick = (subject: string) => { - ontology.push(core.properties.instances, [subject], true); + setCreatedInstanceSubject(subject); close(true); }; diff --git a/browser/data-browser/src/views/OntologyPage/DashedButton.tsx b/browser/data-browser/src/views/OntologyPage/DashedButton.tsx new file mode 100644 index 000000000..f1272af25 --- /dev/null +++ b/browser/data-browser/src/views/OntologyPage/DashedButton.tsx @@ -0,0 +1,24 @@ +import { styled } from 'styled-components'; +import { transition } from '../../helpers/transition'; + +export const DashedButton = styled.button<{ buttonHeight?: string }>` + width: 100%; + height: ${p => p.buttonHeight ?? '20rem'}; + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + appearance: none; + background: none; + border: 2px dashed ${p => p.theme.colors.bg2}; + border-radius: ${p => p.theme.radius}; + color: ${p => p.theme.colors.textLight}; + cursor: pointer; + ${transition('background', 'color', 'border-color')} + &:hover, + &:focus-visible { + background: ${p => p.theme.colors.bg}; + border-color: ${p => p.theme.colors.main}; + color: ${p => p.theme.colors.main}; + } +`; diff --git a/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx b/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx new file mode 100644 index 000000000..3f1561b2c --- /dev/null +++ b/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx @@ -0,0 +1,41 @@ +import { FaInfo } from 'react-icons/fa6'; +import { + IconButton, + IconButtonVariant, +} from '../../components/IconButton/IconButton'; +import { useState } from 'react'; +import { Collapse } from '../../components/Collapse'; +import { Row } from '../../components/Row'; +import Markdown from '../../components/datatypes/Markdown'; + +interface InfoTitleProps { + info: string; +} + +export function InfoTitle({ + info, + children, +}: React.PropsWithChildren) { + const [collapsed, setCollapsed] = useState(true); + + return ( +
+ +

{children}

+ setCollapsed(prev => !prev)} + title='Show helper' + > + + +
+ + + +
+ ); +} diff --git a/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx b/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx index 49ecb332d..7c4ea60b8 100644 --- a/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx +++ b/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx @@ -7,7 +7,7 @@ import { useResource, } from '@tomic/react'; import { ResourceInline } from '../ResourceInline'; -import { toAnchorId } from './toAnchorId'; +import { toAnchorId } from '../../helpers/toAnchorId'; import { useOntologyContext } from './OntologyContext'; interface TypeSuffixProps { diff --git a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx index 52b16ecc9..9df6e8242 100644 --- a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx +++ b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx @@ -2,7 +2,6 @@ import { Datatype, Resource, useStore, validateDatatype } from '@tomic/react'; import { useRef, useState } from 'react'; import { FaPlus } from 'react-icons/fa'; import { styled } from 'styled-components'; -import { transition } from '../../helpers/transition'; import { Dialog, DialogActions, @@ -14,8 +13,9 @@ import { Button } from '../../components/Button'; import { InputStyled, InputWrapper } from '../../components/forms/InputStyles'; import { stringToSlug } from '../../helpers/stringToSlug'; import { Column } from '../../components/Row'; -import { newClass, subjectForClass } from './newClass'; -import { toAnchorId } from './toAnchorId'; +import { newClass, subjectForClass } from './ontologyUtils'; +import { toAnchorId } from '../../helpers/toAnchorId'; +import { DashedButton } from './DashedButton'; interface NewClassButtonProps { resource: Resource; @@ -28,6 +28,7 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element { const inputRef = useRef(null); const subject = subjectForClass(resource, inputValue); + const shortSubject = new URL(subject).pathname; const [dialogProps, show, hide, isOpen] = useDialog({ onSuccess: async () => { @@ -101,7 +102,7 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element { /> - {subject} + {shortSubject} @@ -119,28 +120,6 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element { ); } -const DashedButton = styled.button` - width: 100%; - height: 20rem; - display: flex; - align-items: center; - justify-content: center; - gap: 1ch; - appearance: none; - background: none; - border: 2px dashed ${p => p.theme.colors.bg2}; - border-radius: ${p => p.theme.radius}; - color: ${p => p.theme.colors.textLight}; - cursor: pointer; - &:hover, - &:focus-visible { - background: ${p => p.theme.colors.bg}; - border-color: ${p => p.theme.colors.main}; - color: ${p => p.theme.colors.main}; - } - ${transition('background', 'color', 'border-color')} -`; - const SubjectWrapper = styled.div` width: 100%; max-width: 60ch; diff --git a/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx b/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx new file mode 100644 index 000000000..d81f749bb --- /dev/null +++ b/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx @@ -0,0 +1,128 @@ +import { + Datatype, + Resource, + useStore, + validateDatatype, + type Core, +} from '@tomic/react'; +import { useRef, useState } from 'react'; +import { FaPlus } from 'react-icons/fa'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + useDialog, +} from '../../components/Dialog'; +import { Button } from '../../components/Button'; +import { InputStyled, InputWrapper } from '../../components/forms/InputStyles'; +import { stringToSlug } from '../../helpers/stringToSlug'; +import { Column } from '../../components/Row'; +import { newProperty } from './ontologyUtils'; +import { toAnchorId } from '../../helpers/toAnchorId'; +import { DashedButton } from './DashedButton'; +import { useOntologyContext } from './OntologyContext'; + +interface NewPropertyButtonProps { + parent: Resource; +} + +export function NewPropertyButton({ + parent, +}: NewPropertyButtonProps): JSX.Element { + const store = useStore(); + const [inputValue, setInputValue] = useState(''); + const [isValid, setIsValid] = useState(false); + const inputRef = useRef(null); + const { addProperty } = useOntologyContext(); + + const [dialogProps, show, hide, isOpen] = useDialog({ + onSuccess: async () => { + const createdProperty = await newProperty(inputValue, parent, store); + await addProperty(createdProperty); + requestAnimationFrame(() => { + const id = toAnchorId(createdProperty); + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }); + }); + }, + }); + + const handleShortNameChange = (e: React.ChangeEvent) => { + const slugValue = stringToSlug(e.target.value); + setInputValue(slugValue); + validate(slugValue); + }; + + const validate = (value: string) => { + if (!value) { + setIsValid(false); + + return; + } + + try { + validateDatatype(value, Datatype.SLUG); + setIsValid(true); + } catch (e) { + setIsValid(false); + } + }; + + const openAndReset = () => { + setInputValue(''); + setIsValid(false); + show(); + + requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + hide(false); + } + + if (e.key === 'Enter' && isValid) { + hide(true); + } + }; + + return ( + <> + + Add property + + + {isOpen && ( + <> + +

New Property

+
+ + + + + + + + + + + + + )} +
+ + ); +} diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 9148e5602..ca22bed8a 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -1,5 +1,11 @@ import { ResourcePageProps } from '../ResourcePage'; -import { core, useArray, useCanWrite } from '@tomic/react'; +import { + core, + useArray, + useCanWrite, + useResource, + type Core, +} from '@tomic/react'; import { OntologySidebar } from './OntologySidebar'; import { styled } from 'styled-components'; import { ClassCardRead } from './Class/ClassCardRead'; @@ -11,16 +17,29 @@ import { FaEdit, FaEye } from 'react-icons/fa'; import { OntologyDescription } from './OntologyDescription'; import { ClassCardWrite } from './Class/ClassCardWrite'; import { NewClassButton } from './NewClassButton'; -import { toAnchorId } from './toAnchorId'; +import { toAnchorId } from '../../helpers/toAnchorId'; import { OntologyContextProvider } from './OntologyContext'; import { PropertyCardWrite } from './Property/PropertyCardWrite'; import { Graph } from './Graph'; import { CreateInstanceButton } from './CreateInstanceButton'; import { useState } from 'react'; +import { NewPropertyButton } from './NewPropertyButton'; +import { InfoTitle } from './InfoTitle'; const isEmpty = (arr: Array) => arr.length === 0; export function OntologyPage({ resource }: ResourcePageProps) { + const classesPropResource = useResource( + core.properties.classes, + ); + const propertiesPropResource = useResource( + core.properties.properties, + ); + + const instancesPropResource = useResource( + core.properties.instances, + ); + const [classes] = useArray(resource, core.properties.classes); const [properties] = useArray(resource, core.properties.properties); const [instances] = useArray(resource, core.properties.instances); @@ -56,7 +75,9 @@ export function OntologyPage({ resource }: ResourcePageProps) { -

Classes

+ + Classes + {editMode && (
  • @@ -72,9 +93,17 @@ export function OntologyPage({ resource }: ResourcePageProps) { )}
  • ))} + {!editMode && classes.length === 0 && No classes}
    -

    Properties

    + + Properties + + {editMode && ( +
  • + +
  • + )} {properties.map(c => (
  • {editMode ? ( @@ -84,15 +113,21 @@ export function OntologyPage({ resource }: ResourcePageProps) { )}
  • ))} + {!editMode && properties.length === 0 && ( + No properties + )}
    -

    Instances

    + + Instances + + {editMode && } {instances.map(c => (
  • ))} - {editMode && } + {!editMode && instances.length === 0 && No instances}
    diff --git a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx index 172c7fcd7..a3c900362 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx @@ -4,7 +4,7 @@ import { styled } from 'styled-components'; import { Details } from '../../components/Details'; import { FaAtom, FaCube, FaHashtag } from 'react-icons/fa'; import { ScrollArea } from '../../components/ScrollArea'; -import { toAnchorId } from './toAnchorId'; +import { toAnchorId } from '../../helpers/toAnchorId'; interface OntologySidebarProps { ontology: Resource; diff --git a/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx b/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx index c4c1b1d44..7484a0c79 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx @@ -89,7 +89,7 @@ const TagPanel: FC = ({ resource, ontology }) => { return ( -

    Only allow its value to be one of the following tags:

    +

    Only allow its value to be selected from the following tags:

    {tags.map(tag => ( diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx index 7dc55230b..532331a43 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx @@ -7,7 +7,7 @@ import { Column, Row } from '../../../components/Row'; import { InlineFormattedResourceList } from '../../../components/InlineFormattedResourceList'; import { InlineDatatype } from '../InlineDatatype'; import { AtomicLink } from '../../../components/AtomicLink'; -import { toAnchorId } from '../toAnchorId'; +import { toAnchorId } from '../../../helpers/toAnchorId'; interface PropertyCardReadProps { subject: string; diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx index 78b550cbe..e017f6ec7 100644 --- a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx +++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx @@ -4,7 +4,7 @@ import { urls, useCanWrite, useProperty, useResource } from '@tomic/react'; import { FaHashtag } from 'react-icons/fa'; import { styled } from 'styled-components'; import { Column, Row } from '../../../components/Row'; -import { toAnchorId } from '../toAnchorId'; +import { toAnchorId } from '../../../helpers/toAnchorId'; import InputSwitcher from '../../../components/forms/InputSwitcher'; import ResourceContextMenu, { ContextMenuOptions, @@ -16,7 +16,11 @@ interface PropertyCardWriteProps { subject: string; } -const contextOptions = [ContextMenuOptions.Delete, ContextMenuOptions.History]; +const contextOptions = [ + ContextMenuOptions.Open, + ContextMenuOptions.Delete, + ContextMenuOptions.History, +]; export function PropertyCardWrite({ subject, @@ -44,6 +48,7 @@ export function PropertyCardWrite({ /> - This property does not exist anymore ({subject}) + This property does not exist any more ({subject})