Skip to content

Commit

Permalink
Merge pull request #351 from visdesignlab/345-remove-attr-menu-buttons
Browse files Browse the repository at this point in the history
Remove buttons from attribute menu
  • Loading branch information
NateLanza authored Apr 18, 2024
2 parents c7dbe0f + 8a187f4 commit 8bf9557
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 122 deletions.
47 changes: 47 additions & 0 deletions e2e-tests/app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable testing-library/prefer-screen-queries */
import { test, expect } from '@playwright/test';
import mockData from '../playwright/mock-data/simpsons/simpsons_data.json';
import mockAnnotations from '../playwright/mock-data/simpsons/simpsons_annotations.json';
import mockAltText from '../playwright/mock-data/simpsons/simpsons_alttxt.json';

test.beforeEach(async ({ page }) => {
await page.route('*/**/api/**', async (route) => {
const url = route.request().url();
let json;

if (url) {
if (url.includes('workspaces/Upset%20Examples/tables/simpsons/rows/?limit=9007199254740991')) {
json = mockData;
await route.fulfill({ json });
} else if (url.includes('workspaces/Upset%20Examples/tables/simpsons/annotations/')) {
json = mockAnnotations;
await route.fulfill({ json });
} else if (url.includes('alttxt')) {
json = mockAltText;
await route.fulfill({ json });
} else if (url.includes('workspaces/Upset%20Examples/sessions/table/193/state/')) {
await route.fulfill({ status: 200 });
} else {
await route.continue();
}
} else {
await route.abort();
}
});
});

test('Attribute Dropdown', async ({ page }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');

// Deseslect age and assert that it's removed from the plot
await page.getByLabel('Open attributes selection menu').click();
await page.getByLabel('Age').uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.locator('g:nth-child(8) > g > .css-1mk4luq-Y > rect')).toHaveCount(0);

// Reselect age and assert that it's added back to the plot
await page.getByLabel('Open attributes selection menu').click();
await page.getByLabel('Age').check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.locator('g:nth-child(8) > g > .css-1mk4luq-Y > rect')).toBeVisible();
});
254 changes: 132 additions & 122 deletions packages/app/src/components/AttributeDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {
Button,
Box,
Menu,
Checkbox,
FormControlLabel,
FormGroup,
Typography,
TableRow,
Table,
TableCell,
TableBody,
TableHead,
Container,
TextField,
Button,
Box,
Menu,
Checkbox,
FormControlLabel,
FormGroup,
Typography,
TableRow,
Table,
TableCell,
TableBody,
TableHead,
Container,
TextField,
} from "@mui/material"
import { useContext } from "react"
import { ProvenanceContext } from "./Root"
Expand All @@ -21,129 +21,139 @@ import { useRecoilValue } from "recoil";
import { useState } from "react";
import { CoreUpsetData } from "@visdesignlab/upset2-core";

/**
* Get the count of items that have a specific attribute.
* Does not count items with null or undefined values for this attribute.
* @param attribute - The attribute to count.
* @param data - The data object containing the items.
* @returns The count of items with the specified attribute value.
*/
const getAttributeItemCount = (attribute: string, data: CoreUpsetData) => {
let count = 0;
let count = 0;

Object.values(data.items).forEach((item) => {
Object.entries(item).forEach(([key, val]) => {
if (key === attribute) {
if (val) {
count++;
}
}
})
Object.values(data.items).forEach((item) => {
Object.entries(item).forEach(([key, val]) => {
if (key === attribute) {
if (val) {
count++;
}
}
})
})

return count;
return count;
}

/**
* Dropdown component for selecting attributes.
* @param props - The component props.
* @param props.anchorEl - The anchor element for the dropdown.
* @param props.close - Function to close the dropdown.
* @returns The AttributeDropdown component.
*/
export const AttributeDropdown = (props: {anchorEl: HTMLElement, close: () => void}) => {
const { provenance, actions } = useContext(ProvenanceContext);
const data = useRecoilValue(dataSelector);
const [ checked, setChecked ] = useState<any[]>(
(data) ?
provenance.getState().visibleAttributes.map(
(attr) => attr
):
[]
);
const { provenance, actions } = useContext(ProvenanceContext);
const data = useRecoilValue(dataSelector);
const [ checked, setChecked ] = useState<any[]>(
(data) ?
provenance.getState().visibleAttributes.map(
(attr) => attr
):
[]
);

const [ searchTerm, setSearchTerm ] = useState<string>("");
const [ searchTerm, setSearchTerm ] = useState<string>("");
const attributeItemCount: { [attr: string]: number } = {};

const attributeItemCount: { [attr: string]: number } = {};
data?.attributeColumns.forEach((attr) => {
attributeItemCount[attr] = getAttributeItemCount(attr,data);
})

data?.attributeColumns.forEach((attr) => {
attributeItemCount[attr] = getAttributeItemCount(attr,data);
})
/**
* Handle checkbox toggle: add or remove the attribute from the visible attributes
* and update the provenance state and plot.
* @param e - The event object.
*/
const handleToggle = (e: any) => {
const attr = e.labels[0].textContent;
let newChecked = [...checked];

const handleToggle = (e: any) => {
const attr = e.labels[0].textContent;
let newChecked = [...checked];
if (checked.includes(attr)) {
newChecked = checked.filter((a) => a !== attr);
} else {
newChecked.push(attr);
}

if (checked.includes(attr)) {
newChecked = checked.filter((a) => a !== attr);
} else {
newChecked.push(attr);
}
setChecked(newChecked);
actions.addMultipleAttributes(newChecked);
}

setChecked(newChecked);
}
/**
* Handle search input change.
* @param e - The event object.
*/
const handleSearchChange = (e: any) => {
setSearchTerm(e.target.value);
}

const handleSearchChange = (e: any) => {
setSearchTerm(e.target.value);
/**
* Get the rows to display in the table.
* @returns The filtered rows based on the search term.
*/
const getRows = () => {
if (data === undefined || data === null) {
return []
}
return data.attributeColumns.map((attr, index) => {
return {
id: index,
attribute: attr,
itemCount: getAttributeItemCount(attr,data)
}
}).filter((row) => row.attribute.toLowerCase().includes(searchTerm.toLowerCase()))
}

const getRows = () => {
if (data === undefined || data === null) {
return []
return (
<Menu
open={true}
onClose={props.close}
anchorEl={props.anchorEl}
sx={{ height: "450px" }}
>
<Container maxWidth="md" sx={{ height: "80%" }}>
<TextField
id="search"
type="search"
label="Search"
size="small"
value={searchTerm}
onChange={handleSearchChange}
sx={{ width: "100%" }}
/>
</Container>
<FormGroup sx={{ overflow: "auto" }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell align="right"># Items</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ getRows().map((row) => {
return (
<TableRow key={row.id}>
<TableCell>
<FormControlLabel checked={checked.includes(row.attribute)} control={<Checkbox />} label={row.attribute} onChange={(e) => handleToggle(e.target)}/>
</TableCell>
<TableCell><Typography>{row.itemCount}</Typography></TableCell>
</TableRow>
)
})
}
return data.attributeColumns.map((attr, index) => {
return {
id: index,
attribute: attr,
itemCount: getAttributeItemCount(attr,data)
}
}).filter((row) => row.attribute.toLowerCase().includes(searchTerm.toLowerCase()))
}

return (
<Menu
open={true}
onClose={props.close}
anchorEl={props.anchorEl}
sx={{ height: "450px" }}
>
<Container maxWidth="md" sx={{ height: "80%" }}>
<TextField
id="search"
type="search"
label="Search"
size="small"
value={searchTerm}
onChange={handleSearchChange}
sx={{ width: "100%" }}
/>
</Container>
<FormGroup sx={{ overflow: "auto" }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell align="right"># Items</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ getRows().map((row) => {
return (
<TableRow key={row.id}>
<TableCell>
<FormControlLabel checked={checked.includes(row.attribute)} control={<Checkbox />} label={row.attribute} onChange={(e) => handleToggle(e.target)}/>
</TableCell>
<TableCell><Typography>{row.itemCount}</Typography></TableCell>
</TableRow>
)
})
}
</TableBody>
</Table>
</FormGroup>
<Box sx={{display: 'flex', justifyContent: "space-between"}}>
<Button color="error" onClick={props.close}>
Cancel
</Button>
<Button
color="secondary"
onClick={() => {
if (data) {
const attrToAdd = [...checked];
actions.addMultipleAttributes(attrToAdd);
}
props.close();
}}
>
Submit
</Button>
</Box>
</Menu>
)
</TableBody>
</Table>
</FormGroup>
</Menu>
)
}

0 comments on commit 8bf9557

Please sign in to comment.