From 918404696f0bdd9ef2486dc6d53b700b9600191c Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 8 Apr 2024 11:25:33 +0200 Subject: [PATCH] #845 Default Ontology --- .../src/components/Dropdown/index.tsx | 12 +- .../OntologySideBar/OntologiesPanel.tsx | 47 +++--- .../BasicInstanceHandlers.ts | 30 +--- .../CustomForms/NewDriveDialog.tsx | 134 ++++++++++++++++++ .../CustomForms/NewTableDialog.tsx | 92 ++++++++---- .../CustomCreateActions/CustomForms/index.ts | 4 +- .../src/hooks/useAddToOntology.ts | 45 ++++++ .../src/hooks/useCreateAndNavigate.ts | 21 ++- browser/data-browser/src/views/DrivePage.tsx | 106 +++++++++----- .../src/views/OntologyPage/OntologyPage.tsx | 8 +- .../PropertyForm/NewPropertyDialog.tsx | 24 +++- .../src/views/TablePage/TableCell.tsx | 4 +- .../src/views/TablePage/TableHeading.tsx | 13 +- .../src/views/TablePage/TableHeadingMenu.tsx | 88 ++++++------ browser/lib/src/ontologies/collections.ts | 2 +- browser/lib/src/ontologies/commits.ts | 2 +- browser/lib/src/ontologies/core.ts | 2 +- browser/lib/src/ontologies/dataBrowser.ts | 4 +- browser/lib/src/ontologies/server.ts | 9 +- 19 files changed, 454 insertions(+), 193 deletions(-) create mode 100644 browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx create mode 100644 browser/data-browser/src/hooks/useAddToOntology.ts diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx index 76acb9a11..89fb48d5e 100644 --- a/browser/data-browser/src/components/Dropdown/index.tsx +++ b/browser/data-browser/src/components/Dropdown/index.tsx @@ -412,6 +412,9 @@ const MenuItemStyled = styled(Button)` p.selected ? p.theme.colors.bg1 : p.theme.colors.bg}; text-decoration: ${p => (p.selected ? 'underline' : 'none')}; + & svg { + color: ${p => p.theme.colors.textLight}; + } &:hover { background-color: ${p => p.theme.colors.bg1}; } @@ -419,12 +422,17 @@ const MenuItemStyled = styled(Button)` background-color: ${p => p.theme.colors.bg2}; } &:disabled { - color: ${p => p.theme.colors.textLight}; + color: ${p => p.theme.colors.textLight2}; cursor: default; + background-color: ${p => p.theme.colors.bg}; + &:hover { cursor: 'default'; } - background-color: ${p => p.theme.colors.bg}; + + & svg { + color: ${p => p.theme.colors.textLight2}; + } } svg { diff --git a/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx b/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx index 66466a732..d884e22b2 100644 --- a/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx +++ b/browser/data-browser/src/components/SideBar/OntologySideBar/OntologiesPanel.tsx @@ -1,29 +1,35 @@ import { styled } from 'styled-components'; -import { - Collection, - unknownSubject, - urls, - useCollection, - useMemberFromCollection, -} from '@tomic/react'; +import { core, unknownSubject, useResource, useStore } from '@tomic/react'; import { SideBarItem } from '../SideBarItem'; import { Row } from '../../Row'; import { AtomicLink } from '../../AtomicLink'; import { getIconForClass } from '../../../views/FolderPage/iconMap'; import { ScrollArea } from '../../ScrollArea'; import { ErrorLook } from '../../ErrorLook'; +import { useEffect, useState } from 'react'; +import { useSettings } from '../../../helpers/AppSettings'; export function OntologiesPanel(): JSX.Element | null { - const { collection } = useCollection({ - property: urls.properties.isA, - value: urls.classes.ontology, - }); + const store = useStore(); + const [ontologies, setOntologies] = useState([]); + const { drive } = useSettings(); + + useEffect(() => { + store + .search('', { + filters: { + [core.properties.isA]: core.classes.ontology, + }, + parents: drive, + }) + .then(setOntologies); + }, [store]); return ( - {[...Array(collection.totalMembers).keys()].map(index => ( - + {ontologies.map(subject => ( + ))} @@ -37,25 +43,24 @@ const Wrapper = styled.div` `; const StyledScrollArea = styled(ScrollArea)` - height: 10rem; + max-height: 10rem; overflow-x: hidden; `; interface ItemProps { - index: number; - collection: Collection; + subject: string; } -function Item({ index, collection }: ItemProps): JSX.Element { - const resource = useMemberFromCollection(collection, index); +function Item({ subject }: ItemProps): JSX.Element { + const resource = useResource(subject); - const Icon = getIconForClass(urls.classes.ontology); + const Icon = getIconForClass(core.classes.ontology); if (resource.loading) { return
loading
; } - if (resource.error || resource.getSubject() === unknownSubject) { + if (resource.error || resource.subject === unknownSubject) { return ( Invalid Resource @@ -64,7 +69,7 @@ function Item({ index, collection }: ItemProps): JSX.Element { } return ( - + diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts index d7b2b7295..3ef97a192 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/BasicInstanceHandlers.ts @@ -1,4 +1,4 @@ -import { dataBrowser, core, classes, server } from '@tomic/react'; +import { dataBrowser, core, classes } from '@tomic/react'; import { registerBasicInstanceHandler } from '../useNewResourceUI'; /** @@ -12,7 +12,7 @@ export const registerBasicInstanceHandlers = () => { await createAndNavigate( dataBrowser.classes.folder, { - [core.properties.name]: 'Untitled Folder', + [core.properties.name]: 'untitled-folder', [dataBrowser.properties.displayStyle]: classes.displayStyles.list, }, parent, @@ -45,30 +45,4 @@ export const registerBasicInstanceHandlers = () => { ); }, ); - - registerBasicInstanceHandler( - server.classes.drive, - async (_parent, createAndNavigate, { store, settings }) => { - const agent = store.getAgent(); - - if (!agent || agent.subject === undefined) { - throw new Error( - 'No agent set in the Store, required when creating a Drive', - ); - } - - const newResource = await createAndNavigate(server.classes.drive, { - [core.properties.write]: [agent.subject], - [core.properties.read]: [agent.subject], - }); - - // resources created with createAndNavigate have a parent by default which we don't want for drives. - newResource.remove(core.properties.parent); - - const agentResource = await store.getResource(agent.subject); - agentResource.push(server.properties.drives, [newResource.subject]); - agentResource.save(); - settings.setDrive(newResource.subject); - }, - ); }; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx new file mode 100644 index 000000000..af41deb5b --- /dev/null +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewDriveDialog.tsx @@ -0,0 +1,134 @@ +import { core, useStore, server, dataBrowser, Resource } from '@tomic/react'; +import { useState, useCallback, FormEvent, FC, useEffect } from 'react'; +import { styled } from 'styled-components'; +import { stringToSlug } from '../../../../../helpers/stringToSlug'; +import { Button } from '../../../../Button'; +import { + useDialog, + Dialog, + DialogContent, + DialogActions, +} from '../../../../Dialog'; +import Field from '../../../Field'; +import { InputWrapper, InputStyled } from '../../../InputStyles'; +import { CustomResourceDialogProps } from '../../useNewResourceUI'; +import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; +import { useSettings } from '../../../../../helpers/AppSettings'; + +export const NewDriveDialog: FC = ({ + parent, + onClose, +}) => { + const store = useStore(); + const { setDrive } = useSettings(); + const [name, setName] = useState(''); + + const createAndNavigate = useCreateAndNavigate(); + + const onSuccess = useCallback(async () => { + if (!name.trim()) return; + + const agent = store.getAgent(); + + if (!agent || agent.subject === undefined) { + throw new Error( + 'No agent set in the Store, required when creating a Drive', + ); + } + + const newDrive = await createAndNavigate( + server.classes.drive, + { + [core.properties.name]: name, + [core.properties.write]: [agent.subject], + [core.properties.read]: [agent.subject], + }, + undefined, + undefined, + async resource => { + // resources created with createAndNavigate have a parent by default which we don't want for drives. + resource.remove(core.properties.parent); + + // Add drive to the agents drive list. + const agentResource = await store.getResource(agent.subject!); + agentResource.push(server.properties.drives, [resource.subject]); + await agentResource.save(); + + // Create a default ontology. + const ontologyName = stringToSlug(name); + const ontology = await store.newResource({ + subject: await store.buildUniqueSubjectFromParts( + ['defaultOntology'], + resource.subject, + ), + isA: core.classes.ontology, + parent: resource.subject, + propVals: { + [core.properties.shortname]: ontologyName, + [core.properties + .description]: `Default ontology for the ${name} drive`, + [core.properties.classes]: [], + [core.properties.properties]: [], + [core.properties.instances]: [], + }, + }); + + await ontology.save(); + + await resource.set(server.properties.defaultOntology, ontology.subject); + await resource.set(dataBrowser.properties.subResources, [ + ontology.subject, + ]); + await resource.save(); + }, + ); + + // Change current drive to new drive + setDrive(newDrive.subject); + + onClose(); + }, [name, createAndNavigate, onClose, parent, setDrive, store]); + + const [dialogProps, show, hide] = useDialog({ onSuccess, onCancel: onClose }); + + useEffect(() => { + show(); + }, []); + + return ( + +

New Drive

+ +
{ + e.preventDefault(); + hide(true); + }} + > + + + setName(e.target.value)} + /> + + +
+
+ + + + +
+ ); +}; + +const H1 = styled.h1` + margin: 0; +`; diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx index 5cf5eda33..2da8abd03 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/NewTableDialog.tsx @@ -1,4 +1,4 @@ -import { useResource, Core, dataBrowser, core, useStore } from '@tomic/react'; +import { dataBrowser, core, useStore } from '@tomic/react'; import { useState, useCallback, useEffect, FormEvent, FC } from 'react'; import { styled } from 'styled-components'; import { stringToSlug } from '../../../../../helpers/stringToSlug'; @@ -15,55 +15,70 @@ import Field from '../../../Field'; import { InputWrapper, InputStyled } from '../../../InputStyles'; import type { CustomResourceDialogProps } from '../../useNewResourceUI'; import { useCreateAndNavigate } from '../../../../../hooks/useCreateAndNavigate'; - -const instanceOpts = { - newResource: true, -}; +import { ResourceSelector } from '../../../ResourceSelector'; +import { Checkbox, CheckboxLabel } from '../../../Checkbox'; +import { useAddToOntology } from '../../../../../hooks/useAddToOntology'; export const NewTableDialog: FC = ({ parent, onClose, }) => { const store = useStore(); - const [instanceSubject] = useState(() => store.createSubject('class')); - const instanceResource = useResource( - instanceSubject, - instanceOpts, - ); - + const [useExistingClass, setUseExistingClass] = useState(false); + const [existingClass, setExistingClass] = useState(); const [name, setName] = useState(''); + const addToOntology = useAddToOntology(); const createResourceAndNavigate = useCreateAndNavigate(); const onCancel = useCallback(() => { - instanceResource.destroy(); onClose(); - }, [onClose, instanceResource]); + }, [onClose]); const onSuccess = useCallback(async () => { - await instanceResource.set(core.properties.shortname, stringToSlug(name)); - await instanceResource.set( - core.properties.description, - `Represents a row in the ${name} table`, - ); - await instanceResource.set(core.properties.isA, [core.classes.class]); - await instanceResource.set(core.properties.parent, parent); - await instanceResource.set(core.properties.recommends, [ - core.properties.name, - ]); - await instanceResource.save(); + let classSubject: string; + + if (!useExistingClass) { + const instanceResource = await store.newResource({ + subject: store.createSubject('class'), + isA: core.classes.class, + propVals: { + [core.properties.shortname]: stringToSlug(name), + [core.properties + .description]: `Represents a row in the ${name} table`, + [core.properties.recommends]: [core.properties.name], + }, + }); + + await addToOntology(instanceResource); + classSubject = instanceResource.subject; + } else { + if (existingClass === undefined) { + throw new Error('Existing class is undefined'); + } + + classSubject = existingClass; + } createResourceAndNavigate( dataBrowser.classes.table, { [core.properties.name]: name, - [core.properties.classtype]: instanceResource.getSubject(), + [core.properties.classtype]: classSubject, }, parent, ); onClose(); - }, [name, instanceResource, onClose, parent]); + }, [ + name, + onClose, + parent, + useExistingClass, + existingClass, + addToOntology, + createResourceAndNavigate, + ]); const [dialogProps, show, hide] = useDialog({ onCancel, onSuccess }); @@ -71,6 +86,9 @@ export const NewTableDialog: FC = ({ show(); }, []); + const hasName = name.trim() !== ''; + const saveDisabled = useExistingClass ? !hasName || !existingClass : !hasName; + return ( @@ -94,13 +112,31 @@ export const NewTableDialog: FC = ({ /> + + + Use existing class + + + {useExistingClass && ( + + )} + - @@ -109,7 +145,7 @@ export const NewTableDialog: FC = ({ }; const WiderDialogContent = styled(DialogContent)` - width: min(80vw, 20rem); + /* width: min(80vw, 20rem); */ `; const RelativeDialogTitle = styled(DialogTitle)` diff --git a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts index 20adb421c..28ef0656d 100644 --- a/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts +++ b/browser/data-browser/src/components/forms/NewForm/CustomCreateActions/CustomForms/index.ts @@ -1,9 +1,10 @@ -import { dataBrowser, core, collections } from '@tomic/react'; +import { dataBrowser, core, collections, server } from '@tomic/react'; import { registerNewResourceDialog } from '../../useNewResourceUI'; import { NewBookmarkDialog } from './NewBookmarkDialog'; import { NewOntologyDialog } from './NewOntologyDialog'; import { NewTableDialog } from './NewTableDialog'; import { NewCollectionDialog } from './NewCollectionDialog'; +import { NewDriveDialog } from './NewDriveDialog'; export const registerCustomForms = () => { registerNewResourceDialog(dataBrowser.classes.bookmark, NewBookmarkDialog); @@ -13,4 +14,5 @@ export const registerCustomForms = () => { collections.classes.collection, NewCollectionDialog, ); + registerNewResourceDialog(server.classes.drive, NewDriveDialog); }; diff --git a/browser/data-browser/src/hooks/useAddToOntology.ts b/browser/data-browser/src/hooks/useAddToOntology.ts new file mode 100644 index 000000000..2a05406a9 --- /dev/null +++ b/browser/data-browser/src/hooks/useAddToOntology.ts @@ -0,0 +1,45 @@ +import { + Resource, + useResource, + useStore, + Server, + unknownSubject, + core, +} from '@tomic/react'; +import { useSettings } from '../helpers/AppSettings'; +import { useCallback } from 'react'; + +export function useAddToOntology(ontologySubject?: string) { + const store = useStore(); + const { drive: driveSubject } = useSettings(); + const drive = useResource(driveSubject); + + const ontology = useResource( + ontologySubject ?? drive.props.defaultOntology ?? unknownSubject, + ); + + return useCallback( + async (resource: Resource) => { + if (ontology.subject === unknownSubject) { + await resource.set(core.properties.parent, driveSubject); + resource.save(); + + return; + } + + await resource.set(core.properties.parent, ontology.subject); + await resource.save(); + + if (resource.hasClasses(core.classes.class)) { + ontology.push(core.properties.classes, [resource.subject], true); + } else if (resource.hasClasses(core.classes.property)) { + ontology.push(core.properties.properties, [resource.subject], true); + } else { + ontology.push(core.properties.instances, [resource.subject], true); + } + + await ontology.save(); + }, + [store, drive, ontology], + ); +} diff --git a/browser/data-browser/src/hooks/useCreateAndNavigate.ts b/browser/data-browser/src/hooks/useCreateAndNavigate.ts index fab8e55f3..32ef51a3a 100644 --- a/browser/data-browser/src/hooks/useCreateAndNavigate.ts +++ b/browser/data-browser/src/hooks/useCreateAndNavigate.ts @@ -9,7 +9,9 @@ export type CreateAndNavigate = ( isA: string, propVals: Record, parent?: string, + /** Query parameters for the resource / endpoint */ extraParams?: Record, + onCreated?: (resource: Resource) => Promise, ) => Promise; /** @@ -22,13 +24,13 @@ export function useCreateAndNavigate(): CreateAndNavigate { const store = useStore(); const navigate = useNavigate(); - return useCallback( + const createAndNavigate: CreateAndNavigate = useCallback( async ( - isA: string, - propVals: Record, - parent?: string, - /** Query parameters for the resource / endpoint */ - extraParams?: Record, + isA, + propVals, + parent, + extraParams, + onCreated, ): Promise => { const classResource = await store.getResource(isA); @@ -47,6 +49,11 @@ export function useCreateAndNavigate(): CreateAndNavigate { try { await resource.save(); + + if (onCreated) { + await onCreated(resource); + } + navigate(constructOpenURL(newSubject, extraParams)); toast.success(`${classResource.title} created`); store.notifyResourceManuallyCreated(resource); @@ -58,4 +65,6 @@ export function useCreateAndNavigate(): CreateAndNavigate { }, [store, navigate, parent], ); + + return createAndNavigate; } diff --git a/browser/data-browser/src/views/DrivePage.tsx b/browser/data-browser/src/views/DrivePage.tsx index 4b11103d8..e1681e331 100644 --- a/browser/data-browser/src/views/DrivePage.tsx +++ b/browser/data-browser/src/views/DrivePage.tsx @@ -1,4 +1,12 @@ -import { useArray, properties, Datatype } from '@tomic/react'; +import { + useArray, + Datatype, + dataBrowser, + core, + server, + useProperty, + useCanWrite, +} from '@tomic/react'; import { ContainerNarrow } from '../components/Containers'; import { Card, CardInsideFull, CardRow } from '../components/Card'; import { ResourceInline } from './ResourceInline'; @@ -10,59 +18,79 @@ import { FaPlus } from 'react-icons/fa'; import { paths } from '../routes/paths'; import { ResourcePageProps } from './ResourcePage'; import { EditableTitle } from '../components/EditableTitle'; -import { Row } from '../components/Row'; +import { Column, Row } from '../components/Row'; +import { styled } from 'styled-components'; +import InputSwitcher from '../components/forms/InputSwitcher'; /** A View for Drives, which function similar to a homepage or dashboard. */ function DrivePage({ resource }: ResourcePageProps): JSX.Element { - const [subResources] = useArray(resource, properties.subResources); + const [subResources] = useArray( + resource, + dataBrowser.properties.subResources, + ); const { drive: baseURL, setDrive: setBaseURL } = useSettings(); + const defaultOntologyProp = useProperty(server.properties.defaultOntology); + const [canEdit] = useCanWrite(resource); + if (!baseURL) { - setBaseURL(resource.getSubject()); + setBaseURL(resource.subject); } return ( - - - {baseURL !== resource.getSubject() && ( - - )} - - - -

resources:

- - {subResources.map(child => { - return ( + + + + {baseURL !== resource.subject && ( + + )} + + +
+ Default Ontology + +
+ + Resources: + + {subResources.map(child => ( - ); - })} - - - Create new resource - - - - - {baseURL.startsWith('http://localhost') && ( -

- You are running Atomic-Server on `localhost`, which means that it will - not be available from any other machine than your current local - device. If you want your Atomic-Server to be available from the web, - you should set this up at a Domain on a server. -

- )} + ))} + + + Create new resource + + +
+
+ {baseURL.startsWith('http://localhost') && ( +

+ You are running Atomic-Server on `localhost`, which means that it + will not be available from any other machine than your current local + device. If you want your Atomic-Server to be available from the web, + you should set this up at a Domain on a server. +

+ )} +
); } export default DrivePage; + +const Heading = styled.h2` + font-size: 1.3rem; +`; diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx index 2aa01cfb1..65afb4f42 100644 --- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx +++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx @@ -1,5 +1,5 @@ import { ResourcePageProps } from '../ResourcePage'; -import { urls, useArray, useCanWrite } from '@tomic/react'; +import { core, useArray, useCanWrite } from '@tomic/react'; import { OntologySidebar } from './OntologySidebar'; import { styled } from 'styled-components'; import { ClassCardRead } from './Class/ClassCardRead'; @@ -19,9 +19,9 @@ import { CreateInstanceButton } from './CreateInstanceButton'; import { useState } from 'react'; export function OntologyPage({ resource }: ResourcePageProps) { - const [classes] = useArray(resource, urls.properties.classes); - const [properties] = useArray(resource, urls.properties.properties); - const [instances] = useArray(resource, urls.properties.instances); + const [classes] = useArray(resource, core.properties.classes); + const [properties] = useArray(resource, core.properties.properties); + const [instances] = useArray(resource, core.properties.instances); const [canWrite] = useCanWrite(resource); const [editMode, setEditMode] = useState(false); diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx index 7bc61415a..a2f1d61ba 100644 --- a/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx +++ b/browser/data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx @@ -1,4 +1,5 @@ import { + Core, Datatype, Resource, Store, @@ -22,7 +23,7 @@ import { PropertyForm, PropertyFormCategory } from './PropertyForm'; interface NewPropertyDialogProps { showDialog: boolean; - tableClassResource: Resource; + tableClassResource: Resource; bindShow: React.Dispatch; selectedCategory?: string; } @@ -35,10 +36,10 @@ const createSubjectWithBase = (base: string) => { const populatePropertyWithDefaults = async ( property: Resource, - tableClass: Resource, + tableClass: Resource, ) => { await property.set(core.properties.isA, [core.classes.property]); - await property.set(core.properties.parent, tableClass.subject); + await property.set(core.properties.parent, tableClass.props.parent); await property.set(core.properties.shortname, 'new-column', false); await property.set(core.properties.name, '', false); await property.set(core.properties.description, 'A column in a table'); @@ -108,9 +109,24 @@ export function NewPropertyDialog({ return; } + const tableClassParent = await store.getResource( + tableClassResource.props.parent, + ); + + if (tableClassParent.hasClasses(core.classes.ontology)) { + await resource.set(core.properties.parent, tableClassParent.subject); + + tableClassParent.push( + core.properties.properties, + [resource.subject], + true, + ); + + await tableClassParent.save(); + } + await resource.save(); await saveChildren(store, resource); - await store.notifyResourceManuallyCreated(resource); pushProp([resource.subject]); setResource(null); diff --git a/browser/data-browser/src/views/TablePage/TableCell.tsx b/browser/data-browser/src/views/TablePage/TableCell.tsx index 3756e8f7c..db862fbee 100644 --- a/browser/data-browser/src/views/TablePage/TableCell.tsx +++ b/browser/data-browser/src/views/TablePage/TableCell.tsx @@ -1,8 +1,8 @@ import { + commits, JSONValue, Property, Resource, - urls, useDebouncedCallback, useValue, } from '@tomic/react'; @@ -75,7 +75,7 @@ export function TableCell({ const [createdAt, setCreatedAt] = useValue( resource, - urls.properties.commit.createdAt, + commits.properties.createdAt, { commit: false, commitDebounce: 0 }, ); diff --git a/browser/data-browser/src/views/TablePage/TableHeading.tsx b/browser/data-browser/src/views/TablePage/TableHeading.tsx index 4a2aa970b..78de37c0e 100644 --- a/browser/data-browser/src/views/TablePage/TableHeading.tsx +++ b/browser/data-browser/src/views/TablePage/TableHeading.tsx @@ -1,4 +1,5 @@ import { + Core, Datatype, Property, Resource, @@ -43,12 +44,16 @@ export const TableHeading: TableHeadingComponent = ({ const propResource = useResource(column.subject); const [title] = useTitle(propResource); - const { setSortBy, sorting } = useContext(TablePageContext); + const { setSortBy, sorting, tableClassSubject } = + useContext(TablePageContext); + const tableClass = useResource(tableClassSubject); + + const isRequired = (tableClass.props.requires ?? []).includes(column.subject); const Icon = getIcon(propResource, sorting, hoverOrFocus, column.datatype); - const isSorted = sorting.prop === propResource.getSubject(); + const isSorted = sorting.prop === propResource.subject; - const text = title || column.shortname; + const text = `${title || column.shortname}${isRequired ? '*' : ''}`; return ( <> @@ -62,7 +67,7 @@ export const TableHeading: TableHeadingComponent = ({ setSortBy(propResource.getSubject())} + onClick={() => setSortBy(propResource.subject)} bold={isSorted} title={text} > diff --git a/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx b/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx index ced40b999..2a79fe87b 100644 --- a/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx +++ b/browser/data-browser/src/views/TablePage/TableHeadingMenu.tsx @@ -6,10 +6,10 @@ import { useString, core, } from '@tomic/react'; -import { useCallback, useContext, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { DropdownMenu, Item } from '../../components/Dropdown'; import { buildDefaultTrigger } from '../../components/Dropdown/DefaultTrigger'; -import { FaEdit, FaEllipsisV, FaEye, FaTimes, FaTrash } from 'react-icons/fa'; +import { FaEdit, FaEllipsisV, FaEye, FaTimes } from 'react-icons/fa'; import { styled } from 'styled-components'; import { EditPropertyDialog } from './PropertyForm/EditPropertyDialog'; import { TablePageContext } from './tablePageContext'; @@ -21,6 +21,8 @@ import { ResourceInline } from '../ResourceInline'; import { ResourceUsage } from '../../components/ResourceUsage'; import { useNavigate } from 'react-router'; import { constructOpenURL } from '../../helpers/navigation'; +import { Checkbox, CheckboxLabel } from '../../components/forms/Checkbox'; +import { Column } from '../../components/Row'; interface TableHeadingMenuProps { resource: Resource; @@ -38,11 +40,14 @@ const useIsExternalProperty = (property: Resource) => { export function TableHeadingMenu({ resource, }: TableHeadingMenuProps): JSX.Element { - const canWrite = useCanWrite(resource); const [showEditDialog, setShowEditDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [fullDelete, setFullDelete] = useState(false); + const { tableClassSubject } = useContext(TablePageContext); const tableClassResource = useResource(tableClassSubject); + const [canWriteClass] = useCanWrite(tableClassResource); + const [canWriteProperty] = useCanWrite(resource); const navigate = useNavigate(); const isExternalProperty = useIsExternalProperty(resource); @@ -82,45 +87,40 @@ export function TableHeadingMenu({ } }, [deleteProperty, removeProperty, isExternalProperty]); - const items = useMemo((): Item[] => { - const initialItems = [ - { - id: 'edit', - label: 'Edit', - onClick: () => setShowEditDialog(true), - icon: , - disabled: !canWrite || isExternalProperty, - }, + const items = useMemo( + (): Item[] => [ { id: 'view', label: 'View', onClick: () => { - navigate(constructOpenURL(resource.getSubject())); + navigate(constructOpenURL(resource.subject)); }, icon: , }, - ]; - - if (isExternalProperty) { - initialItems.push({ + { + id: 'edit', + label: 'Edit', + onClick: () => setShowEditDialog(true), + icon: , + disabled: !canWriteProperty, + }, + { id: 'remove', label: 'Remove', onClick: () => setShowDeleteDialog(true), icon: , - disabled: !canWrite, - }); - } else { - initialItems.push({ - id: 'delete', - label: 'Delete', - onClick: () => setShowDeleteDialog(true), - icon: , - disabled: !canWrite, - }); - } + disabled: !canWriteClass, + }, + ], + [canWriteClass, canWriteProperty, navigate, resource], + ); - return initialItems; - }, []); + // Reset fullDelete when dialog is closed + useEffect(() => { + if (!showDeleteDialog) { + setFullDelete(false); + } + }, [showDeleteDialog]); return ( @@ -131,30 +131,24 @@ export function TableHeadingMenu({ bindShow={setShowEditDialog} /> - {isExternalProperty ? ( +

- Remove from this - table + Remove from{' '} +

- ) : ( - <> -

- Are you sure you want to delete this column? -
- This will delete the{' '} - property and - its children. -

- - - )} + + + + Delete property and its children + +
); diff --git a/browser/lib/src/ontologies/collections.ts b/browser/lib/src/ontologies/collections.ts index 2800da98c..e8d1eb1cb 100644 --- a/browser/lib/src/ontologies/collections.ts +++ b/browser/lib/src/ontologies/collections.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const collections = { classes: { diff --git a/browser/lib/src/ontologies/commits.ts b/browser/lib/src/ontologies/commits.ts index 50eb6e018..a75e78cb5 100644 --- a/browser/lib/src/ontologies/commits.ts +++ b/browser/lib/src/ontologies/commits.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const commits = { classes: { diff --git a/browser/lib/src/ontologies/core.ts b/browser/lib/src/ontologies/core.ts index 2a5503cda..e18ad4d05 100644 --- a/browser/lib/src/ontologies/core.ts +++ b/browser/lib/src/ontologies/core.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const core = { classes: { diff --git a/browser/lib/src/ontologies/dataBrowser.ts b/browser/lib/src/ontologies/dataBrowser.ts index 12dd67d71..4f524a370 100644 --- a/browser/lib/src/ontologies/dataBrowser.ts +++ b/browser/lib/src/ontologies/dataBrowser.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const dataBrowser = { classes: { @@ -194,7 +194,7 @@ declare module '../index.js' { interface PropTypeMapping { [dataBrowser.properties.subResources]: string[]; [dataBrowser.properties.displayStyle]: string; - [dataBrowser.properties.publishedAt]: string; + [dataBrowser.properties.publishedAt]: number; [dataBrowser.properties.elements]: string[]; [dataBrowser.properties.messages]: string[]; [dataBrowser.properties.nextPage]: string; diff --git a/browser/lib/src/ontologies/server.ts b/browser/lib/src/ontologies/server.ts index 510b850c7..c12319fd6 100644 --- a/browser/lib/src/ontologies/server.ts +++ b/browser/lib/src/ontologies/server.ts @@ -3,7 +3,7 @@ * For more info on how to use ontologies: https://github.com/atomicdata-dev/atomic-server/blob/develop/browser/cli/readme.md * -------------------------------- */ -import { BaseProps } from '../index.js'; +import type { BaseProps } from '../index.js'; export const server = { classes: { @@ -41,6 +41,8 @@ export const server = { status: 'https://atomicdata.dev/ontology/server/property/status', responseMessage: 'https://atomicdata.dev/ontology/server/property/response-message', + defaultOntology: + 'https://atomicdata.dev/ontology/server/property/default-ontology', }, } as const; @@ -75,7 +77,8 @@ declare module '../index.js' { | typeof server.properties.children | 'https://atomicdata.dev/properties/description' | 'https://atomicdata.dev/properties/subresources' - | 'https://atomicdata.dev/properties/write'; + | 'https://atomicdata.dev/properties/write' + | typeof server.properties.defaultOntology; }; [server.classes.redirect]: { requires: BaseProps | typeof server.properties.destination; @@ -132,6 +135,7 @@ declare module '../index.js' { [server.properties.destination]: string; [server.properties.status]: number; [server.properties.responseMessage]: string; + [server.properties.defaultOntology]: string; } interface PropSubjectToNameMapping { @@ -158,5 +162,6 @@ declare module '../index.js' { [server.properties.destination]: 'destination'; [server.properties.status]: 'status'; [server.properties.responseMessage]: 'responseMessage'; + [server.properties.defaultOntology]: 'defaultOntology'; } }