Skip to content

Commit

Permalink
feat: add attribute in nested table
Browse files Browse the repository at this point in the history
  • Loading branch information
emcelroy committed Sep 17, 2024
1 parent 2e11cab commit acd2f84
Show file tree
Hide file tree
Showing 17 changed files with 590 additions and 147 deletions.
33 changes: 33 additions & 0 deletions src/components/add-attribute-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.addAttributeButtonContainer {
display: inline-block;
margin-left: 10px;

.addAttributeButton {
all: unset;
align-items: center;
border-radius: 50%;
color: #333;
cursor: pointer;
display: flex;
font-size: 11px;
font-weight: bold;
height: 12px;
opacity: 0.5;

&:hover {
opacity: 1;
}

svg {
background: #333;
border-radius: 50%;
height: 12px;
margin-right: 5px;
width: 12px;

path {
fill: white;
}
}
}
}
28 changes: 28 additions & 0 deletions src/components/add-attribute-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { ICollection, ICollections } from "../types";

import AddIcon from "../assets/add-icon.svg";

import css from "./add-attribute-button.scss";

interface IProps {
collectionId: number;
collections: ICollections;
handleAddAttribute?: (collection: ICollection, attrName: string) => Promise<void>;
}

export const AddAttributeButton: React.FC<IProps> = ({collections, collectionId, handleAddAttribute}: IProps) => {

const handleAddAttributeToCollection = async () => {
const collection = collections.find((c) => c.id === collectionId);
collection && handleAddAttribute && await handleAddAttribute(collection, "");
};

return (
<div className={css.addAttributeButtonContainer}>
<button onClick={handleAddAttributeToCollection} className={css.addAttributeButton}>
<AddIcon /> Add Attribute to Collection
</button>
</div>
);
};
13 changes: 9 additions & 4 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function App() {
handleSelectDataSet: _handleSelectDataSet, handleUpdateAttributePosition,
handleAddCollection, handleAddAttribute, handleSetCollections, handleSelectSelf,
updateTitle, selectCODAPCases, listenForSelectionChanges,
handleCreateCollectionFromAttribute, handleUpdateCollections
handleCreateCollectionFromAttribute, renameAttribute
} = useCodapState();

useEffect(() => {
Expand All @@ -32,10 +32,14 @@ function App() {
updateInteractiveState({view});
}, [updateInteractiveState]);

const handleSelectDataSet = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
const handleSelectDataSet = useCallback((e: React.ChangeEvent<HTMLSelectElement>, defaultDisplayMode?: string) => {
const dataSetName = e.target.value;
_handleSelectDataSet(dataSetName);
updateInteractiveState({dataSetName});
const update: Partial<InteractiveState> = {dataSetName};
if (defaultDisplayMode) {
update.displayMode = defaultDisplayMode;
}
updateInteractiveState(update);
}, [_handleSelectDataSet, updateInteractiveState]);

const handleShowComponent = () => {
Expand Down Expand Up @@ -104,7 +108,8 @@ function App() {
handleUpdateAttributePosition={handleUpdateAttributePosition}
handleSetCollections={handleSetCollections}
handleCreateCollectionFromAttribute={handleCreateCollectionFromAttribute}
handleUpdateCollections={handleUpdateCollections}
handleAddAttribute={handleAddAttribute}
renameAttribute={renameAttribute}
/>
);

Expand Down
74 changes: 46 additions & 28 deletions src/components/draggable-table-tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { useTableTopScrollTopContext } from "../hooks/useTableScrollTop";
import { useCodapState } from "../hooks/useCodapState";
import { getAttribute } from "@concord-consortium/codap-plugin-api";
import { getCollectionById } from "../utils/apiHelpers";
import { PropsWithChildren } from "../types";
import { ICollection, ICollections, PropsWithChildren } from "../types";
import { EditableTableCell } from "./editable-table-cell";
import { AddAttributeButton } from "./add-attribute-button";
import { EditableTableHeader } from "./editable-table-header";

import AddIcon from "../assets/plus-level-1.svg";
import DropdownIcon from "../assets/dropdown-arrow-icon.svg";
Expand Down Expand Up @@ -38,10 +40,14 @@ interface DraggagleTableHeaderProps {
colSpan?: number;
dataSetName: string;
dataSetTitle: string;
editableHasFocus?: boolean;
isParent?: boolean;
attrId?: number;
renameAttribute: (collectionName: string, attrId: number, oldName: string, newName: string) => Promise<void>;
}

export const DraggagleTableHeader: React.FC<PropsWithChildren<DraggagleTableHeaderProps>> = (props) => {
const {collectionId, attrTitle, dataSetName, children} = props;
const {collectionId, attrTitle, dataSetName, editableHasFocus, children, isParent, attrId, renameAttribute} = props;
const {dragOverId, dragSide, handleDragStart, handleDragOver, handleOnDrop, handleDragEnter,
handleDragLeave, handleDragEnd} = useDraggableTableContext();
const {handleSortAttribute} = useCodapState();
Expand Down Expand Up @@ -69,8 +75,7 @@ export const DraggagleTableHeader: React.FC<PropsWithChildren<DraggagleTableHead
};
}, [showHeaderMenu, tableContainer]);


const handleShowHeaderMenu = (e: React.MouseEvent<HTMLTableHeaderCellElement>) => {
const handleShowHeaderMenu = (e: React.MouseEvent<HTMLTableCellElement | HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setShowHeaderMenu(!showHeaderMenu);
Expand All @@ -83,6 +88,7 @@ export const DraggagleTableHeader: React.FC<PropsWithChildren<DraggagleTableHead
handleSortAttribute(dataSetName, attribute.id, isDescending);
setShowHeaderMenu(false);
};

return (
<>
<th
Expand All @@ -97,19 +103,27 @@ export const DraggagleTableHeader: React.FC<PropsWithChildren<DraggagleTableHead
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragEnd={handleDragEnd}
onMouseEnter={() => setShowDropdownIcon(true)}
onMouseLeave={() => setShowDropdownIcon(false)}
onClick={handleShowHeaderMenu}
>
<div className={css.thChildContainer}>
<div>{children}</div>
{showDropdownIcon &&
<div className={css.dropdownIcon}>
<DropdownIcon
onClick={handleShowHeaderMenu}
className={css.dropdownIcon}
/>
</div>
<div className={`${css.thChildContainer} ${isParent ? css.isParent : ""}`}>
<button
onMouseEnter={() => setShowDropdownIcon(true)}
onMouseLeave={() => setShowDropdownIcon(false)}
onClick={handleShowHeaderMenu}
>
{String(children).match(/^newAttr/) && attrId
? <EditableTableHeader
attrId={attrId}
collectionName={String(children)}
collectionId={collectionId}
hasFocus={editableHasFocus}
renameAttribute={renameAttribute}
/>
: children}
</button>
{
<button className={css.dropdownIcon} onClick={handleShowHeaderMenu}>
{showDropdownIcon && <DropdownIcon className={css.dropdownIcon} />}
</button>
}
</div>
</th>
Expand All @@ -130,14 +144,16 @@ export const DraggagleTableHeader: React.FC<PropsWithChildren<DraggagleTableHead
};

interface DroppableTableHeaderProps {
collections: ICollections;
childCollectionId: number;
collectionId: number;
dataSetName: string;
handleAddAttribute?: (collection: ICollection, attrName: string) => Promise<void>;
}

export const DroppableTableHeader: React.FC<PropsWithChildren<DroppableTableHeaderProps>> = (props) => {
const {collectionId, children} = props;
const {dragOverId, handleDragOver, handleOnDrop, handleDragEnter,
handleDragLeave} = useDraggableTableContext();

const {childCollectionId, collectionId, collections, children, handleAddAttribute} = props;
const {dragOverId, handleDragOver, handleOnDrop, handleDragEnter, handleDragLeave} = useDraggableTableContext();
const id = `${collectionId}`;
const style = getStyle(id, dragOverId, "left");

Expand All @@ -150,7 +166,14 @@ export const DroppableTableHeader: React.FC<PropsWithChildren<DroppableTableHead
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
{children}
<div className={css.parentCollHeader}>
{children}
<AddAttributeButton
collectionId={childCollectionId}
collections={collections}
handleAddAttribute={handleAddAttribute}
/>
</div>
</th>
);
};
Expand All @@ -161,15 +184,12 @@ interface DraggagleTableDataProps {
caseId: string;
style?: React.CSSProperties;
isParent?: boolean;
resizeCounter?: number;
parentLevel?: number;
selectedDataSetName: string;
handleUpdateCollections: () => void;
}

export const DraggagleTableData: React.FC<PropsWithChildren<DraggagleTableDataProps>> = (props) => {
const {collectionId, attrTitle, children, caseId, isParent, resizeCounter, parentLevel=0,
selectedDataSetName, handleUpdateCollections} = props;
const {collectionId, attrTitle, children, caseId, isParent, parentLevel=0, selectedDataSetName} = props;
const {dragOverId, dragSide} = useDraggableTableContext();
const {style} = getIdAndStyle(collectionId, attrTitle, dragOverId, dragSide);
const {tableScrollTop, scrollY} = useTableTopScrollTopContext();
Expand Down Expand Up @@ -205,15 +225,13 @@ export const DraggagleTableData: React.FC<PropsWithChildren<DraggagleTableDataPr
}
return newTop;
}
// resizeCounter is a hack to force rerender of text positioning when window is resized
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableScrollTop, isParent, scrollY, parentLevel, resizeCounter]);
}, [tableScrollTop, isParent, scrollY, parentLevel]);

const EditableCell = () => {
return (
<EditableTableCell
attrTitle={attrTitle}
handleUpdateCollections={handleUpdateCollections}
caseId={caseId}
selectedDataSetName={selectedDataSetName}
>
Expand Down
18 changes: 18 additions & 0 deletions src/components/editable-table-cell.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
.editableTableCell {
height: 100%;
min-height: 16px;
width: 100%;

div:first-child {
height: 100%;
min-height: 16px;
width: 100%;

span:empty {
display: block;
height: 100%;
min-height: 16px;
width: 100%;
}
}

input {
background: white;
width: calc(100% - 8px);
}
}
17 changes: 11 additions & 6 deletions src/components/editable-table-cell.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import React, { ReactNode, useState } from "react";
import { Editable, EditablePreview, EditableInput } from "@chakra-ui/react";
import { updateCaseById } from "@concord-consortium/codap-plugin-api";
import { useCodapState } from "../hooks/useCodapState";

import css from "./editable-table-cell.scss";

interface IProps {
attrTitle: string;
caseId: string;
children: ReactNode;
handleUpdateCollections: () => void;
selectedDataSetName: string;
}

export const EditableTableCell = (props: IProps) => {
const { attrTitle, caseId, children, handleUpdateCollections, selectedDataSetName } = props;
const displayValue = String(children);
const { attrTitle, caseId, children } = props;
const { editCaseValue } = useCodapState();
const [displayValue, setDisplayValue] = useState(String(children));
const [editingValue, setEditingValue] = useState(displayValue);
const [isEditing, setIsEditing] = useState(false);

Expand All @@ -28,11 +28,16 @@ export const EditableTableCell = (props: IProps) => {
};

const handleSubmit = async (newValue: string) => {
if (newValue === displayValue) {
setIsEditing(false);
return;
}

try {
await updateCaseById(selectedDataSetName, caseId, {[attrTitle]: newValue});
await editCaseValue(newValue, caseId, attrTitle);
setDisplayValue(newValue);
setEditingValue(newValue);
setIsEditing(false);
handleUpdateCollections();
} catch (e) {
console.error("Case not updated: ", e);
}
Expand Down
64 changes: 64 additions & 0 deletions src/components/editable-table-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { ReactNode, useState } from "react";
import { Editable, EditablePreview, EditableInput } from "@chakra-ui/react";

import css from "./editable-table-cell.scss";

interface IProps {
attrId: number;
collectionName: string;
collectionId: number;
children?: ReactNode;
hasFocus?: boolean;
selectedDataSetName?: string;
renameAttribute: (collectionName: string, attrId: number, oldName: string, newName: string) => Promise<void>;
}

export const EditableTableHeader = (props: IProps) => {
const { attrId, collectionName, hasFocus, renameAttribute } = props;
const [displayValue, setDisplayValue] = useState(collectionName);
const [editingValue, setEditingValue] = useState(displayValue);
const [isEditing, setIsEditing] = useState(hasFocus);

const handleChangeValue = (newValue: string) => {
setEditingValue(newValue);
};

const handleCancel = () => {
setEditingValue(displayValue);
setIsEditing(false);
};

const handleSubmit = async (newValue: string) => {
if (newValue === displayValue) {
setIsEditing(false);
return;
}

try {
await renameAttribute(collectionName, attrId, displayValue, newValue);
setDisplayValue(newValue);
setEditingValue(newValue);
setIsEditing(false);
} catch (e) {
console.error("Case not updated: ", e);
}
};

return (
<div className={css.editableTableCell}>
<Editable
isPreviewFocusable={true}
onCancel={handleCancel}
onChange={handleChangeValue}
onEdit={() => setIsEditing(true)}
onSubmit={handleSubmit}
startWithEditView={hasFocus}
submitOnBlur={true}
value={isEditing ? editingValue : displayValue}
>
{!isEditing && <EditablePreview />}
<EditableInput />
</Editable>
</div>
);
};
Loading

0 comments on commit acd2f84

Please sign in to comment.