Skip to content

Commit

Permalink
#873 Add support for resource arrays in tables
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Apr 29, 2024
1 parent 6af70b3 commit 2a5f1a7
Show file tree
Hide file tree
Showing 21 changed files with 862 additions and 464 deletions.
10 changes: 6 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
PR Checklist:
## Related Issues

- [ ] Link to related issues: #number
closes #number

## Checklist
- [ ] Add changelog entry linking to issue, describe API changes
- [ ] Add tests (optional)
- [ ] (If new feature) add to description / readme
- [ ] Add or update tests if needed
- [ ] Update docs if needed
2 changes: 1 addition & 1 deletion browser/data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@
"preview": "vite preview",
"start": "vite",
"test": "jest",
"typecheck": "pnpm exec tsc --noEmit"
"typecheck": "npx tsc --noEmit"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ResourceInline } from '../views/ResourceInline';

interface InlineFormattedResourceListProps {
subjects: string[];
/** Optional component to render items instead of an inline resource */
RenderComp?: React.FC<{ subject: string }>;
}

const formatter = new Intl.ListFormat('en-GB', {
Expand All @@ -11,6 +13,7 @@ const formatter = new Intl.ListFormat('en-GB', {

export function InlineFormattedResourceList({
subjects,
RenderComp,
}: InlineFormattedResourceListProps): JSX.Element {
// There are rare cases where a resource array can locally have an undefined value, we filter these out to prevent the formatter from throwing an error.
const filteredSubjects = subjects.filter(subject => subject !== undefined);
Expand All @@ -22,6 +25,10 @@ export function InlineFormattedResourceList({
return value;
}

if (RenderComp) {
return <RenderComp subject={value} key={value} />;
}

return <ResourceInline subject={value} key={value} />;
})}
</>
Expand Down
9 changes: 8 additions & 1 deletion browser/data-browser/src/components/TableEditor/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,14 @@ export function Cell({
setCursorMode(CursorMode.Visual);
setActiveCell(rowIndex, columnIndex);
},
[setActiveCell, columnIndex, shouldEnterEditMode, cursorMode, isActive],
[
setActiveCell,
columnIndex,
shouldEnterEditMode,
cursorMode,
isActive,
disabledKeyboardInteractions,
],
);

const handleClick = useCallback(() => {
Expand Down
137 changes: 27 additions & 110 deletions browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,45 @@ import {
core,
server,
unknownSubject,
urls,
useArray,
useResource,
useString,
useTitle,
} from '@tomic/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaEdit, FaTimes } from 'react-icons/fa';
import * as RadixPopover from '@radix-ui/react-popover';
import { FaEdit } from 'react-icons/fa';
import { styled } from 'styled-components';
import { FileDropzoneInput } from '../../../components/forms/FileDropzone/FileDropzoneInput';
import {
InputStyled,
InputWrapper,
} from '../../../components/forms/InputStyles';
import { Popover } from '../../../components/Popover';
import {
CursorMode,
useTableEditorContext,
} from '../../../components/TableEditor/TableEditorContext';
import { getIconForClass } from '../../FolderPage/iconMap';
import { AgentCell } from './ResourceCells/AgentCell';
import { FileCell } from './ResourceCells/FileCell';
import { SimpleResourceLink } from './ResourceCells/SimpleResourceLink';
import {
CellContainer,
DisplayCellProps,
EditCellProps,
ResourceCellProps,
} from './Type';
import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
import { useResourceSearch } from './useResourceSearch';
import { IconButton } from '../../../components/IconButton/IconButton';
import { AtomicLink } from '../../../components/AtomicLink';
import {
KeyboardInteraction,
useCellOptions,
} from '../../../components/TableEditor';
import { ResourceCell } from './ResourceCells/ResourceCell';
import {
PopoverTrigger,
SearchPopover,
SearchResultWrapper,
} from './CellComponents';
import { FaXmark } from 'react-icons/fa6';

const useClassType = (subject: string) => {
const property = useResource<Core.Property>(subject);

const classType = useResource<Core.Class>(property.props.classtype);
const hasClassType = classType?.getSubject() !== unknownSubject;
const hasClassType = classType?.subject !== unknownSubject;

return {
classType,
Expand Down Expand Up @@ -111,28 +107,11 @@ function AtomicURLCellEdit({

const { results, selectedIndex, handleKeyDown } = useResourceSearch(
searchValue,
hasClassType ? classType.getSubject() : undefined,
hasClassType ? classType.subject : undefined,
setOpen,
handleResultClick,
);

const modifiedHandleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
setOpen(false);

return;
}

if (e.key === 'Tab') {
return;
}

handleKeyDown(e);
},
[handleKeyDown],
);

const handleFilesUploaded = useCallback(
(files: string[]) => {
const file = files[0];
Expand All @@ -149,7 +128,7 @@ function AtomicURLCellEdit({
return (
<PopoverTrigger>
<FaEdit />{' '}
{cell.getSubject() === unknownSubject
{cell.subject === unknownSubject
? `select ${hasClassType ? classType.title : 'resource'}`
: title}
</PopoverTrigger>
Expand All @@ -158,16 +137,16 @@ function AtomicURLCellEdit({

useEffect(() => {
if (selectedElement.current) {
selectedElement.current.scrollIntoView(false);
selectedElement.current.scrollIntoView({ block: 'nearest' });
}
}, [selectedIndex]);

const placehoder = hasClassType ? `Search ${classType.title}` : 'Search...';

const showFileDropzone =
results.length === 0 && classType.getSubject() === urls.classes.file;
results.length === 0 && classType.subject === server.classes.file;
const showNoResults =
results.length === 0 && classType.getSubject() !== urls.classes.file;
results.length === 0 && classType.subject !== server.classes.file;

return (
<SearchPopover
Expand All @@ -182,10 +161,10 @@ function AtomicURLCellEdit({
value={searchValue}
placeholder={placehoder}
onChange={handleChange}
onKeyDown={modifiedHandleKeyDown}
onKeyDown={handleKeyDown}
/>
</InputWrapper>
<ResultWrapper>
<SearchResultWrapper>
{results.length > 0 && (
<ol>
{results.map((result, index) => (
Expand All @@ -208,35 +187,19 @@ function AtomicURLCellEdit({
onFilesUploaded={handleFilesUploaded}
/>
)}
</ResultWrapper>
</SearchResultWrapper>
</SearchPopover>
);
}

function AtomicURLCellDisplay({
value,
}: DisplayCellProps<JSONValue>): JSX.Element {
const resource = useResource(value as string);

if (!value) {
return <></>;
}

const Comp = resource.matchClass(
{
[core.classes.agent]: AgentCell,
[server.classes.file]: FileCell,
},
BasicCell,
);

return <Comp resource={resource} />;
}

function BasicCell({ resource }: ResourceCellProps) {
const [title] = useTitle(resource);

return <SimpleResourceLink resource={resource}>{title}</SimpleResourceLink>;
return <ResourceCell subject={value as string} />;
}

interface ResultProps {
Expand All @@ -247,7 +210,7 @@ interface ResultProps {
function Result({ subject, onClick }: ResultProps) {
const resource = useResource(subject);
const [title] = useTitle(resource);
const [[classType]] = useArray(resource, urls.properties.isA);
const [[classType]] = useArray(resource, core.properties.isA);

const Icon = getIconForClass(classType);

Expand Down Expand Up @@ -276,13 +239,10 @@ function FileUploadContainer({
row,
onChange,
}: FileUploadContainerProps): JSX.Element {
const [mimeType] = useString(cellResource, urls.properties.file.mimetype);
const [downloadUrl] = useString(
cellResource,
urls.properties.file.downloadUrl,
);
const [filename] = useString(cellResource, urls.properties.file.filename);
const [description] = useString(cellResource, urls.properties.description);
const [mimeType] = useString(cellResource, server.properties.mimetype);
const [downloadUrl] = useString(cellResource, server.properties.downloadUrl);
const [filename] = useString(cellResource, server.properties.filename);
const [description] = useString(cellResource, core.properties.description);

const isImage = mimeType?.startsWith('image/');

Expand All @@ -301,10 +261,10 @@ function FileUploadContainer({
<PreviewImg src={downloadUrl ?? ''} alt={description ?? ''} />
)}
{!isImage ? (
<AtomicLink subject={cellResource.getSubject()}>{filename}</AtomicLink>
<AtomicLink subject={cellResource.subject}>{filename}</AtomicLink>
) : null}
<ClearFileButton title='Clear' onClick={() => onChange(undefined)}>
<FaTimes />
<FaXmark />
</ClearFileButton>
</ViewerWrapper>
);
Expand Down Expand Up @@ -340,53 +300,10 @@ const ResultButton = styled.button`
}
`;

const SearchPopover = styled(Popover)`
padding: 1rem;
border: 1px solid ${p => p.theme.colors.bg2};
display: flex;
flex-direction: column;
gap: 1rem;
`;

const ResultWrapper = styled.div`
height: min(90vh, 20rem);
width: min(90vw, 35rem);
overflow-x: hidden;
overflow-y: auto;
ol {
padding: 0;
margin: 0;
}
li {
list-style: none;
&[data-selected='true'] button {
background: ${p => p.theme.colors.main};
color: white;
svg {
color: white;
}
}
}
`;

const StyledFileDropzoneInput = styled(FileDropzoneInput)`
height: 100%;
`;

const PopoverTrigger = styled(RadixPopover.Trigger)`
border: none;
background: none;
color: ${p => p.theme.colors.main};
display: inline-flex;
gap: 1ch;
align-items: center;
user-select: none;
cursor: pointer;
`;

const ViewerWrapper = styled.div`
display: flex;
justify-content: center;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { styled } from 'styled-components';
import * as RadixPopover from '@radix-ui/react-popover';
import { Popover } from '../../../components/Popover';

export const AbsoluteCell = styled.div`
position: absolute;
display: flex;
align-items: center;
z-index: 10;
left: 0;
top: 0;
background-color: ${p => p.theme.colors.bg};
box-shadow: ${p => p.theme.boxShadowSoft};
border: 2px solid ${p => p.theme.colors.main};
height: fit-content;
width: 100%;
padding-inline: var(--table-inner-padding);
padding-block: 3px;
min-height: 40px;
`;

export const SearchPopover = styled(Popover)`
padding: 1rem;
border: 1px solid ${p => p.theme.colors.bg2};
display: flex;
flex-direction: column;
gap: 1rem;
`;

export const SearchResultWrapper = styled.div`
height: min(90vh, 20rem);
width: min(90vw, 35rem);
overflow-x: hidden;
overflow-y: auto;
ol {
padding: 0;
margin: 0;
}
li {
list-style: none;
&[data-selected='true'] button {
background: ${p => p.theme.colors.main};
color: white;
svg {
color: white;
}
}
}
`;

export const PopoverTrigger = styled(RadixPopover.Trigger)`
border: none;
background: none;
color: ${p => p.theme.colors.main};
display: inline-flex;
gap: 1ch;
align-items: center;
user-select: none;
cursor: pointer;
`;

0 comments on commit 2a5f1a7

Please sign in to comment.