diff --git a/Earthfile b/Earthfile index 3af1a874c..0d21e9372 100644 --- a/Earthfile +++ b/Earthfile @@ -2,7 +2,7 @@ VERSION --try 0.8 PROJECT ontola/atomic-server IMPORT ./browser AS browser IMPORT github.com/earthly/lib/rust AS rust -FROM rust:buster +FROM rust:bookworm WORKDIR /code pipeline: @@ -33,6 +33,8 @@ docker-all: install: RUN apt-get update -qq + # NASM is required for the image library + RUN apt install nasm RUN rustup component add clippy RUN rustup component add rustfmt RUN cargo install cross diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 115d58560..9d6304e81 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -21,6 +21,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#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. - [#914](https://github.com/atomicdata-dev/atomic-server/issues/914) Fix an issue where changing the subject in a new resource form could update the parent of existing resources if their subject matched the new subject. +- [#919](https://github.com/atomicdata-dev/atomic-server/issues/919) Automatically sort classes and properties in the ontology editor. ### @tomic/lib diff --git a/browser/data-browser/src/components/Main.tsx b/browser/data-browser/src/components/Main.tsx index 0cfc31a36..462f9d05f 100644 --- a/browser/data-browser/src/components/Main.tsx +++ b/browser/data-browser/src/components/Main.tsx @@ -38,4 +38,11 @@ const StyledMain = memo(styled.main` ${p => transitionName(RESOURCE_PAGE_TRANSITION_TAG, p.subject)}; height: calc(100vh - ${p => p.theme.heights.breadCrumbBar}); overflow-y: auto; + scroll-padding: calc( + ${p => p.theme.heights.breadCrumbBar} + ${p => p.theme.size(2)} + ); + + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } `); diff --git a/browser/data-browser/src/hooks/useAddToOntology.ts b/browser/data-browser/src/hooks/useAddToOntology.ts index 2a05406a9..8dc747a16 100644 --- a/browser/data-browser/src/hooks/useAddToOntology.ts +++ b/browser/data-browser/src/hooks/useAddToOntology.ts @@ -5,16 +5,18 @@ import { Server, unknownSubject, core, + Core, } from '@tomic/react'; import { useSettings } from '../helpers/AppSettings'; import { useCallback } from 'react'; +import { sortSubjectList } from '../views/OntologyPage/sortSubjectList'; export function useAddToOntology(ontologySubject?: string) { const store = useStore(); const { drive: driveSubject } = useSettings(); const drive = useResource(driveSubject); - const ontology = useResource( + const ontology = useResource( ontologySubject ?? drive.props.defaultOntology ?? unknownSubject, ); @@ -31,9 +33,21 @@ export function useAddToOntology(ontologySubject?: string) { await resource.save(); if (resource.hasClasses(core.classes.class)) { - ontology.push(core.properties.classes, [resource.subject], true); + await ontology.set( + core.properties.classes, + await sortSubjectList(store, [ + ...(ontology.props.classes ?? []), + resource.subject, + ]), + ); } else if (resource.hasClasses(core.classes.property)) { - ontology.push(core.properties.properties, [resource.subject], true); + await ontology.set( + core.properties.properties, + await sortSubjectList(store, [ + ...(ontology.props.properties ?? []), + resource.subject, + ]), + ); } else { ontology.push(core.properties.instances, [resource.subject], true); } diff --git a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx index feb0a8da9..52b16ecc9 100644 --- a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx +++ b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx @@ -15,6 +15,7 @@ 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'; interface NewClassButtonProps { resource: Resource; @@ -29,8 +30,10 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element { const subject = subjectForClass(resource, inputValue); const [dialogProps, show, hide, isOpen] = useDialog({ - onSuccess: () => { - newClass(inputValue, resource, store); + onSuccess: async () => { + const createdClass = await newClass(inputValue, resource, store); + const id = toAnchorId(createdClass); + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }); }, }); diff --git a/browser/data-browser/src/views/OntologyPage/OntologyContext.tsx b/browser/data-browser/src/views/OntologyPage/OntologyContext.tsx index f800f2c8d..e9208aa20 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyContext.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyContext.tsx @@ -1,5 +1,13 @@ -import { Resource, unknownSubject, urls, useArray } from '@tomic/react'; +import { + core, + Resource, + unknownSubject, + urls, + useArray, + useStore, +} from '@tomic/react'; import { createContext, useCallback, useContext, useMemo } from 'react'; +import { sortSubjectList } from './sortSubjectList'; interface OntologyContext { addClass: (subject: string) => Promise; @@ -29,7 +37,8 @@ export function OntologyContextProvider({ ontology, children, }: React.PropsWithChildren) { - const [classes, setClasses] = useArray(ontology, urls.properties.classes, { + const store = useStore(); + const [classes, setClasses] = useArray(ontology, core.properties.classes, { commit: true, }); @@ -55,7 +64,9 @@ export function OntologyContextProvider({ const addProperty = useCallback( async (subject: string) => { - await setProperties([...properties, subject]); + await setProperties( + await sortSubjectList(store, [...properties, subject]), + ); }, [properties, setProperties], ); diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 062293236..5e0c38f1a 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -58,6 +58,11 @@ export function OntologyPage({ resource }: ResourcePageProps) {

Classes

+ {editMode && ( +
  • + +
  • + )} {classes.map(c => (
  • {editMode ? ( @@ -67,11 +72,6 @@ export function OntologyPage({ resource }: ResourcePageProps) { )}
  • ))} - {editMode && ( -
  • - -
  • - )}

    Properties

    @@ -118,7 +118,6 @@ const ListSlot = styled.div` const FullPageWrapper = styled.div<{ edit: boolean }>` --ontology-graph-position: sticky; --ontology-graph-ratio: 9 / 16; - display: grid; grid-template-areas: ${p => p.edit diff --git a/browser/data-browser/src/views/OntologyPage/newClass.ts b/browser/data-browser/src/views/OntologyPage/newClass.ts index a9a772ff4..dcf388cfe 100644 --- a/browser/data-browser/src/views/OntologyPage/newClass.ts +++ b/browser/data-browser/src/views/OntologyPage/newClass.ts @@ -1,4 +1,5 @@ -import { Resource, Store, core } from '@tomic/react'; +import { Resource, Store, core, type Core } from '@tomic/react'; +import { sortSubjectList } from './sortSubjectList'; const DEFAULT_DESCRIPTION = 'Change me'; @@ -7,7 +8,7 @@ export const subjectForClass = (parent: Resource, shortName: string): string => export async function newClass( shortName: string, - parent: Resource, + parent: Resource, store: Store, ): Promise { const subject = subjectForClass(parent, shortName); @@ -24,7 +25,12 @@ export async function newClass( await resource.save(); - parent.push(core.properties.classes, [subject]); + const classes = parent.props.classes ?? []; + + await parent.set( + core.properties.classes, + await sortSubjectList(store, [...classes, subject]), + ); await parent.save(); diff --git a/browser/data-browser/src/views/OntologyPage/sortSubjectList.ts b/browser/data-browser/src/views/OntologyPage/sortSubjectList.ts new file mode 100644 index 000000000..2bb88a5e9 --- /dev/null +++ b/browser/data-browser/src/views/OntologyPage/sortSubjectList.ts @@ -0,0 +1,16 @@ +import { type Resource, type Store } from '@tomic/react'; + +export async function sortSubjectList( + store: Store, + subjectList: string[], +): Promise { + const resources: Resource[] = []; + + for (const subject of subjectList) { + resources.push(await store.getResource(subject)); + } + + resources.sort((a, b) => a.title.localeCompare(b.title)); + + return resources.map(r => r.subject); +} diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx index b00bbe3f2..da9dde3a2 100644 --- a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx +++ b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx @@ -21,6 +21,7 @@ import { FormValidationContextProvider } from '../../../components/forms/formVal import { randomString } from '../../../helpers/randomString'; import { PropertyForm } from './PropertyForm'; import { PropertyFormCategory } from './categories'; +import { sortSubjectList } from '../../OntologyPage/sortSubjectList'; interface NewPropertyDialogProps { showDialog: boolean; @@ -117,10 +118,12 @@ export function NewPropertyDialog({ if (tableClassParent.hasClasses(core.classes.ontology)) { await resource.set(core.properties.parent, tableClassParent.subject); - tableClassParent.push( + const ontologyProps = + tableClassParent.get(core.properties.properties) ?? []; + + await tableClassParent.set( core.properties.properties, - [resource.subject], - true, + await sortSubjectList(store, [...ontologyProps, resource.subject]), ); await tableClassParent.save(); diff --git a/browser/data-browser/src/views/TablePage/TablePage.tsx b/browser/data-browser/src/views/TablePage/TablePage.tsx index 1315cdcc7..e717c99a1 100644 --- a/browser/data-browser/src/views/TablePage/TablePage.tsx +++ b/browser/data-browser/src/views/TablePage/TablePage.tsx @@ -56,7 +56,7 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element { const tablePageContext: TablePageContextType = useMemo( () => ({ - tableClassSubject: tableClass.getSubject(), + tableClassSubject: tableClass.subject, sorting, setSortBy, addItemsToHistoryStack, diff --git a/browser/e2e/tests/ontology.spec.ts b/browser/e2e/tests/ontology.spec.ts index 764e5ce67..922b35d14 100644 --- a/browser/e2e/tests/ontology.spec.ts +++ b/browser/e2e/tests/ontology.spec.ts @@ -236,7 +236,7 @@ test.describe('Ontology', async () => { await page .getByRole('button', { name: 'add an item to the allows-only list' }) - .nth(1) + .nth(0) .click(); await page.getByRole('button', { name: 'Search for a arrow-kind' }).click(); await page @@ -248,7 +248,7 @@ test.describe('Ontology', async () => { await page .getByRole('button', { name: 'add an item to the allows-only list' }) - .nth(1) + .nth(0) .click(); await page.getByRole('button', { name: 'Search for a arrow-kind' }).click(); await page