Skip to content

Commit

Permalink
#25 WIP Add columns
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Mar 3, 2023
1 parent f3a1b4b commit db864e0
Show file tree
Hide file tree
Showing 15 changed files with 312 additions and 54 deletions.
18 changes: 11 additions & 7 deletions data-browser/src/components/Dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { useDialog } from './useDialog';

export interface InternalDialogProps {
show: boolean;
onClose: () => void;
onClose: (success: boolean) => void;
onClosed: () => void;
}

Expand Down Expand Up @@ -65,6 +65,10 @@ export const Dialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
const innerDialogRef = useRef<HTMLDivElement>(null);
const portalRef = useContext(DialogPortalContext);

const cancelDialog = useCallback(() => {
onClose(false);
}, [onClose]);

const handleOutSideClick = useCallback<
React.MouseEventHandler<HTMLDialogElement>
>(
Expand All @@ -73,17 +77,17 @@ export const Dialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
!innerDialogRef.current?.contains(e.target as HTMLElement) &&
innerDialogRef.current !== e.target
) {
onClose();
cancelDialog();
}
},
[innerDialogRef.current],
[innerDialogRef.current, cancelDialog],
);

// Close the dialog when the escape key is pressed
useHotkeys(
'esc',
() => {
onClose();
cancelDialog();
},
{ enabled: show },
);
Expand Down Expand Up @@ -115,18 +119,18 @@ export const Dialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
onClosed();
}, ANIM_MS);
}
}, [show]);
}, [show, onClosed]);

if (!portalRef.current) {
return null;
}

return createPortal(
<StyledDialog ref={dialogRef} onClick={handleOutSideClick}>
<StyledDialog ref={dialogRef} onMouseDown={handleOutSideClick}>
<DialogTreeContext.Provider value={true}>
<StyledInnerDialog ref={innerDialogRef}>
<CloseButtonSlot slot='close'>
<Button icon onClick={onClose} aria-label='close'>
<Button icon onClick={cancelDialog} aria-label='close'>
<FaTimes />
</Button>
</CloseButtonSlot>
Expand Down
26 changes: 21 additions & 5 deletions data-browser/src/components/Dialog/useDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { InternalDialogProps } from './index';

export type UseDialogReturnType = [
Expand All @@ -7,28 +7,44 @@ export type UseDialogReturnType = [
/** Function to show the dialog */
show: () => void,
/** Function to close the dialog */
close: () => void,
close: (success?: boolean) => void,
/** Boolean indicating wether the dialog is currently open */
isOpen: boolean,
];

/** Sets up state, and functions to use with a {@link Dialog} */
export const useDialog = (): UseDialogReturnType => {
export const useDialog = (
bindShow?: React.Dispatch<boolean>,
onCancel?: () => void,
onSuccess?: () => void,
): UseDialogReturnType => {
const [showDialog, setShowDialog] = useState(false);
const [visible, setVisible] = useState(false);
const [wasSuccess, setWasSuccess] = useState(false);

const show = useCallback(() => {
setShowDialog(true);
setVisible(true);
bindShow?.(true);
}, []);

const close = useCallback(() => {
const close = useCallback((success = false) => {
setWasSuccess(success);
setShowDialog(false);
}, []);

const handleClosed = useCallback(() => {
bindShow?.(false);
setVisible(false);
}, []);

if (wasSuccess) {
onSuccess?.();
} else {
onCancel?.();
}

setWasSuccess(false);
}, [wasSuccess, onSuccess, onCancel]);

/** Props that should be passed to a {@link Dialog} component. */
const dialogProps = useMemo<InternalDialogProps>(
Expand Down
4 changes: 3 additions & 1 deletion data-browser/src/components/TableEditor/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export type TableHeadingComponent<T> = ({
export interface TableHeaderProps<T> {
columns: T[];
onResize: (index: number, size: string) => void;
onNewColumnClick?: React.MouseEventHandler<HTMLButtonElement>;
HeadingComponent: TableHeadingComponent<T>;
columnToKey: (column: T) => string;
}

export function TableHeader<T>({
columns,
onResize,
onNewColumnClick,
HeadingComponent,
columnToKey,
}: TableHeaderProps<T>): JSX.Element {
Expand All @@ -37,7 +39,7 @@ export function TableHeader<T>({
</TableHeading>
))}
<TableHeadingWrapper>
<IconButton title='Add column'>
<IconButton title='Add column' onClick={onNewColumnClick}>
<FaPlus />
</IconButton>
</TableHeadingWrapper>
Expand Down
7 changes: 5 additions & 2 deletions data-browser/src/components/TableEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface FancyTableProps<T> {
onClearRow?: (index: number) => void;
onClearCells?: (cells: CellIndex<T>[]) => void;
onCopyCommand?: (cells: CellIndex<T>[]) => Promise<CopyValue[][]>;
onNewColumnClick?: React.MouseEventHandler<HTMLButtonElement>;

HeadingComponent: TableHeadingComponent<T>;
}
Expand All @@ -35,7 +36,7 @@ interface RowProps {
style: React.CSSProperties;
}

type OnScroll = (props: ListOnScrollProps) => any;
type OnScroll = (props: ListOnScrollProps) => unknown;

export function FancyTable<T>(props: FancyTableProps<T>): JSX.Element {
return (
Expand All @@ -59,6 +60,7 @@ function FancyTableInner<T>({
onClearCells,
onClearRow,
onCopyCommand,
onNewColumnClick,
}: FancyTableProps<T>): JSX.Element {
const tableRef = useRef<HTMLDivElement>(null);
const scrollerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -126,9 +128,10 @@ function FancyTableInner<T>({
<PercentageInsanityFix>
<TableHeader
columns={columns}
onResize={resizeCell}
HeadingComponent={HeadingComponent}
columnToKey={columnToKey}
onResize={resizeCell}
onNewColumnClick={onNewColumnClick}
/>
<AutoSizeTamer>
<Autosizer disableWidth>{List}</Autosizer>
Expand Down
6 changes: 6 additions & 0 deletions data-browser/src/helpers/stringToSlug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function stringToSlug(str: string): string {
return str
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w-]+/g, '');
}
2 changes: 2 additions & 0 deletions data-browser/src/views/FolderPage/iconMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
FaFileAlt,
FaFileImport,
FaFolder,
FaHashtag,
FaHdd,
FaListAlt,
FaShareSquare,
Expand All @@ -31,6 +32,7 @@ const iconMap = new Map<string, IconType>([
[classes.class, FaCube],
[classes.property, FaCubes],
[classes.table, FaTable],
[classes.property, FaHashtag],
]);

export function getIconForClass(
Expand Down
134 changes: 134 additions & 0 deletions data-browser/src/views/TablePage/PropertyForm/NewPropertyDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Resource, Store, urls, useStore } from '@tomic/react';
import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '../../../components/Button';
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
useDialog,
} from '../../../components/Dialog';
import { randomString } from '../../../helpers/randomString';
import { PropertyForm } from './PropertyForm';

interface NewPropertyDialogProps {
showDialog: boolean;
tableClassResource: Resource;
bindShow: React.Dispatch<boolean>;
}

const createSubjectWithBase = (base: string) => {
const sepperator = base.endsWith('/') ? '' : '/';

return `${base}${sepperator}property-${randomString(8)}`;
};

const polulatePropertyWithDefaults = async (
property: Resource,
tableClass: Resource,
store: Store,
) => {
await property.set(urls.properties.isA, [urls.classes.property], store);
await property.set(urls.properties.parent, tableClass.getSubject(), store);
await property.set(urls.properties.shortname, 'new-column', store);
await property.set(urls.properties.name, 'New Column', store);
await property.set(urls.properties.description, 'A column in a table', store);
await property.set(urls.properties.datatype, urls.datatypes.string, store);
};

export function NewPropertyDialog({
showDialog,
tableClassResource,
bindShow,
}: NewPropertyDialogProps): JSX.Element {
const store = useStore();
const [resource, setResource] = useState<Resource | null>(null);

const handleUserCancelAction = useCallback(async () => {
try {
await resource?.destroy(store);
} finally {
// Server does not have this resource yet so it will nag at us. We set the state to null anyway.
setResource(null);
}
}, [resource, store]);

const handleUserSuccessAction = useCallback(async () => {
if (!resource) {
return;
}

await resource.save(store);
await store.notifyResourceManuallyCreated(resource);

const currentRecommends =
tableClassResource.get(urls.properties.recommends) ?? [];

await tableClassResource.set(
urls.properties.recommends,
[...(currentRecommends as string[]), resource.getSubject()],
store,
);

await tableClassResource.save(store);
setResource(null);
}, [resource, store, tableClassResource]);

const [dialogProps, show, hide] = useDialog(
bindShow,
handleUserCancelAction,
handleUserSuccessAction,
);

const createProperty = async () => {
const subject = createSubjectWithBase(tableClassResource.getSubject());
const propertyResource = store.getResourceLoading(subject, {
newResource: true,
});

await polulatePropertyWithDefaults(
propertyResource,
tableClassResource,
store,
);

setResource(propertyResource);
};

const handleCancelClick = useCallback(() => {
hide();
}, [hide]);

const handleCreateClick = useCallback(() => {
hide(true);
}, [hide]);

useEffect(() => {
if (showDialog) {
createProperty().then(() => {
show();
});
}
}, [showDialog]);

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

return (
<Dialog {...dialogProps}>
<DialogTitle>
<h1>New Column</h1>
</DialogTitle>
<DialogContent>
<PropertyForm resource={resource} />
</DialogContent>
<DialogActions>
<Button onClick={handleCancelClick} subtle>
Cancel
</Button>
<Button onClick={handleCreateClick}>Create</Button>
</DialogActions>
</Dialog>
);
}
Loading

0 comments on commit db864e0

Please sign in to comment.