Skip to content

Commit

Permalink
#845 Create instances from ontology view
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Apr 15, 2024
1 parent 191ced5 commit 2ea168e
Show file tree
Hide file tree
Showing 15 changed files with 483 additions and 80 deletions.
4 changes: 3 additions & 1 deletion browser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Changelog

This changelog covers all three packages, as they are (for now) updated as a whole
This changelog covers all five packages, as they are (for now) updated as a whole

## Unreleased

### Atomic Browser

- [#845](https://github.com/atomicdata-dev/atomic-server/issues/845) Add option to create instances and tables from the ontology view.
- [#845](https://github.com/atomicdata-dev/atomic-server/issues/845) Add default Ontology option to drives.
- [#841](https://github.com/atomicdata-dev/atomic-server/issues/841) Add better inputs for `Timestamp` and `Date` datatypes.
- [#842](https://github.com/atomicdata-dev/atomic-server/issues/842) Add media picker for properties with classtype file.
- [#850](https://github.com/atomicdata-dev/atomic-server/issues/850) Add drag & drop sorting to ResourceArray inputs.
Expand Down
1 change: 1 addition & 0 deletions browser/data-browser/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const ButtonClean = styled.button<ButtonPropsStyled>`
appearance: none;
background-color: initial;
-webkit-tap-highlight-color: transparent; /* Remove the tap / click effect on touch devices */
user-select: none;
`;

/** Base button style. You're likely to want to use ButtonMargin in most places */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { styled } from 'styled-components';
import { FaCaretRight } from 'react-icons/fa';
import { Collapse } from '../Collapse';
import { Collapse } from './Collapse';

export interface DetailsProps {
open?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Waits for when the document becomes active again after it has been inert.
* (Useful for waiting for a dialog to close before navigating to a new page)
*/
export function waitForActiveDocument(callback: () => void) {
const observer = new MutationObserver(() => {
if (!document.body.hasAttribute('inert')) {
callback();
observer.disconnect();
}
});

observer.observe(document.body, {
attributes: true,
attributeFilter: ['inert'],
});
}
13 changes: 6 additions & 7 deletions browser/data-browser/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ export type MenuItemMinimial = {
shortcut?: string;
};

export type Item = typeof DIVIDER | MenuItemMinimial;
export type DropdownItem = typeof DIVIDER | MenuItemMinimial;

interface DropdownMenuProps {
/** The list of menu items */
items: Item[];
items: DropdownItem[];
trigger: DropdownTriggerRenderFunction;
/** Enables the keyboard shortcut */
isMainMenu?: boolean;
Expand All @@ -51,7 +51,7 @@ export const isItem = (
): item is MenuItemMinimial =>
typeof item !== 'string' && typeof item?.label === 'string';

const shouldSkip = (item?: Item) => !isItem(item) || item.disabled;
const shouldSkip = (item?: DropdownItem) => !isItem(item) || item.disabled;

const getAdditionalOffest = (increment: number) =>
increment === 0 ? 1 : Math.sign(increment);
Expand All @@ -62,7 +62,7 @@ const getAdditionalOffest = (increment: number) =>
* Returns 0 when no suitable index is found.
*/
const createIndexOffset =
(items: Item[]) => (startingPoint: number, offset: number) => {
(items: DropdownItem[]) => (startingPoint: number, offset: number) => {
const findNextAvailable = (
scopedStartingPoint: number,
scopedOffset: number,
Expand All @@ -84,8 +84,8 @@ const createIndexOffset =
return findNextAvailable(startingPoint, offset);
};

function normalizeItems(items: Item[]) {
return items.reduce((acc: Item[], current, i) => {
function normalizeItems(items: DropdownItem[]) {
return items.reduce((acc: DropdownItem[], current, i) => {
// If the item is a divider at the start or end of the list, remove it.
if ((i === 0 || i === items.length - 1) && !isItem(current)) {
return acc;
Expand Down Expand Up @@ -136,7 +136,6 @@ export function DropdownMenu({

useClickAwayListener([triggerRef, dropdownRef], handleClose, isActive, [
'click',
'mouseout',
]);

const normalizedItems = useMemo(() => normalizeItems(items), [items]);
Expand Down
51 changes: 51 additions & 0 deletions browser/data-browser/src/components/ParentPicker/ParentPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { styled } from 'styled-components';
import { Column } from '../Row';
import { ParentPickerItem } from './ParentPickerItem';
import { InputStyled, InputWrapper } from '../forms/InputStyles';
import { useSettings } from '../../helpers/AppSettings';
import { FaFolderOpen } from 'react-icons/fa6';

export interface ParentPickerProps {
root?: string;
value: string | undefined;
onChange: (subject: string) => void;
}

export function ParentPicker({
root,
value,
onChange,
}: ParentPickerProps): React.JSX.Element {
const { drive } = useSettings();

return (
<Column>
<InputWrapper hasPrefix>
<FaFolderOpen size='1rem' />
<InputStyled
placeholder='Enter a subject'
value={value ?? ''}
onChange={e => onChange(e.target.value)}
/>
</InputWrapper>
<PickerWrapper aria-label='parent selector'>
<ParentPickerItem
inialOpen={true}
subject={root ?? drive}
onClick={onChange}
selectedValue={value}
/>
</PickerWrapper>
</Column>
);
}

const PickerWrapper = styled.section`
background-color: ${p => p.theme.colors.bg};
border-radius: ${p => p.theme.radius};
border: 1px solid ${p => p.theme.colors.bg2};
padding: ${p => p.theme.margin}rem;
height: 20.5rem;
overflow-y: auto;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useEffect, useState } from 'react';
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
useDialog,
} from '../Dialog';
import { ParentPicker } from './ParentPicker';
import { Button } from '../Button';
import { waitForActiveDocument } from '../Dialog/waitForActiveDocument';

interface ParentPickerDialogProps {
root?: string;
open: boolean;
onSelect: (subject: string) => void;
onCancel?: () => void;
onOpenChange?: (open: boolean) => void;
title?: string;
}

export function ParentPickerDialog({
open,
root,
title,
onSelect,
onCancel,
onOpenChange,
}: ParentPickerDialogProps): React.JSX.Element {
const [selected, setSelected] = useState<string>();

const [dialogProps, show, close, isOpen] = useDialog({
onCancel,
bindShow: onOpenChange,
});

const select = () => {
if (!selected) return;

waitForActiveDocument(() => {
onSelect(selected);
});
close(true);
};

useEffect(() => {
if (open) {
show();
} else {
close();
setSelected(undefined);
}
}, [open]);

return (
<Dialog {...dialogProps}>
{isOpen && (
<>
<DialogTitle>
<h1>{title ?? 'Select a location'}</h1>
</DialogTitle>
<DialogContent>
<ParentPicker root={root} value={selected} onChange={setSelected} />
</DialogContent>
<DialogActions>
<Button subtle onClick={() => close(false)}>
Cancel
</Button>
<Button onClick={select} disabled={!selected}>
Select
</Button>
</DialogActions>
</>
)}
</Dialog>
);
}
148 changes: 148 additions & 0 deletions browser/data-browser/src/components/ParentPicker/ParentPickerItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
core,
dataBrowser,
Resource,
server,
useArray,
useCollection,
useResource,
useStore,
} from '@tomic/react';
import { Details } from '../Details';
import { useEffect, useState } from 'react';
import { getIconForClass } from '../../views/FolderPage/iconMap';
import styled from 'styled-components';

const shouldBeRendered = (resource: Resource) =>
resource.hasClasses(dataBrowser.classes.folder) ||
resource.hasClasses(server.classes.drive);

interface ParentPickerItemProps {
subject: string;
selectedValue: string | undefined;
inialOpen?: boolean;
onClick: (subject: string) => void;
}

export const ParentPickerItem: React.FC<ParentPickerItemProps> = ({
subject,
...props
}) => {
const resource = useResource(subject);

if (
!resource.hasClasses(dataBrowser.classes.folder) &&
!resource.hasClasses(server.classes.drive)
) {
return null;
}

return <InnerItem subject={subject} {...props} />;
};

const InnerItem = ({
subject,
selectedValue,
inialOpen,
onClick,
}: ParentPickerItemProps) => {
const store = useStore();
const { collection } = useCollection({
property: core.properties.parent,
value: subject,
});

const [children, setChildren] = useState<string[]>([]);

useEffect(() => {
collection.getAllMembers().then(async (members: string[]) => {
const resources = await Promise.all(
members.map(s => store.getResource(s)),
);
const filtered = resources.filter(shouldBeRendered);

setChildren(filtered.map(r => r.subject));
});
}, [collection]);

if (children.length === 0) {
return (
<Title
indented
subject={subject}
onClick={onClick}
selected={selectedValue === subject}
/>
);
}

return (
<Details
initialState={inialOpen}
open={inialOpen}
title={
<Title
subject={subject}
selected={selectedValue === subject}
onClick={onClick}
/>
}
>
{children.map(child => (
<ParentPickerItem
key={child}
subject={child}
selectedValue={selectedValue}
onClick={onClick}
/>
))}
</Details>
);
};

interface TitleProps extends Omit<ParentPickerItemProps, 'selectedValue'> {
indented?: boolean;
selected?: boolean;
}

const Title = ({
subject,
indented,
selected,
onClick,
}: TitleProps): React.JSX.Element => {
const resource = useResource(subject);
const [isA] = useArray(resource, core.properties.isA);

const Icon = getIconForClass(isA[0]);

return (
<FolderButton
selected={selected}
indented={indented}
onClick={() => onClick(subject)}
>
<Icon />
{resource.title}
</FolderButton>
);
};

const FolderButton = styled.button<{ indented?: boolean; selected?: boolean }>`
display: flex;
align-items: center;
gap: 1ch;
background-color: ${p => (p.selected ? p.theme.colors.bg1 : 'transparent')};
color: ${p => (p.selected ? p.theme.colors.main : p.theme.colors.textLight)};
cursor: pointer;
border: none;
padding: 0.3rem 0.5rem;
margin-inline-start: ${p => (p.indented ? '2rem' : '0')};
border-radius: ${p => p.theme.radius};
user-select: none;
&:hover {
background-color: ${p => p.theme.colors.bg1};
color: ${p => (p.selected ? p.theme.colors.main : p.theme.colors.text)};
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
shareURL,
importerURL,
} from '../../helpers/navigation';
import { DIVIDER, DropdownMenu, isItem, Item } from '../Dropdown';
import { DIVIDER, DropdownMenu, isItem, DropdownItem } from '../Dropdown';
import toast from 'react-hot-toast';
import { paths } from '../../routes/paths';
import { shortcuts } from '../HotKeyWrapper';
Expand Down Expand Up @@ -105,7 +105,7 @@ function ResourceContextMenu({
return null;
}

const items: Item[] = [
const items: DropdownItem[] = [
...(simple
? []
: [
Expand Down
Loading

0 comments on commit 2ea168e

Please sign in to comment.