diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index f7a8f5dbb..7106810e7 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -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
diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json
index 0c567c487..aa2c1d7f5 100644
--- a/browser/data-browser/package.json
+++ b/browser/data-browser/package.json
@@ -78,6 +78,6 @@
"preview": "vite preview",
"start": "vite",
"test": "jest",
- "typecheck": "pnpm exec tsc --noEmit"
+ "typecheck": "npx tsc --noEmit"
}
}
diff --git a/browser/data-browser/src/components/InlineFormattedResourceList.tsx b/browser/data-browser/src/components/InlineFormattedResourceList.tsx
index fabb5542f..fb6d5a700 100644
--- a/browser/data-browser/src/components/InlineFormattedResourceList.tsx
+++ b/browser/data-browser/src/components/InlineFormattedResourceList.tsx
@@ -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', {
@@ -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);
@@ -22,6 +25,10 @@ export function InlineFormattedResourceList({
return value;
}
+ if (RenderComp) {
+ return ;
+ }
+
return ;
})}
>
diff --git a/browser/data-browser/src/components/TableEditor/Cell.tsx b/browser/data-browser/src/components/TableEditor/Cell.tsx
index 6f2239e33..3f1568470 100644
--- a/browser/data-browser/src/components/TableEditor/Cell.tsx
+++ b/browser/data-browser/src/components/TableEditor/Cell.tsx
@@ -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(() => {
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx
index fa32c35f6..cd34847ae 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx
+++ b/browser/data-browser/src/views/TablePage/EditorCells/AtomicURLCell.tsx
@@ -5,36 +5,25 @@ 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';
@@ -42,12 +31,19 @@ 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(subject);
const classType = useResource(property.props.classtype);
- const hasClassType = classType?.getSubject() !== unknownSubject;
+ const hasClassType = classType?.subject !== unknownSubject;
return {
classType,
@@ -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) => {
- 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];
@@ -149,7 +128,7 @@ function AtomicURLCellEdit({
return (
{' '}
- {cell.getSubject() === unknownSubject
+ {cell.subject === unknownSubject
? `select ${hasClassType ? classType.title : 'resource'}`
: title}
@@ -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 (
-
+
{results.length > 0 && (
{results.map((result, index) => (
@@ -208,7 +187,7 @@ function AtomicURLCellEdit({
onFilesUploaded={handleFilesUploaded}
/>
)}
-
+
);
}
@@ -216,27 +195,11 @@ function AtomicURLCellEdit({
function AtomicURLCellDisplay({
value,
}: DisplayCellProps): 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 ;
-}
-
-function BasicCell({ resource }: ResourceCellProps) {
- const [title] = useTitle(resource);
-
- return {title};
+ return ;
}
interface ResultProps {
@@ -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);
@@ -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/');
@@ -301,10 +261,10 @@ function FileUploadContainer({
)}
{!isImage ? (
- {filename}
+ {filename}
) : null}
onChange(undefined)}>
-
+
);
@@ -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;
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx b/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx
new file mode 100644
index 000000000..f5a0a70c0
--- /dev/null
+++ b/browser/data-browser/src/views/TablePage/EditorCells/CellComponents.tsx
@@ -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;
+`;
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx
new file mode 100644
index 000000000..95945099e
--- /dev/null
+++ b/browser/data-browser/src/views/TablePage/EditorCells/MultiRelationCell.tsx
@@ -0,0 +1,300 @@
+import {
+ Core,
+ JSONValue,
+ unknownSubject,
+ urls,
+ useArray,
+ useResource,
+ useTitle,
+} from '@tomic/react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { styled } from 'styled-components';
+import {
+ InputStyled,
+ InputWrapper,
+} from '../../../components/forms/InputStyles';
+import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext';
+import { getIconForClass } from '../../FolderPage/iconMap';
+import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
+import { useResourceSearch } from './useResourceSearch';
+import { IconButton } from '../../../components/IconButton/IconButton';
+import {
+ KeyboardInteraction,
+ useCellOptions,
+} from '../../../components/TableEditor';
+import { InlineFormattedResourceList } from '../../../components/InlineFormattedResourceList';
+import { FaPlus, FaXmark } from 'react-icons/fa6';
+import {
+ AbsoluteCell,
+ PopoverTrigger,
+ SearchPopover,
+ SearchResultWrapper,
+} from './CellComponents';
+import { Row } from '../../../components/Row';
+import { CellOptions } from '../../../components/TableEditor/hooks/useCellOptions';
+import { Checkbox } from '../../../components/forms/Checkbox';
+import { ResourceCell } from './ResourceCells/ResourceCell';
+import { AtomicLink } from '../../../components/AtomicLink';
+
+const useClassType = (subject: string) => {
+ const property = useResource(subject);
+
+ const classType = useResource(property.props.classtype);
+ const hasClassType = classType?.subject !== unknownSubject;
+
+ return {
+ classType,
+ hasClassType,
+ };
+};
+
+function MultiRelationCellEdit({
+ value,
+ onChange,
+ property,
+}: EditCellProps): JSX.Element {
+ const val = Array.isArray(value) ? value : [];
+
+ const { classType, hasClassType } = useClassType(property);
+ const [open, setOpen] = useState(true);
+ const { setCursorMode, activeCellRef } = useTableEditorContext();
+ const selectedElement = useRef(null);
+
+ const [searchValue, setSearchValue] = useState('');
+
+ const cellOptions = useMemo((): CellOptions => {
+ const disabledKeyboardInteractions = new Set([
+ KeyboardInteraction.EditNextRow,
+ ]);
+
+ if (open) {
+ disabledKeyboardInteractions.add(KeyboardInteraction.ExitEditMode);
+ }
+
+ return {
+ disabledKeyboardInteractions,
+ hideActiveIndicator: true,
+ };
+ }, [val, open]);
+
+ useCellOptions(cellOptions);
+
+ const handleChange = useCallback((e: React.ChangeEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setSearchValue(e.target.value);
+ }, []);
+
+ const handleResultClick = useCallback(
+ (result: string) => {
+ if (!result) return;
+
+ if (val.includes(result)) {
+ onChange(val.filter(v => v !== result));
+ } else {
+ onChange([...val, result]);
+ }
+ },
+ [onChange, val],
+ );
+
+ const handleRemoveItem = (subject: string) => {
+ onChange(val.filter(v => v !== subject));
+ };
+
+ const handleOpenChange = useCallback(
+ (state: boolean) => {
+ setOpen(state);
+ },
+ [setCursorMode],
+ );
+
+ const { results, selectedIndex, handleKeyDown } = useResourceSearch(
+ searchValue,
+ hasClassType ? classType.subject : undefined,
+ setOpen,
+ handleResultClick,
+ );
+
+ const Trigger = useMemo(() => {
+ return (
+
+
+
+
+
+ );
+ }, []);
+
+ useEffect(() => {
+ if (!open) {
+ activeCellRef.current?.focus();
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (selectedElement.current) {
+ selectedElement.current.scrollIntoView({ block: 'nearest' });
+ }
+ }, [selectedIndex]);
+
+ const placehoder = hasClassType ? `Search ${classType.title}` : 'Search...';
+
+ const showNoResults =
+ results.length === 0 && classType.subject !== urls.classes.file;
+
+ return (
+
+
+ {(value as string[])?.map(subject => (
+
+ ))}
+
+
+
+
+
+ {results.length > 0 && (
+
+ {results.map((result, index) => (
+ -
+
+
+ ))}
+
+ )}
+ {showNoResults && 'No results'}
+
+
+
+
+ );
+}
+
+interface ResourceItemButtonProps {
+ subject: string;
+ onRemove: (subject: string) => void;
+}
+
+function ResourceItemButton({
+ subject,
+ onRemove,
+}: ResourceItemButtonProps): JSX.Element {
+ const resource = useResource(subject);
+
+ return (
+
+
+ {resource.title}
+
+ onRemove(subject)}
+ >
+
+
+
+ );
+}
+
+function MultiRelationCellDisplay({
+ value,
+}: DisplayCellProps): JSX.Element {
+ if (!value || !Array.isArray(value)) {
+ return <>>;
+ }
+
+ return (
+
+
+
+ );
+}
+
+interface ResultProps {
+ subject: string;
+ onClick: (subject: string) => void;
+ selected: boolean;
+}
+
+function Result({ subject, onClick, selected }: ResultProps) {
+ const resource = useResource(subject);
+ const [title] = useTitle(resource);
+ const [[classType]] = useArray(resource, urls.properties.isA);
+
+ const Icon = getIconForClass(classType);
+
+ return (
+ onClick(subject)} tabIndex={-1}>
+ undefined}>
+
+ {title}
+
+ );
+}
+
+export const MultiRelationCell: CellContainer = {
+ Edit: MultiRelationCellEdit,
+ Display: MultiRelationCellDisplay,
+};
+
+const ResourceItemButtonWrapper = styled.span`
+ display: inline-flex;
+ padding-inline: 1ch;
+ align-items: center;
+ border: 1px solid ${p => p.theme.colors.main};
+ color: ${p => p.theme.colors.mainDark};
+
+ border-radius: ${p => p.theme.radius};
+`;
+
+const ResultButton = styled.button`
+ display: flex;
+ width: 100%;
+ align-items: center;
+ gap: 0.5rem;
+ background: none;
+ border: none;
+ color: currentColor;
+ cursor: pointer;
+ padding: 0.3rem;
+ border-radius: ${p => p.theme.radius};
+ &:hover {
+ background: ${p => p.theme.colors.main};
+ color: white;
+
+ svg {
+ color: white;
+ }
+ }
+
+ svg {
+ color: ${p => p.theme.colors.textLight};
+ }
+`;
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/ResourceArrayCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/ResourceArrayCell.tsx
index 658760569..c1b9b6d21 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/ResourceArrayCell.tsx
+++ b/browser/data-browser/src/views/TablePage/EditorCells/ResourceArrayCell.tsx
@@ -1,271 +1,30 @@
-import {
- core,
- JSONValue,
- Store,
- useArray,
- useResource,
- useStore,
-} from '@tomic/react';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { FaPlus, FaTimes } from 'react-icons/fa';
-import * as RadixPopover from '@radix-ui/react-popover';
-import { styled } from 'styled-components';
-import { IconButton } from '../../../components/IconButton/IconButton';
-import { Popover } from '../../../components/Popover';
-import { SelectableTag, Tag } from '../../../components/Tag';
+import { JSONValue, useResource } from '@tomic/react';
import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
-import {
- InputStyled,
- InputWrapper,
-} from '../../../components/forms/InputStyles';
-import { Row } from '../../../components/Row';
-import { stringToSlug } from '../../../helpers/stringToSlug';
-import { loopingIndex } from '../../../helpers/loopingIndex';
-import { fadeIn } from '../../../helpers/commonAnimations';
-import {
- KeyboardInteraction,
- useCellOptions,
-} from '../../../components/TableEditor';
-import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext';
-import { CellOptions } from '../../../components/TableEditor/hooks/useCellOptions';
+import { getCategoryFromResource } from '../PropertyForm/categories';
+import { SelectCell } from './SelectCell';
+import { MultiRelationCell } from './MultiRelationCell';
-const TAG_SPACING = '0.5rem';
+function ResourceArrayCellEdit(props: EditCellProps): JSX.Element {
+ const propResource = useResource(props.property);
-const emptyArray: string[] = [];
-
-function buildListWithTitles(
- store: Store,
- subjects: string[],
- ignore: string[],
-): { subject: string; title: string }[] {
- return subjects
- .filter(v => !ignore.includes(v))
- .map(subject => {
- const resource = store.getResourceLoading(subject);
- const title = resource?.get(core.properties.shortname) ?? subject;
-
- return { subject, title: title as string };
- });
-}
-
-function ResourceArrayCellEdit({
- value,
- property,
- onChange,
-}: EditCellProps): JSX.Element {
- const val = (value as string[]) ?? emptyArray;
-
- const store = useStore();
- const propertyResource = useResource(property);
- const [allowsOnly] = useArray(propertyResource, core.properties.allowsOnly);
- const [query, setQuery] = useState('');
- const filteredTags = useMemo(() => {
- const listWithTitles = buildListWithTitles(store, allowsOnly, val);
-
- return listWithTitles
- .filter(v => v.title.includes(query))
- .map(ft => ft.subject);
- }, [store, allowsOnly, val, query]);
- const [open, setOpen] = useState(true);
- const [selectedIndex, setSelectedIndex] = useState(0);
-
- const { activeCellRef } = useTableEditorContext();
-
- const cellOptions = useMemo((): CellOptions => {
- const disabledKeyboardInteractions = new Set([
- KeyboardInteraction.EditNextRow,
- ]);
-
- if (open) {
- disabledKeyboardInteractions.add(KeyboardInteraction.ExitEditMode);
- }
-
- return {
- disabledKeyboardInteractions,
- hideActiveIndicator: true,
- };
- }, [val, open]);
-
- useCellOptions(cellOptions);
-
- // const listWithTitles = useMemo(
- // () => buildListWithTitles(store, allowsOnly, val),
- // [allowsOnly, val],
- // );
-
- const handleSearch = useCallback((e: React.ChangeEvent) => {
- setQuery(stringToSlug(e.target.value));
- setSelectedIndex(0);
- }, []);
-
- const handleAddTag = useCallback(
- (subject: string) => {
- onChange(Array.from(new Set([...val, subject])));
- },
- [val, onChange],
- );
-
- const handleRemoveTag = useCallback(
- (subject: string) => {
- onChange(val.filter(tagSubject => tagSubject !== subject));
- },
- [val, onChange],
- );
-
- const changeSelection = useCallback(
- (mod: number) => {
- setSelectedIndex(prev => loopingIndex(prev + mod, filteredTags.length));
- },
- [filteredTags],
- );
-
- useEffect(() => {
- if (!open) {
- activeCellRef.current?.focus();
- }
- }, [open]);
-
- const handleKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- switch (e.key) {
- case 'ArrowUp':
- e.preventDefault();
- changeSelection(-1);
- break;
- case 'ArrowDown':
- e.preventDefault();
- changeSelection(1);
- break;
- case 'Enter':
- e.preventDefault();
- handleAddTag(filteredTags[selectedIndex]);
- break;
- case 'Escape':
- e.preventDefault();
-
- setOpen(false);
- break;
- }
- },
- [changeSelection, filteredTags, selectedIndex, open],
- );
-
- return (
-
-
- {val.map(v => (
-
- handleRemoveTag(v)}
- >
-
-
-
- ))}
-
-
-
- }
- >
-
-
-
-
-
-
- {filteredTags.map((v, i) => (
-
- ))}
-
-
-
-
-
-
- );
-}
-
-function ResourceArrayCellDisplay({
- value,
-}: DisplayCellProps): JSX.Element {
- if (!value) {
- return <>>;
+ if (getCategoryFromResource(propResource) === 'select') {
+ return ;
+ } else {
+ return ;
}
-
- return (
-
- {(value as string[]).map(v => (
-
- ))}
-
- );
}
-const StyledIcon = styled(FaPlus)`
- animation: ${fadeIn} 0.1s ease-in-out;
- color: ${p => p.theme.colors.textLight};
-`;
+function ResourceArrayCellDisplay(
+ props: DisplayCellProps,
+): JSX.Element {
+ const property = useResource(props.property);
-const TagIconButton = styled(IconButton)`
- height: unset;
- width: unset;
- padding: unset;
-
- color: var(--tag-dark-color);
- background-blend-mode: lighten;
-
- &:not([disabled]):hover,
- &:not([disabled]):focus {
- transform: scale(1.2);
- background-color: unset;
+ if (getCategoryFromResource(property) === 'select') {
+ return ;
+ } else {
+ return ;
}
-`;
-
-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;
-`;
-
-const Content = styled.div`
- width: min(40ch, 90vh);
- border-radius: ${p => p.theme.radius};
-`;
-
-const ResultWrapper = styled.div`
- padding: ${p => p.theme.margin}rem;
-`;
-
-const SearchInputWrapper = styled(InputWrapper)`
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-`;
+}
export const ResourceArrayCell: CellContainer = {
Edit: ResourceArrayCellEdit,
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/AgentCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/AgentCell.tsx
index aefdcfab6..529632b53 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/AgentCell.tsx
+++ b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/AgentCell.tsx
@@ -1,11 +1,12 @@
-import { useTitle } from '@tomic/react';
+import { useResource, useTitle } from '@tomic/react';
import { complement, setLightness } from 'polished';
import { styled } from 'styled-components';
import { ResourceCellProps } from '../Type';
import { SimpleResourceLink } from './SimpleResourceLink';
-export function AgentCell({ resource }: ResourceCellProps) {
+export function AgentCell({ subject }: ResourceCellProps) {
+ const resource = useResource(subject);
const [title] = useTitle(resource);
return (
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/FileCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/FileCell.tsx
index 430df7961..075afd7a2 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/FileCell.tsx
+++ b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/FileCell.tsx
@@ -1,11 +1,12 @@
-import { properties, useString, useTitle } from '@tomic/react';
+import { properties, useResource, useString, useTitle } from '@tomic/react';
import { styled } from 'styled-components';
import { getFileIcon, imageMimeTypes } from '../../../../helpers/filetypes';
import { ResourceCellProps } from '../Type';
import { SimpleResourceLink } from './SimpleResourceLink';
-export function FileCell({ resource }: ResourceCellProps) {
+export function FileCell({ subject }: ResourceCellProps) {
+ const resource = useResource(subject);
const [title] = useTitle(resource);
const [mimeType] = useString(resource, properties.file.mimetype);
const [downloadUrl] = useString(resource, properties.file.downloadUrl);
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/ResourceCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/ResourceCell.tsx
new file mode 100644
index 000000000..586945625
--- /dev/null
+++ b/browser/data-browser/src/views/TablePage/EditorCells/ResourceCells/ResourceCell.tsx
@@ -0,0 +1,26 @@
+import { useResource, core, server, useTitle } from '@tomic/react';
+import { ResourceCellProps } from '../Type';
+import { AgentCell } from './AgentCell';
+import { FileCell } from './FileCell';
+import { SimpleResourceLink } from './SimpleResourceLink';
+
+export function ResourceCell({ subject }: ResourceCellProps) {
+ const resource = useResource(subject);
+
+ const Comp = resource.matchClass(
+ {
+ [core.classes.agent]: AgentCell,
+ [server.classes.file]: FileCell,
+ },
+ BasicCell,
+ );
+
+ return ;
+}
+
+function BasicCell({ subject }: ResourceCellProps) {
+ const resource = useResource(subject);
+ const [title] = useTitle(resource);
+
+ return {title};
+}
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx
new file mode 100644
index 000000000..1fa904b2d
--- /dev/null
+++ b/browser/data-browser/src/views/TablePage/EditorCells/SelectCell.tsx
@@ -0,0 +1,253 @@
+import {
+ core,
+ JSONValue,
+ Store,
+ useArray,
+ useResource,
+ useStore,
+} from '@tomic/react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { FaPlus } from 'react-icons/fa';
+import * as RadixPopover from '@radix-ui/react-popover';
+import { styled } from 'styled-components';
+import { IconButton } from '../../../components/IconButton/IconButton';
+import { Popover } from '../../../components/Popover';
+import { SelectableTag, Tag } from '../../../components/Tag';
+import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
+import {
+ InputStyled,
+ InputWrapper,
+} from '../../../components/forms/InputStyles';
+import { Row } from '../../../components/Row';
+import { stringToSlug } from '../../../helpers/stringToSlug';
+import { loopingIndex } from '../../../helpers/loopingIndex';
+import { fadeIn } from '../../../helpers/commonAnimations';
+import {
+ KeyboardInteraction,
+ useCellOptions,
+} from '../../../components/TableEditor';
+import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext';
+import { CellOptions } from '../../../components/TableEditor/hooks/useCellOptions';
+import { AbsoluteCell } from './CellComponents';
+import { FaXmark } from 'react-icons/fa6';
+
+const TAG_SPACING = '0.5rem';
+
+const emptyArray: string[] = [];
+
+function buildListWithTitles(
+ store: Store,
+ subjects: string[],
+ ignore: string[],
+): { subject: string; title: string }[] {
+ return subjects
+ .filter(v => !ignore.includes(v))
+ .map(subject => {
+ const resource = store.getResourceLoading(subject);
+ const title = resource?.get(core.properties.shortname) ?? subject;
+
+ return { subject, title: title as string };
+ });
+}
+
+function SelectCellEdit({
+ value,
+ property,
+ onChange,
+}: EditCellProps): JSX.Element {
+ const val = (value as string[]) ?? emptyArray;
+
+ const store = useStore();
+ const propertyResource = useResource(property);
+ const [allowsOnly] = useArray(propertyResource, core.properties.allowsOnly);
+ const [query, setQuery] = useState('');
+ const filteredTags = useMemo(() => {
+ const listWithTitles = buildListWithTitles(store, allowsOnly, val);
+
+ return listWithTitles
+ .filter(v => v.title.includes(query))
+ .map(ft => ft.subject);
+ }, [store, allowsOnly, val, query]);
+ const [open, setOpen] = useState(true);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const { activeCellRef } = useTableEditorContext();
+
+ const cellOptions = useMemo((): CellOptions => {
+ const disabledKeyboardInteractions = new Set([
+ KeyboardInteraction.EditNextRow,
+ ]);
+
+ if (open) {
+ disabledKeyboardInteractions.add(KeyboardInteraction.ExitEditMode);
+ }
+
+ return {
+ disabledKeyboardInteractions,
+ hideActiveIndicator: true,
+ };
+ }, [val, open]);
+
+ useCellOptions(cellOptions);
+
+ const handleSearch = useCallback((e: React.ChangeEvent) => {
+ setQuery(stringToSlug(e.target.value));
+ setSelectedIndex(0);
+ }, []);
+
+ const handleAddTag = useCallback(
+ (subject: string) => {
+ onChange(Array.from(new Set([...val, subject])));
+ },
+ [val, onChange],
+ );
+
+ const handleRemoveTag = useCallback(
+ (subject: string) => {
+ onChange(val.filter(tagSubject => tagSubject !== subject));
+ },
+ [val, onChange],
+ );
+
+ const changeSelection = useCallback(
+ (mod: number) => {
+ setSelectedIndex(prev => loopingIndex(prev + mod, filteredTags.length));
+ },
+ [filteredTags],
+ );
+
+ useEffect(() => {
+ if (!open) {
+ activeCellRef.current?.focus();
+ }
+ }, [open]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowUp':
+ e.preventDefault();
+ changeSelection(-1);
+ break;
+ case 'ArrowDown':
+ e.preventDefault();
+ changeSelection(1);
+ break;
+ case 'Enter':
+ e.preventDefault();
+ handleAddTag(filteredTags[selectedIndex]);
+ break;
+ case 'Escape':
+ e.preventDefault();
+
+ setOpen(false);
+ break;
+ }
+ },
+ [changeSelection, filteredTags, selectedIndex, open],
+ );
+
+ return (
+
+
+ {val.map(v => (
+
+ handleRemoveTag(v)}
+ >
+
+
+
+ ))}
+
+
+
+ }
+ >
+
+
+
+
+
+
+ {filteredTags.map((v, i) => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+function SelectCellDisplay({
+ value,
+}: DisplayCellProps): JSX.Element {
+ if (!value) {
+ return <>>;
+ }
+
+ return (
+
+ {(value as string[]).map(v => (
+
+ ))}
+
+ );
+}
+
+const StyledIcon = styled(FaPlus)`
+ animation: ${fadeIn} 0.1s ease-in-out;
+ color: ${p => p.theme.colors.textLight};
+`;
+
+const TagIconButton = styled(IconButton)`
+ height: unset;
+ width: unset;
+ padding: unset;
+
+ color: var(--tag-dark-color);
+ background-blend-mode: lighten;
+
+ &:not([disabled]):hover,
+ &:not([disabled]):focus {
+ transform: scale(1.2);
+ background-color: unset;
+ }
+`;
+
+const Content = styled.div`
+ width: min(40ch, 90vh);
+ border-radius: ${p => p.theme.radius};
+`;
+
+const ResultWrapper = styled.div`
+ padding: ${p => p.theme.margin}rem;
+`;
+
+const SearchInputWrapper = styled(InputWrapper)`
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+`;
+
+export const SelectCell: CellContainer = {
+ Edit: SelectCellEdit,
+ Display: SelectCellDisplay,
+};
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/Type.ts b/browser/data-browser/src/views/TablePage/EditorCells/Type.ts
index 1a2511ca0..714a409ab 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/Type.ts
+++ b/browser/data-browser/src/views/TablePage/EditorCells/Type.ts
@@ -19,5 +19,5 @@ export type CellContainer = {
};
export interface ResourceCellProps {
- resource: Resource;
+ subject: string;
}
diff --git a/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts b/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts
index 90ebe736c..e628eec18 100644
--- a/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts
+++ b/browser/data-browser/src/views/TablePage/EditorCells/useResourceSearch.ts
@@ -5,6 +5,7 @@ import { useSettings } from '../../../helpers/AppSettings';
export function useResourceSearch(
searchValue: string,
classType: string | undefined,
+ setOpen: (state: boolean) => void,
onResultPick: (result: string) => void,
) {
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -21,6 +22,17 @@ export function useResourceSearch(
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ setOpen(false);
+
+ return;
+ }
+
+ if (e.key === 'Tab') {
+ return;
+ }
+
e.stopPropagation();
if (e.key === 'ArrowUp') {
diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/EditPropertyDialog.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/EditPropertyDialog.tsx
index 069e1cf00..1b47c3f9d 100644
--- a/browser/data-browser/src/views/TablePage/PropertyForm/EditPropertyDialog.tsx
+++ b/browser/data-browser/src/views/TablePage/PropertyForm/EditPropertyDialog.tsx
@@ -1,6 +1,6 @@
-import { Resource, core, useString } from '@tomic/react';
+import { Resource } from '@tomic/react';
import { useCallback, useEffect, useState } from 'react';
-import { PropertyForm, getCategoryFromDatatype } from './PropertyForm';
+import { PropertyForm } from './PropertyForm';
import { FormValidationContextProvider } from '../../../components/forms/formValidation/FormValidationContextProvider';
import {
Dialog,
@@ -10,6 +10,7 @@ import {
useDialog,
} from '../../../components/Dialog';
import { Button } from '../../../components/Button';
+import { getCategoryFromResource } from './categories';
interface EditPropertyDialogProps {
resource: Resource;
@@ -24,9 +25,7 @@ export function EditPropertyDialog({
}: EditPropertyDialogProps): JSX.Element {
const [valid, setValid] = useState(true);
- const [datatype] = useString(resource, core.properties.datatype);
-
- const category = getCategoryFromDatatype(datatype);
+ const category = getCategoryFromResource(resource);
const onSuccess = useCallback(() => {
resource.save();
@@ -54,6 +53,7 @@ export function EditPropertyDialog({
;
}
diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/PropertyForm.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/PropertyForm.tsx
index c8f977076..723fcc7e8 100644
--- a/browser/data-browser/src/views/TablePage/PropertyForm/PropertyForm.tsx
+++ b/browser/data-browser/src/views/TablePage/PropertyForm/PropertyForm.tsx
@@ -1,4 +1,4 @@
-import { Resource, urls, useString } from '@tomic/react';
+import { core, Resource, useString } from '@tomic/react';
import { useCallback, useEffect, useMemo } from 'react';
import { styled } from 'styled-components';
import { ErrorChip } from '../../../components/forms/ErrorChip';
@@ -7,76 +7,20 @@ import {
InputStyled,
InputWrapper,
} from '../../../components/forms/InputStyles';
-import { buildComponentFactory } from '../../../helpers/buildComponentFactory';
import { stringToSlug } from '../../../helpers/stringToSlug';
-import { CheckboxPropertyForm } from './CheckboxPropertyForm';
-import { DatePropertyForm } from './DatePropertyForm';
-import { FilePropertyForm } from './FilePropertyForm';
-import { NumberPropertyForm } from './NumberPropertyForm';
-import { RelationPropertyForm } from './RelationPropertyForm';
-import { SelectPropertyForm } from './SelectPropertyForm';
-import { TextPropertyForm } from './TextPropertyForm';
-
-export type PropertyFormCategory =
- | 'text'
- | 'number'
- | 'date'
- | 'checkbox'
- | 'file'
- | 'select'
- | 'relation';
+import { categoryFormFactory, PropertyFormCategory } from './categories';
interface PropertyFormProps {
onSubmit: () => void;
resource: Resource;
category?: PropertyFormCategory;
+ existingProperty?: boolean;
}
-export const getCategoryFromDatatype = (
- datatype: string | undefined,
-): PropertyFormCategory => {
- switch (datatype) {
- case urls.datatypes.string:
- case urls.datatypes.markdown:
- case urls.datatypes.slug:
- return 'text';
- case urls.datatypes.integer:
- case urls.datatypes.float:
- return 'number';
- case urls.datatypes.boolean:
- return 'checkbox';
- case urls.datatypes.date:
- case urls.datatypes.timestamp:
- return 'date';
- case urls.datatypes.resourceArray:
- return 'select';
- case urls.datatypes.atomicUrl:
- return 'relation';
- }
-
- throw new Error(`Unknown datatype: ${datatype}`);
-};
-
-const NoCategorySelected = () => {
- return No Type selected;
-};
-
-const categoryFormFactory = buildComponentFactory(
- new Map([
- ['text', TextPropertyForm],
- ['number', NumberPropertyForm],
- ['checkbox', CheckboxPropertyForm],
- ['select', SelectPropertyForm],
- ['date', DatePropertyForm],
- ['file', FilePropertyForm],
- ['relation', RelationPropertyForm],
- ]),
- NoCategorySelected,
-);
-
export function PropertyForm({
resource,
onSubmit,
+ existingProperty,
category,
}: PropertyFormProps): JSX.Element {
const [nameError, setNameError, onNameBlur] = useValidation('Required');
@@ -95,12 +39,12 @@ export function PropertyForm({
const [name, setName] = useString(
resource,
- urls.properties.name,
+ core.properties.name,
valueOptions,
);
- const [_, setShortName] = useString(
+ const [shortname, setShortName] = useString(
resource,
- urls.properties.shortname,
+ core.properties.shortname,
valueOptions,
);
@@ -125,6 +69,11 @@ export function PropertyForm({
// If name was already set remove the error.
useEffect(() => {
+ if (existingProperty && !name && shortname) {
+ setName(shortname);
+ setNameError(undefined);
+ }
+
if (name) {
setNameError(undefined);
}
diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/RelationPropertyForm.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/RelationPropertyForm.tsx
index a5cfe610e..e0bf9ab62 100644
--- a/browser/data-browser/src/views/TablePage/PropertyForm/RelationPropertyForm.tsx
+++ b/browser/data-browser/src/views/TablePage/PropertyForm/RelationPropertyForm.tsx
@@ -3,9 +3,15 @@ import { useEffect } from 'react';
import { styled } from 'styled-components';
import { ResourceSelector } from '../../../components/forms/ResourceSelector';
import { PropertyCategoryFormProps } from './PropertyCategoryFormProps';
+import { Checkbox, CheckboxLabel } from '../../../components/forms/Checkbox';
const valueOpts = { commit: false };
+const RELATION_TYPES = new Set([
+ Datatype.RESOURCEARRAY,
+ Datatype.ATOMIC_URL,
+]);
+
export function RelationPropertyForm({
resource,
}: PropertyCategoryFormProps): JSX.Element {
@@ -15,9 +21,21 @@ export function RelationPropertyForm({
valueOpts,
);
+ const [datatype, setDatatype] = useString(resource, core.properties.datatype);
+
+ const handleAllowMultiple = (checked: boolean) => {
+ if (checked) {
+ setDatatype(Datatype.RESOURCEARRAY);
+ } else {
+ setDatatype(Datatype.ATOMIC_URL);
+ }
+ };
+
useEffect(() => {
- resource.set(core.properties.datatype, Datatype.ATOMIC_URL);
- }, []);
+ if (!RELATION_TYPES.has(resource.props.datatype)) {
+ setDatatype(Datatype.ATOMIC_URL);
+ }
+ }, [setDatatype]);
return (
<>
@@ -29,6 +47,13 @@ export function RelationPropertyForm({
setSubject={setClassType}
/>
+
+
+ Allow multiple values
+
>
);
}
diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/SelectPropertyForm.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/SelectPropertyForm.tsx
index ab4e4652d..0c82462f2 100644
--- a/browser/data-browser/src/views/TablePage/PropertyForm/SelectPropertyForm.tsx
+++ b/browser/data-browser/src/views/TablePage/PropertyForm/SelectPropertyForm.tsx
@@ -31,20 +31,13 @@ export function SelectPropertyForm({
valueOpts,
);
- const [subResources, setSubResources] = useArray(
- resource,
- dataBrowser.properties.subResources,
- valueOpts,
- );
-
const handleNewTag = useCallback(
async (tag: Resource) => {
await setAllowOnly([...allowOnly, tag.subject]);
- await setSubResources([...subResources, tag.subject]);
await tag.save();
},
- [allowOnly, setAllowOnly, subResources, setSubResources],
+ [allowOnly, setAllowOnly],
);
const handleDeleteTag = useCallback(
@@ -53,15 +46,15 @@ export function SelectPropertyForm({
tag.destroy();
await setAllowOnly(removeFromArray(allowOnly, subject));
- await setSubResources(removeFromArray(subResources, subject));
},
- [store, setAllowOnly, setSubResources, allowOnly, subResources],
+ [store, setAllowOnly, allowOnly],
);
useEffect(() => {
resource.addClasses(dataBrowser.classes.selectProperty);
resource.set(core.properties.datatype, Datatype.RESOURCEARRAY);
+ resource.set(core.properties.classtype, dataBrowser.classes.tag);
}, []);
return (
diff --git a/browser/data-browser/src/views/TablePage/PropertyForm/categories.tsx b/browser/data-browser/src/views/TablePage/PropertyForm/categories.tsx
new file mode 100644
index 000000000..baf096b0d
--- /dev/null
+++ b/browser/data-browser/src/views/TablePage/PropertyForm/categories.tsx
@@ -0,0 +1,82 @@
+import { Core, dataBrowser, Datatype, Resource, urls } from '@tomic/react';
+import { CheckboxPropertyForm } from './CheckboxPropertyForm';
+import { DatePropertyForm } from './DatePropertyForm';
+import { FilePropertyForm } from './FilePropertyForm';
+import { NumberPropertyForm } from './NumberPropertyForm';
+import { RelationPropertyForm } from './RelationPropertyForm';
+import { SelectPropertyForm } from './SelectPropertyForm';
+import { TextPropertyForm } from './TextPropertyForm';
+import { buildComponentFactory } from '../../../helpers/buildComponentFactory';
+
+export type PropertyFormCategory =
+ | 'text'
+ | 'number'
+ | 'date'
+ | 'checkbox'
+ | 'file'
+ | 'select'
+ | 'relation';
+
+const TEXT_TYPES = new Set([
+ Datatype.STRING,
+ Datatype.MARKDOWN,
+ Datatype.SLUG,
+]);
+const NUMBER_TYPES = new Set([Datatype.INTEGER, Datatype.FLOAT]);
+const DATE_TYPES = new Set([Datatype.DATE, Datatype.TIMESTAMP]);
+
+export const getCategoryFromResource = (
+ resource: Resource,
+): PropertyFormCategory => {
+ const datatype = resource.props.datatype;
+
+ if (TEXT_TYPES.has(datatype)) {
+ return 'text';
+ }
+
+ if (NUMBER_TYPES.has(datatype)) {
+ return 'number';
+ }
+
+ if (datatype === Datatype.BOOLEAN) {
+ return 'checkbox';
+ }
+
+ if (DATE_TYPES.has(datatype)) {
+ return 'date';
+ }
+
+ if (datatype === Datatype.RESOURCEARRAY) {
+ if (
+ resource.props.classtype === dataBrowser.classes.tag ||
+ resource.hasClasses(urls.classes.constraintProperties.selectProperty)
+ ) {
+ return 'select';
+ }
+
+ return 'relation';
+ }
+
+ if (datatype === Datatype.ATOMIC_URL) {
+ return 'relation';
+ }
+
+ throw new Error(`Unknown datatype: ${datatype}`);
+};
+
+const NoCategorySelected = () => {
+ return No Type selected;
+};
+
+export const categoryFormFactory = buildComponentFactory(
+ new Map([
+ ['text', TextPropertyForm],
+ ['number', NumberPropertyForm],
+ ['checkbox', CheckboxPropertyForm],
+ ['select', SelectPropertyForm],
+ ['date', DatePropertyForm],
+ ['file', FilePropertyForm],
+ ['relation', RelationPropertyForm],
+ ]),
+ NoCategorySelected,
+);