From 8ae641371a4ad28ded1fa3d9be7fe412eb39c16e Mon Sep 17 00:00:00 2001 From: Cannon Lock Date: Thu, 2 Nov 2023 11:02:38 -0500 Subject: [PATCH 01/52] Add table edit interface --- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-prod.yaml | 2 +- deps/web-components | 2 +- package.json | 4 + src/pages/maps/@id/edit/EditInterface.ts | 116 +++++ src/pages/maps/@id/edit/EditTable.ts | 181 +++++++ src/pages/maps/@id/edit/editMenu.module.sass | 40 ++ src/pages/maps/@id/edit/editTable.module.sass | 23 + src/pages/maps/@id/edit/index.page.ts | 38 ++ src/pages/maps/@id/edit/main.module.sass | 34 ++ src/pages/maps/@id/edit/map-interface.ts | 323 ++++++++++++ src/pages/maps/@id/edit/server.ts | 61 +++ src/pages/maps/@id/index.page.ts | 2 +- src/pages/maps/index.page.server.ts | 2 +- src/pages/maps/index.page.ts | 4 + yarn.lock | 460 +++++++++++++++++- 16 files changed, 1272 insertions(+), 22 deletions(-) create mode 100644 src/pages/maps/@id/edit/EditInterface.ts create mode 100644 src/pages/maps/@id/edit/EditTable.ts create mode 100644 src/pages/maps/@id/edit/editMenu.module.sass create mode 100644 src/pages/maps/@id/edit/editTable.module.sass create mode 100644 src/pages/maps/@id/edit/index.page.ts create mode 100644 src/pages/maps/@id/edit/main.module.sass create mode 100644 src/pages/maps/@id/edit/map-interface.ts create mode 100644 src/pages/maps/@id/edit/server.ts diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 4d10ee17..c47a3fc8 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -39,7 +39,7 @@ jobs: password: ${{ secrets.HARBOR_CLI_SECRET }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/build-prod.yaml b/.github/workflows/build-prod.yaml index bd32d304..faf02f7d 100644 --- a/.github/workflows/build-prod.yaml +++ b/.github/workflows/build-prod.yaml @@ -36,7 +36,7 @@ jobs: password: ${{ secrets.HARBOR_CLI_SECRET }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/deps/web-components b/deps/web-components index 64dde9d4..79b6a5d4 160000 --- a/deps/web-components +++ b/deps/web-components @@ -1 +1 @@ -Subproject commit 64dde9d4d0d2259fbd1efffb49c072ac9ae49ac7 +Subproject commit 79b6a5d4e8f7658b772cfcb33c2d6c2ebf400e48 diff --git a/package.json b/package.json index e19be15e..1eac5ca1 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,9 @@ "dependencies": { "@blueprintjs/core": "^4.14.1", "@blueprintjs/select": "4", + "@blueprintjs/table": "^4", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", "@lagunovsky/redux-react-router": "^3.2.0", "@loadable/component": "^5.14.1", "@macrostrat/api-utils": "workspace:*", @@ -65,6 +68,7 @@ "@mapbox/point-geometry": "^0.1.0", "@mapbox/tilebelt": "^1.0.2", "@mapbox/vector-tile": "^1.3.1", + "@mui/material": "^5.14.16", "@turf/bbox": "^6.5.0", "@turf/boolean-contains": "^6.5.0", "@turf/buffer": "^6.5.0", diff --git a/src/pages/maps/@id/edit/EditInterface.ts b/src/pages/maps/@id/edit/EditInterface.ts new file mode 100644 index 00000000..7ee38792 --- /dev/null +++ b/src/pages/maps/@id/edit/EditInterface.ts @@ -0,0 +1,116 @@ +import hyper from "@macrostrat/hyper"; +import { + MapAreaContainer, + MapView, + PanelCard, +} from "@macrostrat/map-interface"; +import { + Spinner, + Radio, + RadioGroup, + NonIdealState, + Collapse, + Switch, +} from "@blueprintjs/core"; +import { SETTINGS } from "~/map-interface/settings"; +import { useMapRef } from "@macrostrat/mapbox-react"; +import { ReactElement, ReactFragment, useEffect } from "react"; +import styles from "./editMenu.module.sass"; +import { MapNavbar } from "~/dev/map-layers/utils"; +import { useMemo, useState } from "react"; +import "~/styles/global.styl"; +import { Icon } from "@blueprintjs/core"; +import EditTable from "./EditTable"; + +const h = hyper.styled(styles); + +interface TableProps { + +} + +interface IconButtonProps { + icon: string; + name: string; + onClick: () => void; +} + +function IconButton({ icon, name, onClick }: IconButtonProps) { + return h("button.icon-button", {onClick: onClick}, + [ + h("div.icon-container", {}, [ + h(Icon, {icon: icon, size: 24}) + ]), + h("span.icon-label", {}, name) + ] + ); +} + +interface EditMenuProps { + setMenu: () => void; +} + +function EditMenu({setMenu}: EditMenuProps): ReactElement<{}> | ReactElement | ReactFragment { + return h( + "div.edit-menu", + {}, + [ + h(IconButton, {icon: "polygon-filter", name: "Polygons", onClick: () => setMenu("polygons")}), + ] + ) +} + +interface EditTableDrawerProps { + menu: string; +} + +function EditTableDrawer({menu, children}: EditTableDrawerProps){ + + const [maxWidth, setMaxWidth] = useState(0); + const [startPosition, setStartPosition] = useState(0); + + + useEffect(() => { + setMaxWidth(menu != undefined ? window.innerWidth / 2 : 0) + }, [menu]) + + return h( + "div.edit-table-drawer", + { style: { maxWidth: maxWidth + "px" }}, + [ + children, + h("div.width-adjuster", { + onDragStart: (e) => { + setStartPosition(e.clientX) + }, + onDragEnd: (e) => { + setMaxWidth(maxWidth + (e.clientX - startPosition)) + }, + draggable: true + }, []) + ] + ) +} + + +interface EditInterfaceProps { + title ?: string; + parentRoute ?: string; + source ?: number; +} + +export default function EditInterface({title, parentRoute, source_id} : EditInterfaceProps){ + + const [menu, setMenu] = useState(undefined); + + + + return h("div.interface", {}, [ + menu == undefined ? h(EditMenu, {setMenu}) : null, + h(EditTableDrawer, {menu}, + [ + menu == "polygons" ? h(EditTable, {url: `http://localhost:8000/sources/${source_id}/polygons`}, []) : null + ] + ) + ]) + +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/EditTable.ts b/src/pages/maps/@id/edit/EditTable.ts new file mode 100644 index 00000000..ce20371a --- /dev/null +++ b/src/pages/maps/@id/edit/EditTable.ts @@ -0,0 +1,181 @@ + +import hyper from "@macrostrat/hyper"; + +import { ReactElement, ReactFragment, useState, useEffect, useMemo } from "react"; +import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; +import { Spinner } from "@blueprintjs/core"; +import { Column, Table2, EditableCell2, RowHeaderCell2, SelectionModes } from "@blueprintjs/table"; +import {TablePagination} from "@mui/material" + +import "@blueprintjs/table/lib/css/table.css"; +import styles from "./editTable.module.sass"; + + +const h = hyper.styled(styles); + + + +const range = (start, stop, step = 1) => + Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step) + + +interface Selection { + cols: number[]; + rows: number[]; +} + +class Filter { + constructor(column_name: string, operator: string, value: string){ + this.column_name = column_name + this.operator = operator + this.value = value + } + to_object = () => { + let o = {} + o[this.column_name] = this.operator + "." + this.value + return o + } + + to_array = () => { + return [this.column_name, this.operator + "." + this.value] + } + +} + +interface Filters { + [key: string]: Filter; +} + +interface TableSelection { + columns: string[]; + filters: Filters +} + + +export default function EditTable({url}){ + + // Table values + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(999999); + const [totalCount, setTotalCount] = useState(0); + + const [data, setData] = useState(undefined); + const [dataToggle, setDataToggle] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [tableSelection, setTableSelection] = useState({columns: [], filters: []}) + + const cellRenderer = ({key, row, cell}) => { + + return h(EditableCell2, {onChange: (e) => console.log(), "value": data[row][key]}, []) + } + + let getData = async () => { + + let dataURL = new URL(url) + + dataURL.searchParams.append("page", page.toString()) + dataURL.searchParams.append("page_size", pageSize.toString()) + + let response = await fetch(dataURL) + let data = await response.json() + + setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))) + setData(data) + } + + useEffect(() => { + getData() + }, [page, pageSize, dataToggle]) + + + if(data == undefined){ + return h(Spinner) + } + + const columns = Object.keys(data[0]).filter(x => x != "db_id").map((key) => { + return h(Column, {name: key, cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), "key": key}) + }) + + const getSelectionValues = (selections: Selection[]) => { + + if(selections.length == 0){ + setTableSelection({columns: [], filters: {...tableSelection.filters, "tableSelection": undefined}}) + return + } + + const rows = selections[0]?.rows + const cols = selections[0]?.cols + + const columnsKeys = Object.keys(data[0]) + const selectedColumnKeys = columnsKeys.slice(cols[0], cols[1] + 1) + + let selection: TableSelection + if(rows == undefined){ + selection = {columns: selectedColumnKeys, ...tableSelection} + + } else { + const selectedRowIndices = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length) + const dbIds = selectedRowIndices.map((row) => data[row]['db_id']) + const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")") + + + selection = {columns: selectedColumnKeys, filters: {...tableSelection.filters, "tableSelection": filter}} + } + + setTableSelection(selection) + } + + const rowHeaderCellRenderer = (rowIndex: number) => { + return h(RowHeaderCell2, {name: data[rowIndex]['db_id']}, []) + } + + const submitChange = async (value: string) => { + for (const column of tableSelection.columns) { + + let updateURL = new URL(url) + + for(const filter: Filter of Object.values(tableSelection.filters)){ + updateURL.searchParams.append(...filter.to_array()) + } + + let patch = {[column]: value} + console.log(patch, JSON.stringify(patch)) + + + let response = await fetch(updateURL, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(patch), + }) + + if(response.status != 204){ + console.error("Failed to update", response) + } + } + setDataToggle(!dataToggle) + } + + + return h(HotkeysProvider, {}, [ + h("div.table-container", {}, [ + h("div.input-form", {}, [ + h(InputGroup, {"value": inputValue, className: "update-input-group", onChange: (e) => setInputValue(e.target.value)}), + h(Button, {type: "submit", onClick: () => submitChange(inputValue)}, ["Submit"]) + ]), + h(Table2, + { + selectionModes: SelectionModes.COLUMNS_AND_CELLS, + rowHeaderCellRenderer: rowHeaderCellRenderer, + onSelection: (selections: Selection[]) => getSelectionValues(selections), + numRows: data.length + }, + [ + columns + ] + ), + h(TablePagination, {component: "div", count: totalCount, rowsPerPage: pageSize, page: page, onRowsPerPageChange: (e) => setPageSize(e.target.value), onPageChange: (e, p) => setPage(p)}), + ]) + ]) +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/editMenu.module.sass b/src/pages/maps/@id/edit/editMenu.module.sass new file mode 100644 index 00000000..9f09adf1 --- /dev/null +++ b/src/pages/maps/@id/edit/editMenu.module.sass @@ -0,0 +1,40 @@ +div.interface + height: 100% + background-color: #efefef + border-radius: 10px + margin-right: auto + +button.icon-button + aspect-ratio: 1 / 1 + margin: 5px + display: flex + flex-direction: column + border-radius: 10px + .icon-container + display: flex + margin: auto + padding: 6px + .icon-label + padding-bottom: 10px + +div.edit-table-drawer + transition: max-width 0.1s ease-in-out + overflow: scroll + height: 100% + display: flex + flex-direction: row + padding-left: 10px + padding-bottom: 10px + padding-top: 10px + +div.edit-table-wrapper + overflow: scroll + width: 100% + +div.width-adjuster + cursor: col-resize + width: 10px + padding: 5px + height: 100% + &:hover + background-color: #efefef \ No newline at end of file diff --git a/src/pages/maps/@id/edit/editTable.module.sass b/src/pages/maps/@id/edit/editTable.module.sass new file mode 100644 index 00000000..7334fd26 --- /dev/null +++ b/src/pages/maps/@id/edit/editTable.module.sass @@ -0,0 +1,23 @@ +td + text-wrap: nowrap + background-color: #ffffff78 + +tr:nth-child(odd) td + background-color: #ffffff + +div.table-container + display: flex + flex-direction: column + overflow: auto + +div.interface + padding-left: 10px + padding-bottom: 10px + padding-top: 10px + +div.input-form + display: flex + flex-direction: row + +.update-input-group + flex-grow: 1 \ No newline at end of file diff --git a/src/pages/maps/@id/edit/index.page.ts b/src/pages/maps/@id/edit/index.page.ts new file mode 100644 index 00000000..a57c5e40 --- /dev/null +++ b/src/pages/maps/@id/edit/index.page.ts @@ -0,0 +1,38 @@ +import { PageContextBuiltInServer } from "vike/types"; +import { SETTINGS } from "~/map-interface/settings"; +import h from "@macrostrat/hyper"; +import { ClientOnly } from "~/renderer/client-only"; + +const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; + +export async function onBeforeRender(pageContext: PageContextBuiltInServer) { + + const { id } = pageContext.routeParams; + + const params = new URLSearchParams({ + format: "geojson", + source_id: id, + }); + const response = await fetch(apiAddress + "?" + params); + const data: any = await response.json(); + const map = data?.success?.data?.features[0]; + + return { + pageContext: { + pageProps: { + id, + map, + }, + documentProps: { + // The page's + title: map.properties.name, + }, + }, + }; +} + +const MapInterface = () => import("./map-interface"); + +export function Page({ id, map }) { + return h("div.single-map", h(ClientOnly, { component: MapInterface, id, map })); +} diff --git a/src/pages/maps/@id/edit/main.module.sass b/src/pages/maps/@id/edit/main.module.sass new file mode 100644 index 00000000..a2861296 --- /dev/null +++ b/src/pages/maps/@id/edit/main.module.sass @@ -0,0 +1,34 @@ +body + margin: 0 + padding: 0 + +.single-map + width: 100vh + height: 100vh + margin: 0 + --map-context-stack-width: 16em + +.map-legend-container + overflow-y: scroll + +.map-legend + margin: 1em + +.legend-entry + margin: 0.5em 0 + +.legend-title + display: flex + flex-direction: row + &:hover + cursor: pointer + background: #eee + h4 + margin: 0 + margin-right: 0.5em + +.legend-swatch + width: 1em + height: 1em + display: inline-block + margin-right: 0.5em \ No newline at end of file diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts new file mode 100644 index 00000000..7a09b063 --- /dev/null +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -0,0 +1,323 @@ +import hyper from "@macrostrat/hyper"; +import { + MapAreaContainer, + MapView, + PanelCard, +} from "@macrostrat/map-interface"; +import { + Spinner, + Radio, + RadioGroup, + NonIdealState, + Collapse, + Switch, +} from "@blueprintjs/core"; +import { SETTINGS } from "~/map-interface/settings"; +import { useMapRef } from "@macrostrat/mapbox-react"; +import { useEffect } from "react"; +import styles from "./main.module.sass"; +import { MapNavbar } from "~/dev/map-layers/utils"; +import { useMemo, useState } from "react"; +import "~/styles/global.styl"; +import boundingBox from "@turf/bbox"; +import { LngLatBoundsLike } from "mapbox-gl"; +import { buildMacrostratStyle } from "@macrostrat/mapbox-styles"; +import { getMapboxStyle, mergeStyles } from "@macrostrat/mapbox-utils"; +import { useDarkMode, useAPIResult, JSONView } from "@macrostrat/ui-components"; +import { InfoDrawerContainer, ExpansionPanel } from "@macrostrat/map-interface"; +import { MapMarker } from "@macrostrat/map-interface"; +import { NullableSlider } from "@macrostrat/ui-components"; +import { tempImageIndex, s3Address } from "../../raster-images"; +import EditInterface from "./EditInterface"; + +const h = hyper.styled(styles); + +function rasterURL(source_id) { + const image = tempImageIndex[source_id]; + if (image == null) return null; + return `${s3Address}/${image}`; +} + +interface StyleOpts { + style: string; + focusedMap: number; + layerOpacity: { + vector: number | null; + raster: number | null; + }; +} + +const emptyStyle: any = { + version: 8, + sprite: "mapbox://sprites/mapbox/bright-v9", + glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", + sources: {}, + layers: [], +}; + +function buildOverlayStyle({ + style, + focusedMap, + layerOpacity, +}: StyleOpts): any { + let mapStyle = emptyStyle; + if (layerOpacity.vector != null) { + mapStyle = buildMacrostratStyle({ + tileserverDomain: SETTINGS.burwellTileDomain, + focusedMap, + fillOpacity: layerOpacity.vector - 0.1, + strokeOpacity: layerOpacity.vector + 0.2, + lineOpacity: layerOpacity.vector + 0.4, + }); + } + + const raster = rasterURL(focusedMap); + if (raster != null && layerOpacity.raster != null) { + const rasterStyle = { + ...emptyStyle, + sources: { + raster: { + type: "raster", + tiles: [ + SETTINGS.burwellTileDomain + + "/cog/tiles/{z}/{x}/{y}.png?url=" + + raster, + ], + tileSize: 256, + }, + }, + layers: [ + { + id: "raster", + type: "raster", + source: "raster", + minzoom: 0, + maxzoom: 22, + layout: { + visibility: "visible", + }, + paint: { + "raster-opacity": layerOpacity.raster, + }, + }, + ], + }; + + mapStyle = mergeStyles(rasterStyle, mapStyle); + } + + if (style == null) { + return mapStyle; + } + + return mergeStyles(style, mapStyle); +} + +function ensureBoxInGeographicRange(bounds: LngLatBoundsLike) { + if (bounds[1] < -90) bounds[1] = -90; + if (bounds[3] > 90) bounds[3] = 90; + return bounds; +} + +enum Basemap { + Satellite = "satellite", + Basic = "basic", + None = "none", +} + +function basemapStyle(basemap, inDarkMode) { + switch (basemap) { + case Basemap.Satellite: + return SETTINGS.satelliteMapURL; + case Basemap.Basic: + return inDarkMode ? SETTINGS.darkMapURL : SETTINGS.baseMapURL; + case Basemap.None: + return null; + } +} + +export default function MapInterface({ id, map }) { + const [isOpen, setOpen] = useState(false); + const dark = useDarkMode()?.isEnabled ?? false; + const title = h([ + h("code", map.properties.source_id), + " ", + map.properties.name, + ]); + + const hasRaster = rasterURL(map.properties.source_id) != null; + + const bounds: LngLatBoundsLike = useMemo(() => { + return ensureBoxInGeographicRange(boundingBox(map.geometry)); + }, [map.geometry]); + + const [layer, setLayer] = useState(Basemap.None); + const [style, setStyle] = useState(null); + // Basemap style + useEffect(() => { + if (layer == null) setStyle(null); + const styleURL = basemapStyle(layer, dark); + getMapboxStyle(styleURL, { + access_token: SETTINGS.mapboxAccessToken, + }).then(setStyle); + }, [layer, dark]); + + const [selectedLocation, setSelectedLocation] = useState(null); + + const [layerOpacity, setLayerOpacity] = useState({ + vector: 0.5, + raster: 0.5, + }); + + // Overlay style + const [mapStyle, setMapStyle] = useState(null); + useEffect(() => { + setMapStyle( + buildOverlayStyle({ + style, + focusedMap: map.properties.source_id, + layerOpacity, + }) + ); + }, [ + map.properties.source_id, + style, + layerOpacity.raster == null, + layerOpacity.vector == null, + ]); + + // Layer opacity + useEffect(() => { + if (mapStyle == null) return; + const mergeLayers = buildOverlayStyle({ + style, + focusedMap: map.properties.source_id, + layerOpacity, + }).layers; + + for (const layer of mapStyle.layers) { + let mergeLayer = mergeLayers.find((l) => l.id == layer.id); + layer.layout ??= {}; + layer.paint ??= {}; + if (mergeLayer == null) { + layer.layout.visibility = "none"; + continue; + } else { + layer.layout.visibility = "visible"; + } + for (const prop in ["fill-opacity", "line-opacity", "raster-opacity"]) { + if (mergeLayer.paint[prop] != null) { + layer.paint[prop] = mergeLayer.paint[prop]; + } + } + setMapStyle({ ...mapStyle, layers: mergeLayers }); + } + }, [layerOpacity]); + + const maxBounds: LatLngBoundsLike = useMemo(() => { + const dx = bounds[2] - bounds[0]; + const dy = bounds[3] - bounds[1]; + const buf = 1 * Math.max(dx, dy); + + return ensureBoxInGeographicRange([ + bounds[0] - buf, + bounds[1] - buf, + bounds[2] + buf, + bounds[3] + buf, + ]); + }, [bounds]); + + if (bounds == null || mapStyle == null) return h(Spinner); + + const contextPanel = h(PanelCard, [ + h("div.vector-controls", [ + h("h3", "Vector map"), + h(OpacitySlider, { + opacity: layerOpacity.vector, + setOpacity(v) { + setLayerOpacity({ ...layerOpacity, vector: v }); + }, + }), + ]), + h.if(hasRaster)("div.raster-controls", [ + h("h3", "Raster map"), + h(OpacitySlider, { + opacity: layerOpacity.raster, + setOpacity(v) { + setLayerOpacity({ ...layerOpacity, raster: v }); + }, + }), + ]), + h(BaseLayerSelector, { layer, setLayer }), + ]); + + return h( + MapAreaContainer, + { + className: "single-map", + navbar: h(EditInterface, {title:"Source 1", parentRoute:"/maps/", source_id:id}, []), + contextPanel, + contextPanelOpen: isOpen + }, + [ + h( + MapView, + { + style: mapStyle, //"mapbox://styles/mapbox/satellite-v9", + mapboxToken: SETTINGS.mapboxAccessToken, + //projection: { name: "globe" }, + bounds, + mapPosition: null, + maxBounds, + fitBoundsOptions: { padding: 50 }, + infoMarkerPosition: selectedLocation, + }, + [ + h(MapMarker, { + position: selectedLocation, + setPosition(lnglat) { + setSelectedLocation(lnglat); + }, + }), + ] + //[h(FitBoundsManager, { bounds })] + ), + ] + ); +} + +function BaseLayerSelector({ layer, setLayer }) { + return h("div.base-layer-selector", [ + h("h3", "Base layer"), + h( + RadioGroup, + { + selectedValue: layer, + onChange(e) { + setLayer(e.currentTarget.value); + }, + }, + [ + h(Radio, { label: "Satellite", value: Basemap.Satellite }), + h(Radio, { label: "Basic", value: Basemap.Basic }), + h(Radio, { label: "None", value: Basemap.None }), + ] + ), + ]); +} + + +function OpacitySlider(props) { + return h("div.opacity-slider", [ + h(NullableSlider, { + value: props.opacity, + min: 0.1, + max: 1, + labelStepSize: 0.2, + stepSize: 0.1, + onChange(v) { + props.setOpacity(v); + }, + }), + ]); +} diff --git a/src/pages/maps/@id/edit/server.ts b/src/pages/maps/@id/edit/server.ts new file mode 100644 index 00000000..d4dd5fb8 --- /dev/null +++ b/src/pages/maps/@id/edit/server.ts @@ -0,0 +1,61 @@ + +interface Error { + code: number; + text: string; +} + + +interface Patch { + id: number; + function: () => Response; + error: Error; + attempts: number; +} + + +// Holds the current state of the patching process +class PatchManager { + constructor() { + this.active = [] + this.success = [] + this.failed = [] + this.abandoned = [] + } + + // Wraps the patch function and handles in the context of the manager + async runPatch (patch: Patch) { + + patch.attempts += 1 + + try { + let response = await patch.function() + + if(response.status == 400){ + patch.error.code = response.status + patch.error.text = (await response.json())['details'] + this.abandoned.push(patch) + + } else if(response.status == 200){ + this.success.push(patch) + + } else { + this.failed.push(patch) + + } + } catch (e) { + patch.error.code = response.status + patch.error.text = (await response.json())['details'] + this.failed.push(patch) + } + } +} + + +async function getTable(){ + +} + + +async function patchTable(value, db_id){ + +} \ No newline at end of file diff --git a/src/pages/maps/@id/index.page.ts b/src/pages/maps/@id/index.page.ts index f824513c..e8c0f428 100644 --- a/src/pages/maps/@id/index.page.ts +++ b/src/pages/maps/@id/index.page.ts @@ -3,7 +3,7 @@ import { SETTINGS } from "~/map-interface/settings"; import h from "@macrostrat/hyper"; import { ClientOnly } from "~/renderer/client-only"; -const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; +const apiAddress = "http://localhost:5001" + "/v2/defs/sources"; export async function onBeforeRender(pageContext: PageContextBuiltInServer) { const { id } = pageContext.routeParams; diff --git a/src/pages/maps/index.page.server.ts b/src/pages/maps/index.page.server.ts index baa86d09..b6edf7f0 100644 --- a/src/pages/maps/index.page.server.ts +++ b/src/pages/maps/index.page.server.ts @@ -1,7 +1,7 @@ import fetch from "node-fetch"; import { SETTINGS } from "~/map-interface/settings"; -const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; +const apiAddress = "http://localhost:5001" + "/v2/defs/sources"; export async function onBeforeRender(pageContext) { // `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here. diff --git a/src/pages/maps/index.page.ts b/src/pages/maps/index.page.ts index a65ad0ef..e008dbb8 100644 --- a/src/pages/maps/index.page.ts +++ b/src/pages/maps/index.page.ts @@ -2,6 +2,7 @@ import hyper from "@macrostrat/hyper"; // Page for a list of maps import styles from "./main.module.sass"; import { tempImageIndex, s3Address } from "./raster-images"; +import { Icon, IconSize } from "@blueprintjs/core"; const h = hyper.styled(styles); @@ -26,6 +27,7 @@ export function Page({ sources }) { function SourceItem({ source }) { const { source_id, name } = source; const href = `/maps/${source_id}`; + const edit_href = `/maps/${source_id}/edit`; return h("li", [ h("span.source-id", {}, source_id), " ", @@ -33,5 +35,7 @@ function SourceItem({ source }) { " ", h("span.scale", {}, source.scale), h.if(source.rasterURL != null)([" ", h("span.raster", "Raster")]), + " ", + h("a", { href: edit_href }, [h(Icon, { icon: "edit", size: IconSize.SMALL })]), ]); } diff --git a/yarn.lock b/yarn.lock index d190728f..64e704cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,7 +241,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: @@ -1622,6 +1622,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2": + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -1698,7 +1707,7 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": +"@blueprintjs/colors@npm:^4.0.0-alpha.3, @blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": version: 4.2.1 resolution: "@blueprintjs/colors@npm:4.2.1" dependencies: @@ -1707,7 +1716,33 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": +"@blueprintjs/core@npm:^3.52.0": + version: 3.54.0 + resolution: "@blueprintjs/core@npm:3.54.0" + dependencies: + "@blueprintjs/colors": ^4.0.0-alpha.3 + "@blueprintjs/icons": ^3.33.0 + "@juggle/resize-observer": ^3.3.1 + "@types/dom4": ^2.0.1 + classnames: ^2.2 + dom4: ^2.1.5 + normalize.css: ^8.0.1 + popper.js: ^1.16.1 + react-lifecycles-compat: ^3.0.4 + react-popper: ^1.3.7 + react-transition-group: ^2.9.0 + tslib: ~2.3.1 + peerDependencies: + react: ^15.3.0 || 16 || 17 + react-dom: ^15.3.0 || 16 || 17 + bin: + upgrade-blueprint-2.0.0-rename: scripts/upgrade-blueprint-2.0.0-rename.sh + upgrade-blueprint-3.0.0-rename: scripts/upgrade-blueprint-3.0.0-rename.sh + checksum: 97b8811bfc32284bb36e62a44210e84d5abe164ef553670866e0628718db4a98c79b9665f73014b1474f534a3d3260e94af274e669fb0ebfeb323305a81b5375 + languageName: node + linkType: hard + +"@blueprintjs/core@npm:^3||^4, @blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": version: 4.20.2 resolution: "@blueprintjs/core@npm:4.20.2" dependencies: @@ -1755,6 +1790,16 @@ __metadata: languageName: node linkType: hard +"@blueprintjs/icons@npm:^3.33.0": + version: 3.33.0 + resolution: "@blueprintjs/icons@npm:3.33.0" + dependencies: + classnames: ^2.2 + tslib: ~2.3.1 + checksum: 9b1485a3ce17a97596b7fa7276ddbe85e33c56f061358351a626d353bf3eab6ab1b36a1860aec2feb7933ef0293c5f8e1f3342a89051720d1953343aab753cb3 + languageName: node + linkType: hard + "@blueprintjs/icons@npm:^4.14.5, @blueprintjs/icons@npm:^4.16.0": version: 4.16.0 resolution: "@blueprintjs/icons@npm:4.16.0" @@ -1766,6 +1811,23 @@ __metadata: languageName: node linkType: hard +"@blueprintjs/popover2@npm:^0.12.9": + version: 0.12.9 + resolution: "@blueprintjs/popover2@npm:0.12.9" + dependencies: + "@blueprintjs/core": ^3.52.0 + "@popperjs/core": ^2.5.4 + classnames: ^2.2 + dom4: ^2.1.5 + react-popper: ^2.2.4 + resize-observer-polyfill: ^1.5.1 + tslib: ~1.13.0 + peerDependencies: + react: ^16.8.0 || ^17 + checksum: f782600f3cd372abbdd47b9ca3ab9265c1e62e4b3b353d4b06c11c3663d62b01d597df11ba21aff9885a825c7960af878d8448922944489de4bc80c5019981de + languageName: node + linkType: hard + "@blueprintjs/popover2@npm:^1.13.12, @blueprintjs/popover2@npm:^1.14.11": version: 1.14.11 resolution: "@blueprintjs/popover2@npm:1.14.11" @@ -1807,6 +1869,27 @@ __metadata: languageName: node linkType: hard +"@blueprintjs/table@npm:^4": + version: 4.10.12 + resolution: "@blueprintjs/table@npm:4.10.12" + dependencies: + "@blueprintjs/core": ^4.20.2 + "@blueprintjs/popover2": ^1.14.11 + classnames: ^2.3.1 + prop-types: ^15.7.2 + react-innertext: ^1.1.5 + tslib: ~2.5.0 + peerDependencies: + "@types/react": ^16.14.32 || 17 || 18 + react: ^16.8 || 17 || 18 + react-dom: ^16.8 || 17 || 18 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: f38c20fe0c88bd1aa5a1fcd7b2c72c2678e47e9f44cf461c5ec81570a2a7c9035d918a4a026c530e1b6f2f6c304b22763674923f832baa60fc95cd4dabc82e8f + languageName: node + linkType: hard + "@brillout/import@npm:0.2.3, @brillout/import@npm:^0.2.3": version: 0.2.3 resolution: "@brillout/import@npm:0.2.3" @@ -2129,6 +2212,25 @@ __metadata: languageName: node linkType: hard +"@emotion/babel-plugin@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/babel-plugin@npm:11.11.0" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/serialize": ^1.1.2 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: 6b363edccc10290f7a23242c06f88e451b5feb2ab94152b18bb8883033db5934fb0e421e2d67d09907c13837c21218a3ac28c51707778a54d6cd3706c0c2f3f9 + languageName: node + linkType: hard + "@emotion/cache@npm:11.7.1": version: 11.7.1 resolution: "@emotion/cache@npm:11.7.1" @@ -2154,7 +2256,7 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.7.1": +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.7.1": version: 11.11.0 resolution: "@emotion/cache@npm:11.11.0" dependencies: @@ -2215,6 +2317,15 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^1.2.1": + version: 1.2.1 + resolution: "@emotion/is-prop-valid@npm:1.2.1" + dependencies: + "@emotion/memoize": ^0.8.1 + checksum: 8f42dc573a3fad79b021479becb639b8fe3b60bdd1081a775d32388bca418ee53074c7602a4c845c5f75fa6831eb1cbdc4d208cc0299f57014ed3a02abcad16a + languageName: node + linkType: hard + "@emotion/memoize@npm:0.7.4": version: 0.7.4 resolution: "@emotion/memoize@npm:0.7.4" @@ -2259,6 +2370,27 @@ __metadata: languageName: node linkType: hard +"@emotion/react@npm:^11.11.1": + version: 11.11.1 + resolution: "@emotion/react@npm:11.11.1" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/cache": ^11.11.0 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: aec3c36650f5f0d3d4445ff44d73dd88712b1609645b6af3e6d08049cfbc51f1785fe13dea1a1d4ab1b0800d68f2339ab11e459687180362b1ef98863155aae5 + languageName: node + linkType: hard + "@emotion/serialize@npm:1.0.2": version: 1.0.2 resolution: "@emotion/serialize@npm:1.0.2" @@ -2285,7 +2417,7 @@ __metadata: languageName: node linkType: hard -"@emotion/serialize@npm:^1.0.2": +"@emotion/serialize@npm:^1.0.2, @emotion/serialize@npm:^1.1.2": version: 1.1.2 resolution: "@emotion/serialize@npm:1.1.2" dependencies: @@ -2312,6 +2444,26 @@ __metadata: languageName: node linkType: hard +"@emotion/styled@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/styled@npm:11.11.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/is-prop-valid": ^1.2.1 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 904f641aad3892c65d7d6c0808b036dae1e6d6dad4861c1c7dc0baa59977047c6cad220691206eba7b4059f1a1c6e6c1ef4ebb8c829089e280fa0f2164a01e6b + languageName: node + linkType: hard + "@emotion/stylis@npm:0.8.5": version: 0.8.5 resolution: "@emotion/stylis@npm:0.8.5" @@ -2333,6 +2485,15 @@ __metadata: languageName: node linkType: hard +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": + version: 1.0.1 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" + peerDependencies: + react: ">=16.8.0" + checksum: 700b6e5bbb37a9231f203bb3af11295eed01d73b2293abece0bc2a2237015e944d7b5114d4887ad9a79776504aa51ed2a8b0ddbc117c54495dd01a6b22f93786 + languageName: node + linkType: hard + "@emotion/utils@npm:0.11.3": version: 0.11.3 resolution: "@emotion/utils@npm:0.11.3" @@ -2694,6 +2855,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.4.2": + version: 1.5.0 + resolution: "@floating-ui/core@npm:1.5.0" + dependencies: + "@floating-ui/utils": ^0.1.3 + checksum: 54b4fe26b3c228746ac5589f97303abf158b80aa5f8b99027259decd68d1c2030c4c637648ebd33dfe78a4212699453bc2bd7537fd5a594d3bd3e63d362f666f + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.5.1": + version: 1.5.3 + resolution: "@floating-ui/dom@npm:1.5.3" + dependencies: + "@floating-ui/core": ^1.4.2 + "@floating-ui/utils": ^0.1.3 + checksum: 00053742064aac70957f0bd5c1542caafb3bfe9716588bfe1d409fef72a67ed5e60450d08eb492a77f78c22ed1ce4f7955873cc72bf9f9caf2b0f43ae3561c21 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.2": + version: 2.0.2 + resolution: "@floating-ui/react-dom@npm:2.0.2" + dependencies: + "@floating-ui/dom": ^1.5.1 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 4797e1f7a19c1e531ed0d578ccdcbe58970743e5a480ba30424857fc953063f36d481f8c5d69248a8f1d521b739e94bf5e1ffb35506400dea3d914f166ed2f7f + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.1.3": + version: 0.1.6 + resolution: "@floating-ui/utils@npm:0.1.6" + checksum: b34d4b5470869727f52e312e08272edef985ba5a450a76de0917ba0a9c6f5df2bdbeb99448e2c60f39b177fb8981c772ff1831424e75123471a27ebd5b52c1eb + languageName: node + linkType: hard + "@hypnosphi/create-react-context@npm:^0.3.1": version: 0.3.1 resolution: "@hypnosphi/create-react-context@npm:0.3.1" @@ -3316,15 +3515,12 @@ __metadata: version: 0.0.0-use.local resolution: "@macrostrat/data-sheet@workspace:deps/web-components/packages/data-sheet" dependencies: - "@blueprintjs/core": ^4.17.8 - "@blueprintjs/popover2": ^1.13.12 + "@blueprintjs/core": ^3||^4 + "@blueprintjs/popover2": ^0.12.9 "@macrostrat/hyper": ^1.2.12 - chroma-js: ^2.4.2 - classnames: ^2.3.1 d3-array: ^2.12.1 immutability-helper: ^3.1.1 - react: ^17.0.2||^18 - react-color: ^2.19.3 + react: ^17.0.2 react-datasheet: ^1.4.9 react-dnd: ^14.0.2 react-dnd-html5-backend: ^14.0.0 @@ -3674,6 +3870,9 @@ __metadata: "@babel/preset-typescript": ^7.18.6 "@blueprintjs/core": ^4.14.1 "@blueprintjs/select": 4 + "@blueprintjs/table": ^4 + "@emotion/react": ^11.11.1 + "@emotion/styled": ^11.11.0 "@lagunovsky/redux-react-router": ^3.2.0 "@loadable/component": ^5.14.1 "@macrostrat/api-utils": "workspace:*" @@ -3694,6 +3893,7 @@ __metadata: "@mapbox/tilebelt": ^1.0.2 "@mapbox/vector-tile": ^1.3.1 "@mdx-js/rollup": ^2.3.0 + "@mui/material": ^5.14.16 "@turf/bbox": ^6.5.0 "@turf/boolean-contains": ^6.5.0 "@turf/buffer": ^6.5.0 @@ -4055,6 +4255,164 @@ __metadata: languageName: node linkType: hard +"@mui/base@npm:5.0.0-beta.22": + version: 5.0.0-beta.22 + resolution: "@mui/base@npm:5.0.0-beta.22" + dependencies: + "@babel/runtime": ^7.23.2 + "@floating-ui/react-dom": ^2.0.2 + "@mui/types": ^7.2.8 + "@mui/utils": ^5.14.16 + "@popperjs/core": ^2.11.8 + clsx: ^2.0.0 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8677c13174bd7cb79663f1356d4d3f950eb4ec7561480beb586309b84be3b1fc72db6ef59c45539a2d8a8ed610aefc60bdbf32db7f0db6201e5753c953f44e2d + languageName: node + linkType: hard + +"@mui/core-downloads-tracker@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/core-downloads-tracker@npm:5.14.16" + checksum: 26d691d20eabc7f0f23d4fc4bc21f247dde43ab86c5f3e57201ed63afbb67dba94e0b0e80cb8f2f0e6f6e1ba94e46853f0ca6ab593d558a749bde843ec1d4aa6 + languageName: node + linkType: hard + +"@mui/material@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/material@npm:5.14.16" + dependencies: + "@babel/runtime": ^7.23.2 + "@mui/base": 5.0.0-beta.22 + "@mui/core-downloads-tracker": ^5.14.16 + "@mui/system": ^5.14.16 + "@mui/types": ^7.2.8 + "@mui/utils": ^5.14.16 + "@types/react-transition-group": ^4.4.8 + clsx: ^2.0.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + react-is: ^18.2.0 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 77b2e02b82c9eebef0b24bca8f05721d320f40eb5f6ae77a265f5613933afc98173b90773fc789b63868303cfbc406e7bff2ce445c0c3e7033dcaa8782af6a99 + languageName: node + linkType: hard + +"@mui/private-theming@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/private-theming@npm:5.14.16" + dependencies: + "@babel/runtime": ^7.23.2 + "@mui/utils": ^5.14.16 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: ce4b0e2aa5bf285d72732394c785b47de04745ad2dc4f61a9a249adf0eb25e23998f97c6932d994481656116f177bbdf42bd815ba6f54e8a2d9f9f70a40037ae + languageName: node + linkType: hard + +"@mui/styled-engine@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/styled-engine@npm:5.14.16" + dependencies: + "@babel/runtime": ^7.23.2 + "@emotion/cache": ^11.11.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: be56ed34e3c6300de1934707ce81584638683be83b3b9d7b5ce27b87871b7621487a1fc8c196d9bfc1a59c87090f3feaf07984274d51d6110147f6a85e86670a + languageName: node + linkType: hard + +"@mui/system@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/system@npm:5.14.16" + dependencies: + "@babel/runtime": ^7.23.2 + "@mui/private-theming": ^5.14.16 + "@mui/styled-engine": ^5.14.16 + "@mui/types": ^7.2.8 + "@mui/utils": ^5.14.16 + clsx: ^2.0.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: fdb1212e2366efeeddc30cec794459e06d548180f186a3840ba2985bfc99cf0251f0b97dc572732c73b58cec8586ae8694dfea54053bee2b097aee1716ec48ef + languageName: node + linkType: hard + +"@mui/types@npm:^7.2.8": + version: 7.2.8 + resolution: "@mui/types@npm:7.2.8" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1302d2d1b5a13201efede82ef16438737bd890f9b0a728714fc2da204f6031f055fbd84623ea63ff4ae5d4306b458699d85925608eb8f35df78e1dc0d7a44fc5 + languageName: node + linkType: hard + +"@mui/utils@npm:^5.14.16": + version: 5.14.16 + resolution: "@mui/utils@npm:5.14.16" + dependencies: + "@babel/runtime": ^7.23.2 + "@types/prop-types": ^15.7.9 + prop-types: ^15.8.1 + react-is: ^18.2.0 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 4f2fab609447ccd09d237b0136f6f3122b0a4c9864b1cebe6bd8a26350d3fb3cffb8b23b26af4fa9f34f74fffcb2d549285ce75a7185fcde39e14241aa06e129 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -5089,7 +5447,7 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.9.3": +"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.5.4, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 @@ -6323,6 +6681,13 @@ __metadata: languageName: node linkType: hard +"@types/dom4@npm:^2.0.1": + version: 2.0.3 + resolution: "@types/dom4@npm:2.0.3" + checksum: b8b3d73b4c3fafa0080feb44bb3aeb1718ce0878d18f5ae86ed480bf012d16185afcd82afcd965be4636176de94c351ee3fe74b40430385c3ac8f9192ef86b1b + languageName: node + linkType: hard + "@types/dom4@npm:^2.0.2": version: 2.0.2 resolution: "@types/dom4@npm:2.0.2" @@ -6748,6 +7113,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.9": + version: 15.7.9 + resolution: "@types/prop-types@npm:15.7.9" + checksum: c7591d3ff7593e243908a07e1d3e2bb6e8879008af5800d8378115a90d0fdf669a1cae72a6d7f69e59c4fa7bb4c8ed61f6ebc1c520fe110c6f2b03ac02414072 + languageName: node + linkType: hard + "@types/q@npm:^1.5.1": version: 1.5.6 resolution: "@types/q@npm:1.5.6" @@ -6817,6 +7189,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.8": + version: 4.4.8 + resolution: "@types/react-transition-group@npm:4.4.8" + dependencies: + "@types/react": "*" + checksum: ad7ba2bce97631fda9d89b4ed9772489bd050fec3ccd7563041b206dbe219d37d22e0d7731b1f90f56e89daf40e69ba16beba8066c42165bf8a584533feb6a2c + languageName: node + linkType: hard + "@types/react@npm:*, @types/react@npm:>=15, @types/react@npm:^18.2.20": version: 18.2.22 resolution: "@types/react@npm:18.2.22" @@ -8730,7 +9111,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-macros@npm:^3.0.1": +"babel-plugin-macros@npm:^3.0.1, babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" dependencies: @@ -9868,7 +10249,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": +"classnames@npm:*, classnames@npm:^2.2, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": version: 2.3.2 resolution: "classnames@npm:2.3.2" checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e @@ -10069,6 +10450,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.0.0": + version: 2.0.0 + resolution: "clsx@npm:2.0.0" + checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e + languageName: node + linkType: hard + "cmd-shim@npm:^3.0.0, cmd-shim@npm:^3.0.3": version: 3.0.3 resolution: "cmd-shim@npm:3.0.3" @@ -11185,7 +11573,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.10, csstype@npm:^3.0.2": +"csstype@npm:^3.0.10, csstype@npm:^3.0.2, csstype@npm:^3.1.2": version: 3.1.2 resolution: "csstype@npm:3.1.2" checksum: e1a52e6c25c1314d6beef5168da704ab29c5186b877c07d822bd0806717d9a265e8493a2e35ca7e68d0f5d472d43fac1cdce70fd79fd0853dff81f3028d857b5 @@ -24449,6 +24837,16 @@ __metadata: languageName: node linkType: hard +"react-innertext@npm:^1.1.5": + version: 1.1.5 + resolution: "react-innertext@npm:1.1.5" + peerDependencies: + "@types/react": ">=0.0.0 <=99" + react: ">=0.0.0 <=99" + checksum: 01c9c8a5a471f03965b9d69c8bee02d420fd3a607b678004f0bdee098c9bccb3e2eae8460c90b1d180d2a8d544a3ddb09b8c2a11e5aa8b3a077733e2d9c25c2c + languageName: node + linkType: hard + "react-input-autosize@npm:^3.0.0": version: 3.0.0 resolution: "react-input-autosize@npm:3.0.0" @@ -24492,6 +24890,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.2.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e + languageName: node + linkType: hard + "react-json-tree@npm:^0.15.0": version: 0.15.2 resolution: "react-json-tree@npm:0.15.2" @@ -24526,7 +24931,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^1.3.11": +"react-popper@npm:^1.3.11, react-popper@npm:^1.3.7": version: 1.3.11 resolution: "react-popper@npm:1.3.11" dependencies: @@ -24543,7 +24948,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": +"react-popper@npm:^2.2.4, react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" dependencies: @@ -25473,6 +25878,13 @@ __metadata: languageName: node linkType: hard +"resize-observer-polyfill@npm:^1.5.1": + version: 1.5.1 + resolution: "resize-observer-polyfill@npm:1.5.1" + checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094 + languageName: node + linkType: hard + "resolve-alpn@npm:^1.0.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -28315,6 +28727,20 @@ __metadata: languageName: node linkType: hard +"tslib@npm:~1.13.0": + version: 1.13.0 + resolution: "tslib@npm:1.13.0" + checksum: 50e9327361f94f328c0715582a7f725f69838ab3c2559d143643c5367262fe14552768ba8cfc65bc7dc924a619aea599b3a28b6653458cdca77bbebaf9bc8df4 + languageName: node + linkType: hard + +"tslib@npm:~2.3.1": + version: 2.3.1 + resolution: "tslib@npm:2.3.1" + checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 + languageName: node + linkType: hard + "tslib@npm:~2.5.0": version: 2.5.3 resolution: "tslib@npm:2.5.3" From b38578eb1cf3b5fc0eac8cd9c9f1784f2336bec3 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Thu, 2 Nov 2023 11:57:20 -0500 Subject: [PATCH 02/52] Things work mostly as they used to --- deps/web-components | 2 +- src/pages/maps/@id/index.page.ts | 2 +- src/pages/maps/index.page.server.ts | 2 +- yarn.lock | 102 ++++------------------------ 4 files changed, 15 insertions(+), 93 deletions(-) diff --git a/deps/web-components b/deps/web-components index 79b6a5d4..7a944540 160000 --- a/deps/web-components +++ b/deps/web-components @@ -1 +1 @@ -Subproject commit 79b6a5d4e8f7658b772cfcb33c2d6c2ebf400e48 +Subproject commit 7a94454053404a6b2c2356baae1bb7effc999a76 diff --git a/src/pages/maps/@id/index.page.ts b/src/pages/maps/@id/index.page.ts index e8c0f428..f824513c 100644 --- a/src/pages/maps/@id/index.page.ts +++ b/src/pages/maps/@id/index.page.ts @@ -3,7 +3,7 @@ import { SETTINGS } from "~/map-interface/settings"; import h from "@macrostrat/hyper"; import { ClientOnly } from "~/renderer/client-only"; -const apiAddress = "http://localhost:5001" + "/v2/defs/sources"; +const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; export async function onBeforeRender(pageContext: PageContextBuiltInServer) { const { id } = pageContext.routeParams; diff --git a/src/pages/maps/index.page.server.ts b/src/pages/maps/index.page.server.ts index b6edf7f0..baa86d09 100644 --- a/src/pages/maps/index.page.server.ts +++ b/src/pages/maps/index.page.server.ts @@ -1,7 +1,7 @@ import fetch from "node-fetch"; import { SETTINGS } from "~/map-interface/settings"; -const apiAddress = "http://localhost:5001" + "/v2/defs/sources"; +const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; export async function onBeforeRender(pageContext) { // `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here. diff --git a/yarn.lock b/yarn.lock index 64e704cf..681f8b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1707,7 +1707,7 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/colors@npm:^4.0.0-alpha.3, @blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": +"@blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": version: 4.2.1 resolution: "@blueprintjs/colors@npm:4.2.1" dependencies: @@ -1716,33 +1716,7 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/core@npm:^3.52.0": - version: 3.54.0 - resolution: "@blueprintjs/core@npm:3.54.0" - dependencies: - "@blueprintjs/colors": ^4.0.0-alpha.3 - "@blueprintjs/icons": ^3.33.0 - "@juggle/resize-observer": ^3.3.1 - "@types/dom4": ^2.0.1 - classnames: ^2.2 - dom4: ^2.1.5 - normalize.css: ^8.0.1 - popper.js: ^1.16.1 - react-lifecycles-compat: ^3.0.4 - react-popper: ^1.3.7 - react-transition-group: ^2.9.0 - tslib: ~2.3.1 - peerDependencies: - react: ^15.3.0 || 16 || 17 - react-dom: ^15.3.0 || 16 || 17 - bin: - upgrade-blueprint-2.0.0-rename: scripts/upgrade-blueprint-2.0.0-rename.sh - upgrade-blueprint-3.0.0-rename: scripts/upgrade-blueprint-3.0.0-rename.sh - checksum: 97b8811bfc32284bb36e62a44210e84d5abe164ef553670866e0628718db4a98c79b9665f73014b1474f534a3d3260e94af274e669fb0ebfeb323305a81b5375 - languageName: node - linkType: hard - -"@blueprintjs/core@npm:^3||^4, @blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": +"@blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": version: 4.20.2 resolution: "@blueprintjs/core@npm:4.20.2" dependencies: @@ -1790,16 +1764,6 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/icons@npm:^3.33.0": - version: 3.33.0 - resolution: "@blueprintjs/icons@npm:3.33.0" - dependencies: - classnames: ^2.2 - tslib: ~2.3.1 - checksum: 9b1485a3ce17a97596b7fa7276ddbe85e33c56f061358351a626d353bf3eab6ab1b36a1860aec2feb7933ef0293c5f8e1f3342a89051720d1953343aab753cb3 - languageName: node - linkType: hard - "@blueprintjs/icons@npm:^4.14.5, @blueprintjs/icons@npm:^4.16.0": version: 4.16.0 resolution: "@blueprintjs/icons@npm:4.16.0" @@ -1811,23 +1775,6 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/popover2@npm:^0.12.9": - version: 0.12.9 - resolution: "@blueprintjs/popover2@npm:0.12.9" - dependencies: - "@blueprintjs/core": ^3.52.0 - "@popperjs/core": ^2.5.4 - classnames: ^2.2 - dom4: ^2.1.5 - react-popper: ^2.2.4 - resize-observer-polyfill: ^1.5.1 - tslib: ~1.13.0 - peerDependencies: - react: ^16.8.0 || ^17 - checksum: f782600f3cd372abbdd47b9ca3ab9265c1e62e4b3b353d4b06c11c3663d62b01d597df11ba21aff9885a825c7960af878d8448922944489de4bc80c5019981de - languageName: node - linkType: hard - "@blueprintjs/popover2@npm:^1.13.12, @blueprintjs/popover2@npm:^1.14.11": version: 1.14.11 resolution: "@blueprintjs/popover2@npm:1.14.11" @@ -3515,12 +3462,15 @@ __metadata: version: 0.0.0-use.local resolution: "@macrostrat/data-sheet@workspace:deps/web-components/packages/data-sheet" dependencies: - "@blueprintjs/core": ^3||^4 - "@blueprintjs/popover2": ^0.12.9 + "@blueprintjs/core": ^4.17.8 + "@blueprintjs/popover2": ^1.13.12 "@macrostrat/hyper": ^1.2.12 + chroma-js: ^2.4.2 + classnames: ^2.3.1 d3-array: ^2.12.1 immutability-helper: ^3.1.1 - react: ^17.0.2 + react: ^17.0.2||^18 + react-color: ^2.19.3 react-datasheet: ^1.4.9 react-dnd: ^14.0.2 react-dnd-html5-backend: ^14.0.0 @@ -5447,7 +5397,7 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.5.4, @popperjs/core@npm:^2.9.3": +"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 @@ -6681,13 +6631,6 @@ __metadata: languageName: node linkType: hard -"@types/dom4@npm:^2.0.1": - version: 2.0.3 - resolution: "@types/dom4@npm:2.0.3" - checksum: b8b3d73b4c3fafa0080feb44bb3aeb1718ce0878d18f5ae86ed480bf012d16185afcd82afcd965be4636176de94c351ee3fe74b40430385c3ac8f9192ef86b1b - languageName: node - linkType: hard - "@types/dom4@npm:^2.0.2": version: 2.0.2 resolution: "@types/dom4@npm:2.0.2" @@ -10249,7 +10192,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:*, classnames@npm:^2.2, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": +"classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": version: 2.3.2 resolution: "classnames@npm:2.3.2" checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e @@ -24931,7 +24874,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^1.3.11, react-popper@npm:^1.3.7": +"react-popper@npm:^1.3.11": version: 1.3.11 resolution: "react-popper@npm:1.3.11" dependencies: @@ -24948,7 +24891,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^2.2.4, react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": +"react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" dependencies: @@ -25878,13 +25821,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094 - languageName: node - linkType: hard - "resolve-alpn@npm:^1.0.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -28727,20 +28663,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:~1.13.0": - version: 1.13.0 - resolution: "tslib@npm:1.13.0" - checksum: 50e9327361f94f328c0715582a7f725f69838ab3c2559d143643c5367262fe14552768ba8cfc65bc7dc924a619aea599b3a28b6653458cdca77bbebaf9bc8df4 - languageName: node - linkType: hard - -"tslib@npm:~2.3.1": - version: 2.3.1 - resolution: "tslib@npm:2.3.1" - checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 - languageName: node - linkType: hard - "tslib@npm:~2.5.0": version: 2.5.3 resolution: "tslib@npm:2.5.3" From 4ee7d57e443c79901099e1a84bcb4a79840943c1 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 01:15:01 -0500 Subject: [PATCH 03/52] Format codebase with prettier --- src/pages/maps/@id/edit/EditInterface.ts | 105 ++++++----- src/pages/maps/@id/edit/EditTable.ts | 219 +++++++++++++---------- src/pages/maps/@id/edit/index.page.ts | 6 +- src/pages/maps/@id/edit/map-interface.ts | 9 +- src/pages/maps/@id/edit/server.ts | 53 +++--- 5 files changed, 206 insertions(+), 186 deletions(-) diff --git a/src/pages/maps/@id/edit/EditInterface.ts b/src/pages/maps/@id/edit/EditInterface.ts index 7ee38792..1cedc993 100644 --- a/src/pages/maps/@id/edit/EditInterface.ts +++ b/src/pages/maps/@id/edit/EditInterface.ts @@ -24,9 +24,7 @@ import EditTable from "./EditTable"; const h = hyper.styled(styles); -interface TableProps { - -} +interface TableProps {} interface IconButtonProps { icon: string; @@ -35,82 +33,81 @@ interface IconButtonProps { } function IconButton({ icon, name, onClick }: IconButtonProps) { - return h("button.icon-button", {onClick: onClick}, - [ - h("div.icon-container", {}, [ - h(Icon, {icon: icon, size: 24}) - ]), - h("span.icon-label", {}, name) - ] - ); + return h("button.icon-button", { onClick: onClick }, [ + h("div.icon-container", {}, [h(Icon, { icon: icon, size: 24 })]), + h("span.icon-label", {}, name), + ]); } interface EditMenuProps { setMenu: () => void; } -function EditMenu({setMenu}: EditMenuProps): ReactElement<{}> | ReactElement | ReactFragment { - return h( - "div.edit-menu", - {}, - [ - h(IconButton, {icon: "polygon-filter", name: "Polygons", onClick: () => setMenu("polygons")}), - ] - ) +function EditMenu({ + setMenu, +}: EditMenuProps): ReactElement<{}> | ReactElement | ReactFragment { + return h("div.edit-menu", {}, [ + h(IconButton, { + icon: "polygon-filter", + name: "Polygons", + onClick: () => setMenu("polygons"), + }), + ]); } interface EditTableDrawerProps { menu: string; } -function EditTableDrawer({menu, children}: EditTableDrawerProps){ - +function EditTableDrawer({ menu, children }: EditTableDrawerProps) { const [maxWidth, setMaxWidth] = useState(0); const [startPosition, setStartPosition] = useState(0); - useEffect(() => { - setMaxWidth(menu != undefined ? window.innerWidth / 2 : 0) - }, [menu]) - - return h( - "div.edit-table-drawer", - { style: { maxWidth: maxWidth + "px" }}, - [ - children, - h("div.width-adjuster", { + setMaxWidth(menu != undefined ? window.innerWidth / 2 : 0); + }, [menu]); + + return h("div.edit-table-drawer", { style: { maxWidth: maxWidth + "px" } }, [ + children, + h( + "div.width-adjuster", + { onDragStart: (e) => { - setStartPosition(e.clientX) + setStartPosition(e.clientX); }, onDragEnd: (e) => { - setMaxWidth(maxWidth + (e.clientX - startPosition)) + setMaxWidth(maxWidth + (e.clientX - startPosition)); }, - draggable: true - }, []) - ] - ) + draggable: true, + }, + [] + ), + ]); } - interface EditInterfaceProps { - title ?: string; - parentRoute ?: string; - source ?: number; + title?: string; + parentRoute?: string; + source?: number; } -export default function EditInterface({title, parentRoute, source_id} : EditInterfaceProps){ - +export default function EditInterface({ + title, + parentRoute, + source_id, +}: EditInterfaceProps) { const [menu, setMenu] = useState(undefined); - - return h("div.interface", {}, [ - menu == undefined ? h(EditMenu, {setMenu}) : null, - h(EditTableDrawer, {menu}, - [ - menu == "polygons" ? h(EditTable, {url: `http://localhost:8000/sources/${source_id}/polygons`}, []) : null - ] - ) - ]) - -} \ No newline at end of file + menu == undefined ? h(EditMenu, { setMenu }) : null, + h(EditTableDrawer, { menu }, [ + menu == "polygons" + ? h( + EditTable, + { url: `http://localhost:8000/sources/${source_id}/polygons` }, + [] + ) + : null, + ]), + ]); +} diff --git a/src/pages/maps/@id/edit/EditTable.ts b/src/pages/maps/@id/edit/EditTable.ts index ce20371a..6047449b 100644 --- a/src/pages/maps/@id/edit/EditTable.ts +++ b/src/pages/maps/@id/edit/EditTable.ts @@ -1,23 +1,32 @@ - import hyper from "@macrostrat/hyper"; -import { ReactElement, ReactFragment, useState, useEffect, useMemo } from "react"; +import { + ReactElement, + ReactFragment, + useState, + useEffect, + useMemo, +} from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner } from "@blueprintjs/core"; -import { Column, Table2, EditableCell2, RowHeaderCell2, SelectionModes } from "@blueprintjs/table"; -import {TablePagination} from "@mui/material" +import { + Column, + Table2, + EditableCell2, + RowHeaderCell2, + SelectionModes, +} from "@blueprintjs/table"; +import { TablePagination } from "@mui/material"; import "@blueprintjs/table/lib/css/table.css"; import styles from "./editTable.module.sass"; - const h = hyper.styled(styles); - - const range = (start, stop, step = 1) => - Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step) - + Array(Math.ceil((stop - start) / step)) + .fill(start) + .map((x, y) => x + y * step); interface Selection { cols: number[]; @@ -25,21 +34,20 @@ interface Selection { } class Filter { - constructor(column_name: string, operator: string, value: string){ - this.column_name = column_name - this.operator = operator - this.value = value + constructor(column_name: string, operator: string, value: string) { + this.column_name = column_name; + this.operator = operator; + this.value = value; } to_object = () => { - let o = {} - o[this.column_name] = this.operator + "." + this.value - return o - } + let o = {}; + o[this.column_name] = this.operator + "." + this.value; + return o; + }; to_array = () => { - return [this.column_name, this.operator + "." + this.value] - } - + return [this.column_name, this.operator + "." + this.value]; + }; } interface Filters { @@ -48,12 +56,10 @@ interface Filters { interface TableSelection { columns: string[]; - filters: Filters + filters: Filters; } - -export default function EditTable({url}){ - +export default function EditTable({ url }) { // Table values const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(999999); @@ -62,85 +68,98 @@ export default function EditTable({url}){ const [data, setData] = useState(undefined); const [dataToggle, setDataToggle] = useState(false); const [inputValue, setInputValue] = useState(""); - const [tableSelection, setTableSelection] = useState<TableSelection>({columns: [], filters: []}) - - const cellRenderer = ({key, row, cell}) => { - - return h(EditableCell2, {onChange: (e) => console.log(), "value": data[row][key]}, []) - } + const [tableSelection, setTableSelection] = useState<TableSelection>({ + columns: [], + filters: [], + }); + + const cellRenderer = ({ key, row, cell }) => { + return h( + EditableCell2, + { onChange: (e) => console.log(), value: data[row][key] }, + [] + ); + }; let getData = async () => { + let dataURL = new URL(url); - let dataURL = new URL(url) + dataURL.searchParams.append("page", page.toString()); + dataURL.searchParams.append("page_size", pageSize.toString()); - dataURL.searchParams.append("page", page.toString()) - dataURL.searchParams.append("page_size", pageSize.toString()) + let response = await fetch(dataURL); + let data = await response.json(); - let response = await fetch(dataURL) - let data = await response.json() - - setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))) - setData(data) - } + setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))); + setData(data); + }; useEffect(() => { - getData() - }, [page, pageSize, dataToggle]) - + getData(); + }, [page, pageSize, dataToggle]); - if(data == undefined){ - return h(Spinner) + if (data == undefined) { + return h(Spinner); } - const columns = Object.keys(data[0]).filter(x => x != "db_id").map((key) => { - return h(Column, {name: key, cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), "key": key}) - }) + const columns = Object.keys(data[0]) + .filter((x) => x != "db_id") + .map((key) => { + return h(Column, { + name: key, + cellRenderer: (row, cell) => + cellRenderer({ key: key, row: row, cell: cell }), + key: key, + }); + }); const getSelectionValues = (selections: Selection[]) => { - - if(selections.length == 0){ - setTableSelection({columns: [], filters: {...tableSelection.filters, "tableSelection": undefined}}) - return + if (selections.length == 0) { + setTableSelection({ + columns: [], + filters: { ...tableSelection.filters, tableSelection: undefined }, + }); + return; } - const rows = selections[0]?.rows - const cols = selections[0]?.cols - - const columnsKeys = Object.keys(data[0]) - const selectedColumnKeys = columnsKeys.slice(cols[0], cols[1] + 1) + const rows = selections[0]?.rows; + const cols = selections[0]?.cols; - let selection: TableSelection - if(rows == undefined){ - selection = {columns: selectedColumnKeys, ...tableSelection} + const columnsKeys = Object.keys(data[0]); + const selectedColumnKeys = columnsKeys.slice(cols[0], cols[1] + 1); + let selection: TableSelection; + if (rows == undefined) { + selection = { columns: selectedColumnKeys, ...tableSelection }; } else { - const selectedRowIndices = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length) - const dbIds = selectedRowIndices.map((row) => data[row]['db_id']) - const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")") - - - selection = {columns: selectedColumnKeys, filters: {...tableSelection.filters, "tableSelection": filter}} + const selectedRowIndices = + rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length); + const dbIds = selectedRowIndices.map((row) => data[row]["db_id"]); + const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")"); + + selection = { + columns: selectedColumnKeys, + filters: { ...tableSelection.filters, tableSelection: filter }, + }; } - setTableSelection(selection) - } + setTableSelection(selection); + }; const rowHeaderCellRenderer = (rowIndex: number) => { - return h(RowHeaderCell2, {name: data[rowIndex]['db_id']}, []) - } + return h(RowHeaderCell2, { name: data[rowIndex]["db_id"] }, []); + }; const submitChange = async (value: string) => { for (const column of tableSelection.columns) { + let updateURL = new URL(url); - let updateURL = new URL(url) - - for(const filter: Filter of Object.values(tableSelection.filters)){ - updateURL.searchParams.append(...filter.to_array()) + for (const filter: Filter of Object.values(tableSelection.filters)) { + updateURL.searchParams.append(...filter.to_array()); } - let patch = {[column]: value} - console.log(patch, JSON.stringify(patch)) - + let patch = { [column]: value }; + console.log(patch, JSON.stringify(patch)); let response = await fetch(updateURL, { method: "PATCH", @@ -148,34 +167,46 @@ export default function EditTable({url}){ "Content-Type": "application/json", }, body: JSON.stringify(patch), - }) + }); - if(response.status != 204){ - console.error("Failed to update", response) + if (response.status != 204) { + console.error("Failed to update", response); } } - setDataToggle(!dataToggle) - } - + setDataToggle(!dataToggle); + }; return h(HotkeysProvider, {}, [ h("div.table-container", {}, [ h("div.input-form", {}, [ - h(InputGroup, {"value": inputValue, className: "update-input-group", onChange: (e) => setInputValue(e.target.value)}), - h(Button, {type: "submit", onClick: () => submitChange(inputValue)}, ["Submit"]) + h(InputGroup, { + value: inputValue, + className: "update-input-group", + onChange: (e) => setInputValue(e.target.value), + }), + h(Button, { type: "submit", onClick: () => submitChange(inputValue) }, [ + "Submit", + ]), ]), - h(Table2, + h( + Table2, { selectionModes: SelectionModes.COLUMNS_AND_CELLS, rowHeaderCellRenderer: rowHeaderCellRenderer, - onSelection: (selections: Selection[]) => getSelectionValues(selections), - numRows: data.length + onSelection: (selections: Selection[]) => + getSelectionValues(selections), + numRows: data.length, }, - [ - columns - ] + [columns] ), - h(TablePagination, {component: "div", count: totalCount, rowsPerPage: pageSize, page: page, onRowsPerPageChange: (e) => setPageSize(e.target.value), onPageChange: (e, p) => setPage(p)}), - ]) - ]) -} \ No newline at end of file + h(TablePagination, { + component: "div", + count: totalCount, + rowsPerPage: pageSize, + page: page, + onRowsPerPageChange: (e) => setPageSize(e.target.value), + onPageChange: (e, p) => setPage(p), + }), + ]), + ]); +} diff --git a/src/pages/maps/@id/edit/index.page.ts b/src/pages/maps/@id/edit/index.page.ts index a57c5e40..209e6352 100644 --- a/src/pages/maps/@id/edit/index.page.ts +++ b/src/pages/maps/@id/edit/index.page.ts @@ -6,7 +6,6 @@ import { ClientOnly } from "~/renderer/client-only"; const apiAddress = SETTINGS.apiDomain + "/api/v2/defs/sources"; export async function onBeforeRender(pageContext: PageContextBuiltInServer) { - const { id } = pageContext.routeParams; const params = new URLSearchParams({ @@ -34,5 +33,8 @@ export async function onBeforeRender(pageContext: PageContextBuiltInServer) { const MapInterface = () => import("./map-interface"); export function Page({ id, map }) { - return h("div.single-map", h(ClientOnly, { component: MapInterface, id, map })); + return h( + "div.single-map", + h(ClientOnly, { component: MapInterface, id, map }) + ); } diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index 7a09b063..cb735097 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -255,9 +255,13 @@ export default function MapInterface({ id, map }) { MapAreaContainer, { className: "single-map", - navbar: h(EditInterface, {title:"Source 1", parentRoute:"/maps/", source_id:id}, []), + navbar: h( + EditInterface, + { title: "Source 1", parentRoute: "/maps/", source_id: id }, + [] + ), contextPanel, - contextPanelOpen: isOpen + contextPanelOpen: isOpen, }, [ h( @@ -306,7 +310,6 @@ function BaseLayerSelector({ layer, setLayer }) { ]); } - function OpacitySlider(props) { return h("div.opacity-slider", [ h(NullableSlider, { diff --git a/src/pages/maps/@id/edit/server.ts b/src/pages/maps/@id/edit/server.ts index d4dd5fb8..ac8f02ba 100644 --- a/src/pages/maps/@id/edit/server.ts +++ b/src/pages/maps/@id/edit/server.ts @@ -1,10 +1,8 @@ - interface Error { code: number; text: string; } - interface Patch { id: number; function: () => Response; @@ -12,50 +10,39 @@ interface Patch { attempts: number; } - // Holds the current state of the patching process class PatchManager { constructor() { - this.active = [] - this.success = [] - this.failed = [] - this.abandoned = [] + this.active = []; + this.success = []; + this.failed = []; + this.abandoned = []; } // Wraps the patch function and handles in the context of the manager - async runPatch (patch: Patch) { - - patch.attempts += 1 + async runPatch(patch: Patch) { + patch.attempts += 1; try { - let response = await patch.function() - - if(response.status == 400){ - patch.error.code = response.status - patch.error.text = (await response.json())['details'] - this.abandoned.push(patch) - - } else if(response.status == 200){ - this.success.push(patch) - + let response = await patch.function(); + + if (response.status == 400) { + patch.error.code = response.status; + patch.error.text = (await response.json())["details"]; + this.abandoned.push(patch); + } else if (response.status == 200) { + this.success.push(patch); } else { - this.failed.push(patch) - + this.failed.push(patch); } } catch (e) { - patch.error.code = response.status - patch.error.text = (await response.json())['details'] - this.failed.push(patch) + patch.error.code = response.status; + patch.error.text = (await response.json())["details"]; + this.failed.push(patch); } } } +async function getTable() {} -async function getTable(){ - -} - - -async function patchTable(value, db_id){ - -} \ No newline at end of file +async function patchTable(value, db_id) {} From ad5b0c610ebfdcec426b6b4a2154831c9731ca8a Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 01:18:55 -0500 Subject: [PATCH 04/52] Renamed files to match code style --- .../maps/@id/edit/{EditInterface.ts => edit-interface.ts} | 4 ++-- .../@id/edit/{editMenu.module.sass => edit-menu.module.sass} | 0 .../edit/{editTable.module.sass => edit-table.module.sass} | 0 src/pages/maps/@id/edit/{EditTable.ts => edit-table.ts} | 2 +- src/pages/maps/@id/edit/map-interface.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/pages/maps/@id/edit/{EditInterface.ts => edit-interface.ts} (96%) rename src/pages/maps/@id/edit/{editMenu.module.sass => edit-menu.module.sass} (100%) rename src/pages/maps/@id/edit/{editTable.module.sass => edit-table.module.sass} (100%) rename src/pages/maps/@id/edit/{EditTable.ts => edit-table.ts} (99%) diff --git a/src/pages/maps/@id/edit/EditInterface.ts b/src/pages/maps/@id/edit/edit-interface.ts similarity index 96% rename from src/pages/maps/@id/edit/EditInterface.ts rename to src/pages/maps/@id/edit/edit-interface.ts index 1cedc993..d9f223fc 100644 --- a/src/pages/maps/@id/edit/EditInterface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -15,12 +15,12 @@ import { import { SETTINGS } from "~/map-interface/settings"; import { useMapRef } from "@macrostrat/mapbox-react"; import { ReactElement, ReactFragment, useEffect } from "react"; -import styles from "./editMenu.module.sass"; +import styles from "./edit-menu.module.sass"; import { MapNavbar } from "~/dev/map-layers/utils"; import { useMemo, useState } from "react"; import "~/styles/global.styl"; import { Icon } from "@blueprintjs/core"; -import EditTable from "./EditTable"; +import EditTable from "./edit-table"; const h = hyper.styled(styles); diff --git a/src/pages/maps/@id/edit/editMenu.module.sass b/src/pages/maps/@id/edit/edit-menu.module.sass similarity index 100% rename from src/pages/maps/@id/edit/editMenu.module.sass rename to src/pages/maps/@id/edit/edit-menu.module.sass diff --git a/src/pages/maps/@id/edit/editTable.module.sass b/src/pages/maps/@id/edit/edit-table.module.sass similarity index 100% rename from src/pages/maps/@id/edit/editTable.module.sass rename to src/pages/maps/@id/edit/edit-table.module.sass diff --git a/src/pages/maps/@id/edit/EditTable.ts b/src/pages/maps/@id/edit/edit-table.ts similarity index 99% rename from src/pages/maps/@id/edit/EditTable.ts rename to src/pages/maps/@id/edit/edit-table.ts index 6047449b..6d460739 100644 --- a/src/pages/maps/@id/edit/EditTable.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -19,7 +19,7 @@ import { import { TablePagination } from "@mui/material"; import "@blueprintjs/table/lib/css/table.css"; -import styles from "./editTable.module.sass"; +import styles from "./edit-table.module.sass"; const h = hyper.styled(styles); diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index cb735097..2672b3b6 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -28,7 +28,7 @@ import { InfoDrawerContainer, ExpansionPanel } from "@macrostrat/map-interface"; import { MapMarker } from "@macrostrat/map-interface"; import { NullableSlider } from "@macrostrat/ui-components"; import { tempImageIndex, s3Address } from "../../raster-images"; -import EditInterface from "./EditInterface"; +import EditInterface from "./edit-interface"; const h = hyper.styled(styles); From e19d2a640ef64212280681b9f752a766a4f7d631 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 01:20:16 -0500 Subject: [PATCH 05/52] Remove unused imports --- src/pages/maps/@id/edit/edit-interface.ts | 18 +----------------- src/pages/maps/@id/edit/edit-table.ts | 8 +------- src/pages/maps/@id/edit/map-interface.ts | 14 ++------------ 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index d9f223fc..bb3485ec 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -1,23 +1,7 @@ import hyper from "@macrostrat/hyper"; -import { - MapAreaContainer, - MapView, - PanelCard, -} from "@macrostrat/map-interface"; -import { - Spinner, - Radio, - RadioGroup, - NonIdealState, - Collapse, - Switch, -} from "@blueprintjs/core"; -import { SETTINGS } from "~/map-interface/settings"; -import { useMapRef } from "@macrostrat/mapbox-react"; import { ReactElement, ReactFragment, useEffect } from "react"; import styles from "./edit-menu.module.sass"; -import { MapNavbar } from "~/dev/map-layers/utils"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import "~/styles/global.styl"; import { Icon } from "@blueprintjs/core"; import EditTable from "./edit-table"; diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 6d460739..3ad34a79 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -1,12 +1,6 @@ import hyper from "@macrostrat/hyper"; -import { - ReactElement, - ReactFragment, - useState, - useEffect, - useMemo, -} from "react"; +import { useState, useEffect } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner } from "@blueprintjs/core"; import { diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index 2672b3b6..99166fbd 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -4,27 +4,17 @@ import { MapView, PanelCard, } from "@macrostrat/map-interface"; -import { - Spinner, - Radio, - RadioGroup, - NonIdealState, - Collapse, - Switch, -} from "@blueprintjs/core"; +import { Spinner, Radio, RadioGroup } from "@blueprintjs/core"; import { SETTINGS } from "~/map-interface/settings"; -import { useMapRef } from "@macrostrat/mapbox-react"; import { useEffect } from "react"; import styles from "./main.module.sass"; -import { MapNavbar } from "~/dev/map-layers/utils"; import { useMemo, useState } from "react"; import "~/styles/global.styl"; import boundingBox from "@turf/bbox"; import { LngLatBoundsLike } from "mapbox-gl"; import { buildMacrostratStyle } from "@macrostrat/mapbox-styles"; import { getMapboxStyle, mergeStyles } from "@macrostrat/mapbox-utils"; -import { useDarkMode, useAPIResult, JSONView } from "@macrostrat/ui-components"; -import { InfoDrawerContainer, ExpansionPanel } from "@macrostrat/map-interface"; +import { useDarkMode } from "@macrostrat/ui-components"; import { MapMarker } from "@macrostrat/map-interface"; import { NullableSlider } from "@macrostrat/ui-components"; import { tempImageIndex, s3Address } from "../../raster-images"; From 355c61dd563ef4aed9970f9773c4d5885eab05de Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 01:59:52 -0500 Subject: [PATCH 06/52] Make null checks slightly less confusing --- src/pages/maps/@id/edit/edit-interface.ts | 18 ++++---- src/pages/maps/@id/edit/map-interface.ts | 51 ++++------------------- 2 files changed, 16 insertions(+), 53 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index bb3485ec..c0ec76b7 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -80,18 +80,16 @@ export default function EditInterface({ parentRoute, source_id, }: EditInterfaceProps) { - const [menu, setMenu] = useState(undefined); + const [activePage, setActivePage] = useState(undefined); return h("div.interface", {}, [ - menu == undefined ? h(EditMenu, { setMenu }) : null, - h(EditTableDrawer, { menu }, [ - menu == "polygons" - ? h( - EditTable, - { url: `http://localhost:8000/sources/${source_id}/polygons` }, - [] - ) - : null, + h.if(activePage == null)(EditMenu, { setMenu: setActivePage }), + h(EditTableDrawer, { menu: activePage }, [ + h.if(activePage == "polygons")( + EditTable, + { url: `http://localhost:8000/sources/${source_id}/polygons` }, + [] + ), ]), ]); } diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index 99166fbd..0a46db64 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -18,6 +18,7 @@ import { useDarkMode } from "@macrostrat/ui-components"; import { MapMarker } from "@macrostrat/map-interface"; import { NullableSlider } from "@macrostrat/ui-components"; import { tempImageIndex, s3Address } from "../../raster-images"; +import { MapNavbar } from "~/components/map-navbar"; import EditInterface from "./edit-interface"; const h = hyper.styled(styles); @@ -61,41 +62,6 @@ function buildOverlayStyle({ }); } - const raster = rasterURL(focusedMap); - if (raster != null && layerOpacity.raster != null) { - const rasterStyle = { - ...emptyStyle, - sources: { - raster: { - type: "raster", - tiles: [ - SETTINGS.burwellTileDomain + - "/cog/tiles/{z}/{x}/{y}.png?url=" + - raster, - ], - tileSize: 256, - }, - }, - layers: [ - { - id: "raster", - type: "raster", - source: "raster", - minzoom: 0, - maxzoom: 22, - layout: { - visibility: "visible", - }, - paint: { - "raster-opacity": layerOpacity.raster, - }, - }, - ], - }; - - mapStyle = mergeStyles(rasterStyle, mapStyle); - } - if (style == null) { return mapStyle; } @@ -129,11 +95,7 @@ function basemapStyle(basemap, inDarkMode) { export default function MapInterface({ id, map }) { const [isOpen, setOpen] = useState(false); const dark = useDarkMode()?.isEnabled ?? false; - const title = h([ - h("code", map.properties.source_id), - " ", - map.properties.name, - ]); + const title = map.properties.name; const hasRaster = rasterURL(map.properties.source_id) != null; @@ -245,13 +207,16 @@ export default function MapInterface({ id, map }) { MapAreaContainer, { className: "single-map", - navbar: h( + navbar: h(MapNavbar, { title, parentRoute: "/maps", isOpen, setOpen }), + contextPanel, + contextPanelOpen: isOpen, + detailPanelOpen: true, + detailPanelStyle: "fixed", + detailPanel: h( EditInterface, { title: "Source 1", parentRoute: "/maps/", source_id: id }, [] ), - contextPanel, - contextPanelOpen: isOpen, }, [ h( From 36502bf418f48545dc0bc2cbaa3e63cf358d00c7 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 02:13:37 -0500 Subject: [PATCH 07/52] Use a more standard button --- src/pages/maps/@id/edit/edit-interface.ts | 35 +++++++---------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index c0ec76b7..7db94109 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -1,46 +1,33 @@ import hyper from "@macrostrat/hyper"; -import { ReactElement, ReactFragment, useEffect } from "react"; +import { ReactNode, useEffect } from "react"; import styles from "./edit-menu.module.sass"; import { useState } from "react"; import "~/styles/global.styl"; -import { Icon } from "@blueprintjs/core"; +import { Icon, Button } from "@blueprintjs/core"; import EditTable from "./edit-table"; const h = hyper.styled(styles); interface TableProps {} -interface IconButtonProps { - icon: string; - name: string; - onClick: () => void; -} - -function IconButton({ icon, name, onClick }: IconButtonProps) { - return h("button.icon-button", { onClick: onClick }, [ - h("div.icon-container", {}, [h(Icon, { icon: icon, size: 24 })]), - h("span.icon-label", {}, name), - ]); -} - interface EditMenuProps { - setMenu: () => void; + setActivePage: (page: string) => void; } -function EditMenu({ - setMenu, -}: EditMenuProps): ReactElement<{}> | ReactElement | ReactFragment { +function EditMenu({ setActivePage }: EditMenuProps) { return h("div.edit-menu", {}, [ - h(IconButton, { + h(Button, { icon: "polygon-filter", - name: "Polygons", - onClick: () => setMenu("polygons"), + text: "Polygons", + large: true, + onClick: () => setActivePage("polygons"), }), ]); } interface EditTableDrawerProps { menu: string; + children: ReactNode; } function EditTableDrawer({ menu, children }: EditTableDrawerProps) { @@ -48,7 +35,7 @@ function EditTableDrawer({ menu, children }: EditTableDrawerProps) { const [startPosition, setStartPosition] = useState(0); useEffect(() => { - setMaxWidth(menu != undefined ? window.innerWidth / 2 : 0); + setMaxWidth(menu != null ? window.innerWidth / 2 : 0); }, [menu]); return h("div.edit-table-drawer", { style: { maxWidth: maxWidth + "px" } }, [ @@ -83,7 +70,7 @@ export default function EditInterface({ const [activePage, setActivePage] = useState(undefined); return h("div.interface", {}, [ - h.if(activePage == null)(EditMenu, { setMenu: setActivePage }), + h.if(activePage == null)(EditMenu, { setActivePage }), h(EditTableDrawer, { menu: activePage }, [ h.if(activePage == "polygons")( EditTable, From 8b2348ee36ffd646b9d473f178e672d1750fd14a Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 02:28:21 -0500 Subject: [PATCH 08/52] Make edit panel properly resizable --- src/pages/maps/@id/edit/edit-interface.ts | 39 ++++++++++------------- src/pages/maps/@id/edit/main.module.sass | 5 ++- src/pages/maps/@id/edit/map-interface.ts | 22 ++++++------- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index 7db94109..0d78c50e 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -25,21 +25,16 @@ function EditMenu({ setActivePage }: EditMenuProps) { ]); } -interface EditTableDrawerProps { - menu: string; - children: ReactNode; -} - -function EditTableDrawer({ menu, children }: EditTableDrawerProps) { +function WidthAdjustablePanel({ children }: { children: ReactNode }) { const [maxWidth, setMaxWidth] = useState(0); const [startPosition, setStartPosition] = useState(0); useEffect(() => { - setMaxWidth(menu != null ? window.innerWidth / 2 : 0); - }, [menu]); + if (typeof window === "undefined") return; + setMaxWidth(window.innerWidth / 2); + }, []); return h("div.edit-table-drawer", { style: { maxWidth: maxWidth + "px" } }, [ - children, h( "div.width-adjuster", { @@ -47,36 +42,36 @@ function EditTableDrawer({ menu, children }: EditTableDrawerProps) { setStartPosition(e.clientX); }, onDragEnd: (e) => { - setMaxWidth(maxWidth + (e.clientX - startPosition)); + const dx = e.clientX - startPosition; + const newMaxWidth = maxWidth - dx; + setMaxWidth(newMaxWidth); }, draggable: true, }, [] ), + children, ]); } interface EditInterfaceProps { title?: string; parentRoute?: string; - source?: number; + source_id?: number; } -export default function EditInterface({ - title, - parentRoute, - source_id, -}: EditInterfaceProps) { - const [activePage, setActivePage] = useState(undefined); +export default function EditInterface({ source_id }: EditInterfaceProps) { + const [activePage, setActivePage] = useState(null); - return h("div.interface", {}, [ - h.if(activePage == null)(EditMenu, { setActivePage }), - h(EditTableDrawer, { menu: activePage }, [ + return h( + WidthAdjustablePanel, + h([ + h.if(activePage == null)(EditMenu, { setActivePage }), h.if(activePage == "polygons")( EditTable, { url: `http://localhost:8000/sources/${source_id}/polygons` }, [] ), - ]), - ]); + ]) + ); } diff --git a/src/pages/maps/@id/edit/main.module.sass b/src/pages/maps/@id/edit/main.module.sass index a2861296..f5cf3b3d 100644 --- a/src/pages/maps/@id/edit/main.module.sass +++ b/src/pages/maps/@id/edit/main.module.sass @@ -1,7 +1,10 @@ body margin: 0 padding: 0 - + +:root + --map-detail-stack-width: fit-content + .single-map width: 100vh height: 100vh diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index 0a46db64..e633d18c 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -1,25 +1,23 @@ +import { Radio, RadioGroup, Spinner } from "@blueprintjs/core"; import hyper from "@macrostrat/hyper"; import { MapAreaContainer, + MapMarker, MapView, PanelCard, } from "@macrostrat/map-interface"; -import { Spinner, Radio, RadioGroup } from "@blueprintjs/core"; -import { SETTINGS } from "~/map-interface/settings"; -import { useEffect } from "react"; -import styles from "./main.module.sass"; -import { useMemo, useState } from "react"; -import "~/styles/global.styl"; -import boundingBox from "@turf/bbox"; -import { LngLatBoundsLike } from "mapbox-gl"; import { buildMacrostratStyle } from "@macrostrat/mapbox-styles"; import { getMapboxStyle, mergeStyles } from "@macrostrat/mapbox-utils"; -import { useDarkMode } from "@macrostrat/ui-components"; -import { MapMarker } from "@macrostrat/map-interface"; -import { NullableSlider } from "@macrostrat/ui-components"; -import { tempImageIndex, s3Address } from "../../raster-images"; +import { NullableSlider, useDarkMode } from "@macrostrat/ui-components"; +import boundingBox from "@turf/bbox"; +import { LngLatBoundsLike } from "mapbox-gl"; +import { useEffect, useMemo, useState } from "react"; import { MapNavbar } from "~/components/map-navbar"; +import { SETTINGS } from "~/map-interface/settings"; +import "~/styles/global.styl"; +import { s3Address, tempImageIndex } from "../../raster-images"; import EditInterface from "./edit-interface"; +import styles from "./main.module.sass"; const h = hyper.styled(styles); From f2ef54a732cd7bd1286f3effc448fc73d1505801 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 02:48:12 -0500 Subject: [PATCH 09/52] db_id -> _pkid --- src/pages/maps/@id/edit/edit-table.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 3ad34a79..3a51b899 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -97,7 +97,7 @@ export default function EditTable({ url }) { } const columns = Object.keys(data[0]) - .filter((x) => x != "db_id") + .filter((x) => x != "_pkid") .map((key) => { return h(Column, { name: key, @@ -128,8 +128,8 @@ export default function EditTable({ url }) { } else { const selectedRowIndices = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length); - const dbIds = selectedRowIndices.map((row) => data[row]["db_id"]); - const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")"); + const dbIds = selectedRowIndices.map((row) => data[row]["_pkid"]); + const filter = new Filter("_pkid", "in", "(" + dbIds.join(",") + ")"); selection = { columns: selectedColumnKeys, @@ -141,7 +141,7 @@ export default function EditTable({ url }) { }; const rowHeaderCellRenderer = (rowIndex: number) => { - return h(RowHeaderCell2, { name: data[rowIndex]["db_id"] }, []); + return h(RowHeaderCell2, { name: data[rowIndex]["_pkid"] }, []); }; const submitChange = async (value: string) => { From 6efd0f704c86e7c1c7b714d58defac6d8fe34ab5 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 03:25:28 -0500 Subject: [PATCH 10/52] Fix css styles for table scrolling --- src/pages/maps/@id/edit/edit-interface.ts | 38 ++++++++++--------- src/pages/maps/@id/edit/edit-menu.module.sass | 18 +++++---- .../maps/@id/edit/edit-table.module.sass | 15 +++----- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index 0d78c50e..13128d57 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -34,24 +34,28 @@ function WidthAdjustablePanel({ children }: { children: ReactNode }) { setMaxWidth(window.innerWidth / 2); }, []); - return h("div.edit-table-drawer", { style: { maxWidth: maxWidth + "px" } }, [ - h( - "div.width-adjuster", - { - onDragStart: (e) => { - setStartPosition(e.clientX); - }, - onDragEnd: (e) => { - const dx = e.clientX - startPosition; - const newMaxWidth = maxWidth - dx; - setMaxWidth(newMaxWidth); + return h( + "div.width-adjustable-panel", + { style: { maxWidth: maxWidth + "px" } }, + [ + h( + "div.width-adjuster", + { + onDragStart: (e) => { + setStartPosition(e.clientX); + }, + onDragEnd: (e) => { + const dx = e.clientX - startPosition; + const newMaxWidth = maxWidth - dx; + setMaxWidth(newMaxWidth); + }, + draggable: true, }, - draggable: true, - }, - [] - ), - children, - ]); + [] + ), + h("div.width-adjustable-panel-content", {}, children), + ] + ); } interface EditInterfaceProps { diff --git a/src/pages/maps/@id/edit/edit-menu.module.sass b/src/pages/maps/@id/edit/edit-menu.module.sass index 9f09adf1..3c943aa3 100644 --- a/src/pages/maps/@id/edit/edit-menu.module.sass +++ b/src/pages/maps/@id/edit/edit-menu.module.sass @@ -17,15 +17,18 @@ button.icon-button .icon-label padding-bottom: 10px -div.edit-table-drawer +.width-adjustable-panel transition: max-width 0.1s ease-in-out - overflow: scroll height: 100% display: flex flex-direction: row - padding-left: 10px - padding-bottom: 10px - padding-top: 10px + position: relative + +.width-adjustable-panel-content + overflow: scroll + height: 100% + flex-grow: 1 + padding: 1em div.edit-table-wrapper overflow: scroll @@ -33,8 +36,9 @@ div.edit-table-wrapper div.width-adjuster cursor: col-resize - width: 10px - padding: 5px + width: 6px height: 100% + // Not sure why this defaults to shrinking + flex-shrink: 0 &:hover background-color: #efefef \ No newline at end of file diff --git a/src/pages/maps/@id/edit/edit-table.module.sass b/src/pages/maps/@id/edit/edit-table.module.sass index 7334fd26..c3283780 100644 --- a/src/pages/maps/@id/edit/edit-table.module.sass +++ b/src/pages/maps/@id/edit/edit-table.module.sass @@ -1,3 +1,8 @@ +.table-container + height: 100% + display: flex + flex-direction: column + td text-wrap: nowrap background-color: #ffffff78 @@ -5,16 +10,6 @@ td tr:nth-child(odd) td background-color: #ffffff -div.table-container - display: flex - flex-direction: column - overflow: auto - -div.interface - padding-left: 10px - padding-bottom: 10px - padding-top: 10px - div.input-form display: flex flex-direction: row From caa894493f6063c9a5acbacd990f8436b42b02e2 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 04:11:16 -0500 Subject: [PATCH 11/52] Add react-router for keeping focus on table --- src/pages/maps/@id/edit/edit-interface.ts | 28 +++++++++++++-------- src/pages/maps/@id/edit/edit-table.ts | 16 ++++++------ src/pages/maps/@id/edit/index.page.route.ts | 1 + 3 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 src/pages/maps/@id/edit/index.page.route.ts diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts index 13128d57..38993e2d 100644 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ b/src/pages/maps/@id/edit/edit-interface.ts @@ -3,8 +3,9 @@ import { ReactNode, useEffect } from "react"; import styles from "./edit-menu.module.sass"; import { useState } from "react"; import "~/styles/global.styl"; -import { Icon, Button } from "@blueprintjs/core"; import EditTable from "./edit-table"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import { LinkButton } from "~/map-interface/components/buttons"; const h = hyper.styled(styles); @@ -16,11 +17,11 @@ interface EditMenuProps { function EditMenu({ setActivePage }: EditMenuProps) { return h("div.edit-menu", {}, [ - h(Button, { + h(LinkButton, { icon: "polygon-filter", text: "Polygons", large: true, - onClick: () => setActivePage("polygons"), + to: "polygons", }), ]); } @@ -69,13 +70,20 @@ export default function EditInterface({ source_id }: EditInterfaceProps) { return h( WidthAdjustablePanel, - h([ - h.if(activePage == null)(EditMenu, { setActivePage }), - h.if(activePage == "polygons")( - EditTable, - { url: `http://localhost:8000/sources/${source_id}/polygons` }, - [] - ), + // TODO: make this basename dynamic + h(Router, { basename: `/maps/${source_id}/edit` }, [ + h(Routes, [ + h(Route, { + path: "", + element: h(EditMenu, { setActivePage }), + }), + h(Route, { + path: "polygons", + element: h(EditTable, { + url: `http://localhost:8000/sources/${source_id}/polygons`, + }), + }), + ]), ]) ); } diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 3a51b899..3d56a146 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -193,14 +193,14 @@ export default function EditTable({ url }) { }, [columns] ), - h(TablePagination, { - component: "div", - count: totalCount, - rowsPerPage: pageSize, - page: page, - onRowsPerPageChange: (e) => setPageSize(e.target.value), - onPageChange: (e, p) => setPage(p), - }), + // h(TablePagination, { + // component: "div", + // count: totalCount, + // rowsPerPage: pageSize, + // page: page, + // onRowsPerPageChange: (e) => setPageSize(e.target.value), + // onPageChange: (e, p) => setPage(p), + // }), ]), ]); } diff --git a/src/pages/maps/@id/edit/index.page.route.ts b/src/pages/maps/@id/edit/index.page.route.ts new file mode 100644 index 00000000..a5f8b371 --- /dev/null +++ b/src/pages/maps/@id/edit/index.page.route.ts @@ -0,0 +1 @@ +export default "/maps/@id/edit/*"; From b7cce26307027aa069f32f1dfc53b66b5484f223 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 05:57:48 -0500 Subject: [PATCH 12/52] Add cell editing capability and coloring --- src/pages/maps/@id/edit/edit-table.ts | 86 ++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 3d56a146..8ac3a149 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -1,6 +1,6 @@ import hyper from "@macrostrat/hyper"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner } from "@blueprintjs/core"; import { @@ -10,6 +10,7 @@ import { RowHeaderCell2, SelectionModes, } from "@blueprintjs/table"; +import update from "immutability-helper"; import { TablePagination } from "@mui/material"; import "@blueprintjs/table/lib/css/table.css"; @@ -67,14 +68,54 @@ export default function EditTable({ url }) { filters: [], }); - const cellRenderer = ({ key, row, cell }) => { - return h( - EditableCell2, - { onChange: (e) => console.log(), value: data[row][key] }, - [] - ); + // Sparse array to hold edited data + const [editedData, setEditedData] = useState(new Array()); + + const onChange = (key, row, text) => { + let rowSpec = {}; + if (text == null || text == "") { + rowSpec = { $unset: [key] }; + } else { + const rowOp = editedData[row] == null ? "$set" : "$merge"; + rowSpec = { [rowOp]: { [key]: text } }; + } + + const newData = update(editedData, { + [row]: rowSpec, + }); + setEditedData(newData); + }; + + const isValid = (key, row, text) => { + // Placeholder for future validation + return true; }; + const intentForCell = (key, row) => { + const _val = editedData[row]?.[key]; + if (_val != null) { + return isValid(key, row, _val) ? "success" : "danger"; + } + return "none"; + }; + + const cellRenderer = useCallback( + ({ key, row, cell }) => { + return h( + EditableCell2, + { + onConfirm: (value) => { + onChange(key, row, value); + }, + value: editedData[row]?.[key] ?? data[row][key], + intent: intentForCell(key, row), + }, + [] + ); + }, + [data, editedData] + ); + let getData = async () => { let dataURL = new URL(url); @@ -90,7 +131,7 @@ export default function EditTable({ url }) { useEffect(() => { getData(); - }, [page, pageSize, dataToggle]); + }, [page, pageSize]); if (data == undefined) { return h(Spinner); @@ -190,8 +231,11 @@ export default function EditTable({ url }) { onSelection: (selections: Selection[]) => getSelectionValues(selections), numRows: data.length, + // Dumb hacks to try to get the table to rerender on changes + cellRendererDependencies: [editedData], + enableFocusedCell: true, }, - [columns] + columns ), // h(TablePagination, { // component: "div", @@ -204,3 +248,27 @@ export default function EditTable({ url }) { ]), ]); } + +class TableDataManager { + /** Low-level manager for windowed loading of table data. This will eventually be how + * we work with the data, hopefully. */ + baseURL: string; + totalCount: number; + chunkSize: number = 100; + + init(baseURL: string) { + this.baseURL = baseURL; + } + + async getData(page: number) { + let dataURL = new URL(this.baseURL); + + dataURL.searchParams.append("page", page.toString()); + dataURL.searchParams.append("page_size", this.chunkSize.toString()); + + let response = await fetch(dataURL); + let data = await response.json(); + + this.totalCount = Number.parseInt(response.headers.get("X-Total-Count")); + } +} From 3fee7154fd74d8c78515e7a300bf0934a79c6fee Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 06:24:18 -0500 Subject: [PATCH 13/52] Added clear/submit buttons for changes --- src/pages/maps/@id/edit/edit-table.ts | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 8ac3a149..1a37a819 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -2,7 +2,7 @@ import hyper from "@macrostrat/hyper"; import { useState, useEffect, useCallback } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; -import { Spinner } from "@blueprintjs/core"; +import { Spinner, ButtonGroup } from "@blueprintjs/core"; import { Column, Table2, @@ -73,7 +73,7 @@ export default function EditTable({ url }) { const onChange = (key, row, text) => { let rowSpec = {}; - if (text == null || text == "") { + if (text == data[row][key] || (text == "" && data[row][key] == null)) { rowSpec = { $unset: [key] }; } else { const rowOp = editedData[row] == null ? "$set" : "$merge"; @@ -219,8 +219,25 @@ export default function EditTable({ url }) { className: "update-input-group", onChange: (e) => setInputValue(e.target.value), }), - h(Button, { type: "submit", onClick: () => submitChange(inputValue) }, [ - "Submit", + h(ButtonGroup, [ + h( + Button, + { + onClick: () => setEditedData(new Array()), + disabled: isEmptyArray(editedData), + }, + ["Clear changes"] + ), + h( + Button, + { + type: "submit", + onClick: () => submitChange(inputValue), + disabled: isEmptyArray(editedData), + intent: "success", + }, + ["Submit"] + ), ]), ]), h( @@ -249,6 +266,10 @@ export default function EditTable({ url }) { ]); } +function isEmptyArray(arr) { + return arr.length == 0 || arr.every((x) => x == null); +} + class TableDataManager { /** Low-level manager for windowed loading of table data. This will eventually be how * we work with the data, hopefully. */ From a682a94ea885002e9399bbbe6984e5fad5f46f64 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Sat, 4 Nov 2023 13:39:31 -0500 Subject: [PATCH 14/52] Change storybook to follow dark mode --- deps/web-components | 2 +- src/renderer/page-shell.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/deps/web-components b/deps/web-components index 7a944540..ccde2580 160000 --- a/deps/web-components +++ b/deps/web-components @@ -1 +1 @@ -Subproject commit 7a94454053404a6b2c2356baae1bb7effc999a76 +Subproject commit ccde258074b91b185c7a7557c6352a75a0f3a1d7 diff --git a/src/renderer/page-shell.ts b/src/renderer/page-shell.ts index 3574622a..cec62e75 100644 --- a/src/renderer/page-shell.ts +++ b/src/renderer/page-shell.ts @@ -16,6 +16,10 @@ export function PageShell({ pageContext: PageContext; }) { return h("div.app-shell", [ - h(PageContextProvider, { pageContext }, h(DarkModeProvider, children)), + h( + PageContextProvider, + { pageContext }, + h(DarkModeProvider, { followSystem: true }, children) + ), ]); } From db1c804ff22379ee11bc4f405698c07b8bb20fbc Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Sat, 4 Nov 2023 18:38:02 -0500 Subject: [PATCH 15/52] Add in Filtering --- package.json | 1 + src/pages/maps/@id/edit/EditTable.ts | 294 ++++++++++-------- src/pages/maps/@id/edit/TableMenu.ts | 112 +++++++ src/pages/maps/@id/edit/editTable.module.sass | 21 +- src/pages/maps/@id/edit/override.sass | 7 + src/pages/maps/@id/edit/table-util.ts | 51 +++ src/pages/maps/@id/edit/table.d.ts | 21 ++ yarn.lock | 112 ++----- 8 files changed, 406 insertions(+), 213 deletions(-) create mode 100644 src/pages/maps/@id/edit/TableMenu.ts create mode 100644 src/pages/maps/@id/edit/override.sass create mode 100644 src/pages/maps/@id/edit/table-util.ts create mode 100644 src/pages/maps/@id/edit/table.d.ts diff --git a/package.json b/package.json index 1eac5ca1..ba797ae2 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "topojson-client": "^3.0.0", "transition-hook": "^1.5.2", "ts-node": "^10.9.1", + "use-debounce": "^9.0.4", "use-react-router-breadcrumbs": "^3.2.1", "use-resize-observer": "^9.1.0", "vite": "^4.4.9", diff --git a/src/pages/maps/@id/edit/EditTable.ts b/src/pages/maps/@id/edit/EditTable.ts index ce20371a..99754e45 100644 --- a/src/pages/maps/@id/edit/EditTable.ts +++ b/src/pages/maps/@id/edit/EditTable.ts @@ -1,181 +1,231 @@ +// @ts-ignore import hyper from "@macrostrat/hyper"; -import { ReactElement, ReactFragment, useState, useEffect, useMemo } from "react"; +import {useState, useEffect, useLayoutEffect, useRef} from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner } from "@blueprintjs/core"; -import { Column, Table2, EditableCell2, RowHeaderCell2, SelectionModes } from "@blueprintjs/table"; -import {TablePagination} from "@mui/material" +import { Column, Table2, EditableCell2, RowHeaderCell2, ColumnHeaderCell2, SelectionModes } from "@blueprintjs/table"; + +import TableMenu from "./TableMenu"; import "@blueprintjs/table/lib/css/table.css"; +import "@blueprintjs/select/lib/css/blueprint-select.css"; + +import "./override.sass" import styles from "./editTable.module.sass"; +import {Filters, OperatorQueryParameter} from "./table"; +import {buildURL, Filter} from "./table-util.ts"; const h = hyper.styled(styles); -const range = (start, stop, step = 1) => - Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step) +const range = (start: number, stop: number, step = 1) => + Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step) interface Selection { - cols: number[]; - rows: number[]; + cols: number[]; + rows: number[]; } -class Filter { - constructor(column_name: string, operator: string, value: string){ - this.column_name = column_name - this.operator = operator - this.value = value - } - to_object = () => { - let o = {} - o[this.column_name] = this.operator + "." + this.value - return o - } - - to_array = () => { - return [this.column_name, this.operator + "." + this.value] - } -} -interface Filters { - [key: string]: Filter; -} interface TableSelection { - columns: string[]; - filters: Filters + columns: string[]; + filter: Filter; } -export default function EditTable({url}){ - // Table values - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(999999); - const [totalCount, setTotalCount] = useState(0); - const [data, setData] = useState(undefined); - const [dataToggle, setDataToggle] = useState(false); - const [inputValue, setInputValue] = useState(""); - const [tableSelection, setTableSelection] = useState<TableSelection>({columns: [], filters: []}) +interface EditTableProps { + url: string; +} + +export default function EditTable({url}: EditTableProps){ + + const [data, setData] = useState<any[]>([]); + const [inputValue, setInputValue] = useState<string>(""); + const [error, setError] = useState<string | undefined>(undefined) + const [filters, _setFilters] = useState<Filters>({}) + const [tableSelection, _setTableSelection] = useState<TableSelection>({columns: [], filter: new Filter("db_id", "in", "")}) + const [valueSelection, setValueSelection] = useState<string[]>([]) + + const setFilters = (filters: Filters) => { + _setFilters(filters) + } - const cellRenderer = ({key, row, cell}) => { + const setTableSelection = (selection: TableSelection) => { + _setTableSelection(selection) + } - return h(EditableCell2, {onChange: (e) => console.log(), "value": data[row][key]}, []) - } - let getData = async () => { + const columnHeaderCellRenderer = (columnIndex: number) => { - let dataURL = new URL(url) + const columnName: string = Object.keys(data[0])[columnIndex] - dataURL.searchParams.append("page", page.toString()) - dataURL.searchParams.append("page_size", pageSize.toString()) + const onChange = (param: OperaterQueryParameter) => { - let response = await fetch(dataURL) - let data = await response.json() + const columnFilter = new Filter(columnName, param.operator, param.value) - setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))) - setData(data) - } + setFilters({...filters, [columnName]: columnFilter}) + } - useEffect(() => { - getData() - }, [page, pageSize, dataToggle]) + let filter = filters[columnName] - if(data == undefined){ - return h(Spinner) - } + return h(ColumnHeaderCell2, { + menuRenderer: () => h(TableMenu, {"onChange": onChange, filter}), + name: columnName + }, []) + } - const columns = Object.keys(data[0]).filter(x => x != "db_id").map((key) => { - return h(Column, {name: key, cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), "key": key}) - }) + const cellRenderer = ({key, row, cell}) => { - const getSelectionValues = (selections: Selection[]) => { + return h(EditableCell2, {onChange: (e) => console.log(), "value": data[row][key]}, []) + } - if(selections.length == 0){ - setTableSelection({columns: [], filters: {...tableSelection.filters, "tableSelection": undefined}}) - return - } + let getData = async () => { - const rows = selections[0]?.rows - const cols = selections[0]?.cols + const dataURL = buildURL(url, Object.values(filters)) - const columnsKeys = Object.keys(data[0]) - const selectedColumnKeys = columnsKeys.slice(cols[0], cols[1] + 1) + const response = await fetch(dataURL) + const newData = await response.json() - let selection: TableSelection - if(rows == undefined){ - selection = {columns: selectedColumnKeys, ...tableSelection} + if(newData.length == 0){ + setError("Warning: No results matched query") + } else { + console.log("Data fetched successfully") - } else { - const selectedRowIndices = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length) - const dbIds = selectedRowIndices.map((row) => data[row]['db_id']) - const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")") + setError(undefined) + setData(newData) + } + return newData + } - selection = {columns: selectedColumnKeys, filters: {...tableSelection.filters, "tableSelection": filter}} - } + const dataFetched = useRef(false) + useLayoutEffect(() => { + if(!dataFetched.current){ + dataFetched.current = true + return + } + getData() + }, [tableSelection, filters]) - setTableSelection(selection) - } - const rowHeaderCellRenderer = (rowIndex: number) => { - return h(RowHeaderCell2, {name: data[rowIndex]['db_id']}, []) - } + useEffect(() => { + (async function () { + let data = await getData() + let newFilters: Filters = Object.keys(data[0]).reduce((original, key) => { + let originalFilters: Filters = {...original} + originalFilters[key] = new Filter(key, undefined, "") + return originalFilters + }, {}) + setFilters({...filters, ...newFilters}) + }()); + }, []) - const submitChange = async (value: string) => { - for (const column of tableSelection.columns) { + if(data.length == 0 && error == undefined){ + return h(Spinner) + } - let updateURL = new URL(url) - for(const filter: Filter of Object.values(tableSelection.filters)){ - updateURL.searchParams.append(...filter.to_array()) - } + const getSelectionValues = (selections: Selection[]) => { - let patch = {[column]: value} - console.log(patch, JSON.stringify(patch)) + if(selections.length == 0){ + setTableSelection({columns: [], filter: new Filter("db_id", "in", "")}) + return + } + const rows = selections[0]?.rows + const cols = selections[0]?.cols - let response = await fetch(updateURL, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(patch), - }) + const columnsKeys = Object.keys(data[0]) + const selectedColumnKeys: string[] = columnsKeys.slice(cols[0], cols[1] + 1) + const selectedRowIndices: number[] = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length) + + let selection: TableSelection + if(rows == undefined){ - if(response.status != 204){ - console.error("Failed to update", response) - } - } - setDataToggle(!dataToggle) - } + selection = {filter: new Filter("db_id", "in", ""), columns: selectedColumnKeys} + } else { + const dbIds = selectedRowIndices.map((row) => data[row]['db_id']) + const filter = new Filter("db_id", "in", "(" + dbIds.join(",") + ")") + selection = {columns: selectedColumnKeys, "filter": filter} + } - return h(HotkeysProvider, {}, [ - h("div.table-container", {}, [ - h("div.input-form", {}, [ - h(InputGroup, {"value": inputValue, className: "update-input-group", onChange: (e) => setInputValue(e.target.value)}), - h(Button, {type: "submit", onClick: () => submitChange(inputValue)}, ["Submit"]) - ]), - h(Table2, - { - selectionModes: SelectionModes.COLUMNS_AND_CELLS, - rowHeaderCellRenderer: rowHeaderCellRenderer, - onSelection: (selections: Selection[]) => getSelectionValues(selections), - numRows: data.length - }, - [ - columns - ] - ), - h(TablePagination, {component: "div", count: totalCount, rowsPerPage: pageSize, page: page, onRowsPerPageChange: (e) => setPageSize(e.target.value), onPageChange: (e, p) => setPage(p)}), - ]) - ]) + let valueSelection: string[] = [] + for(const column of selectedColumnKeys){ + for(const row of selectedRowIndices){ + valueSelection.push(data[row][column]) + } + } + + setValueSelection(valueSelection) + setTableSelection(selection) + } + + const rowHeaderCellRenderer = (rowIndex: number) => { + return h(RowHeaderCell2, {name: data[rowIndex]['db_id']}, []) + } + + + + const submitChange = async (value: string) => { + for (const column of tableSelection.columns) { + + let updateURL = buildURL(url, [tableSelection.filter, ...Object.values(filters)]) + + let patch = {[column]: value} + + + let response = await fetch(updateURL, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(patch), + }) + + if(response.status != 204){ + console.error("Failed to update", response) + } + } + getData() + } + + + return h(HotkeysProvider, {}, [ + h("div.table-container", {}, [ + h.if(error)("div.warning", {}, [error]), + h("div.input-form", {}, [ + h(InputGroup, {"value": inputValue, className: "update-input-group", onChange: (e) => setInputValue(e.target.value)}), + h(Button, {type: "submit", onClick: () => submitChange(inputValue)}, ["Submit"]) + ]), + h(Table2, + { + selectionModes: SelectionModes.COLUMNS_AND_CELLS, + rowHeaderCellRenderer: rowHeaderCellRenderer, + onSelection: (selections: Selection[]) => getSelectionValues(selections), + numRows: data.length + }, + [ + ...Object.keys(data[0]).filter(x => x != "db_id").map((key) => { + return h(Column, { + name: key, + columnHeaderCellRenderer: columnHeaderCellRenderer, + cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), + "key": key + }) + }) + ] + ) + ]) + ]) } \ No newline at end of file diff --git a/src/pages/maps/@id/edit/TableMenu.ts b/src/pages/maps/@id/edit/TableMenu.ts new file mode 100644 index 00000000..be119a3b --- /dev/null +++ b/src/pages/maps/@id/edit/TableMenu.ts @@ -0,0 +1,112 @@ +import {Button, Menu, MenuItem, InputGroup} from "@blueprintjs/core"; +import {Select2, ItemRenderer} from "@blueprintjs/select"; +import React from "react"; +import {useDebouncedCallback} from "use-debounce"; + +// @ts-ignore +import hyper from "@macrostrat/hyper"; + +import {OperatorQueryParameter, ColumnOperatorOption} from "./table"; + +import "@blueprintjs/core/lib/css/blueprint.css" +import "@blueprintjs/select/lib/css/blueprint-select.css"; +import styles from "./editTable.module.sass"; +import {Filter} from "./table-util.ts"; + + +const h = hyper.styled(styles); + + +const validExpressions: ColumnOperatorOption[] = [ + {key: "eq", value: "=", verbose: "Equals"}, + {key: "lt", value: "<", verbose: "Is less than"}, + {key: "le", value: "<=", verbose: "Is less than or equal to"}, + {key: "gt", value: ">", verbose: "Is greater than"}, + {key: "ge", value: ">=", verbose: "Is greater than or equal to"}, + {key: "ne", value: "<>", verbose: "Is not equal to"}, + {key: "like", value: "LIKE", verbose: "Like"}, + {key: "is", value: "IS", verbose: "Is", placeholder: "true | false | null"}, + {key: "in", value: "IN", verbose: "In", placeholder: "1,2,3"} +] + + + +const OperatorFilterOption: ItemRenderer<ColumnOperatorOption> = (column, { handleClick, handleFocus, modifiers }) => { + + return h(MenuItem, { + shouldDismissPopover: false, + active: modifiers.active, + disabled: modifiers.disabled, + key: column.key, + label: column.verbose, + onClick: handleClick, + onFocus: handleFocus, + text: column.value, + roleStructure:"listoption" + }, []) +} + +interface TableMenuProps { + onChange: (query: OperatorQueryParameter) => void; + filter: Filter; +} + +const TableMenu = ({onChange, filter} : TableMenuProps) => { + + const [menuOpen, setMenuOpen] = React.useState<boolean>(false); + const [inputPlaceholder, setInputPlaceholder] = React.useState<string>(""); + + // Create a debounced version of the text state + const [inputValue, setInputValue] = React.useState<string>(filter.value); + const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + console.log("Its morphin time!") + setMenuOpen(false); + onChange({operator: filter.operator, value: e.target.value}) + } + const debouncedInputChange = useDebouncedCallback(onInputChange, 1000); + + // Set the expression current value from the parent filter + const selectedExpression = validExpressions.find((expression) => expression.key === filter.operator); + + return h(Menu, {}, [ + h("div.filter-container", {}, [ + h("div.filter-header", {}, ["Filter"]), + h("div.filter-select", {}, [ + h(Select2<ColumnOperatorOption>, { + fill: true, + items: validExpressions, + className: "update-input-group", + filterable: false, + popoverProps: {isOpen: menuOpen}, + itemRenderer: OperatorFilterOption, + onItemSelect: (operator: ColumnOperatorOption) => { + setMenuOpen(false); + setInputPlaceholder(operator.placeholder || ""); + onChange({operator: operator.key, value: filter.value}) + }, + noResults: h(MenuItem, {disabled: true, text: "No results.", roleStructure: "listoption"}, []), + }, [ + h(Button, { + fill: true, + onClick: () => setMenuOpen(!menuOpen), + alignText: "left", + text: selectedExpression?.verbose, + rightIcon: "double-caret-vertical", + className: "update-input-group", + placeholder: "Select A Filter" + }, []) + ]), + ]), + h("div.filter-input", {}, [ + h(InputGroup, { + "value": inputValue, + className: "update-input-group", + placeholder: inputPlaceholder, + onChange: (e: React.ChangeEvent<HTMLInputElement>) => {setInputValue(e.target.value); debouncedInputChange(e)} + }, []) + ]) + ]) + ]) +} + +export default TableMenu; \ No newline at end of file diff --git a/src/pages/maps/@id/edit/editTable.module.sass b/src/pages/maps/@id/edit/editTable.module.sass index 7334fd26..00d26719 100644 --- a/src/pages/maps/@id/edit/editTable.module.sass +++ b/src/pages/maps/@id/edit/editTable.module.sass @@ -20,4 +20,23 @@ div.input-form flex-direction: row .update-input-group - flex-grow: 1 \ No newline at end of file + flex-grow: 1 + +div.filter-header + padding-bottom: .4rem + font-size: 1rem + +div.filter-select + padding-bottom: .2rem + +div.filter-container + box-shadow: #00000038 1px 1px 8px + padding: 10px + border-radius: 10px + +div.warning + background-color: rgb(255 216 152 / 29%) + color: black + border: #ffe26c solid 1px + font-size: 1rem + padding-bottom: 0.2rem \ No newline at end of file diff --git a/src/pages/maps/@id/edit/override.sass b/src/pages/maps/@id/edit/override.sass new file mode 100644 index 00000000..15352a31 --- /dev/null +++ b/src/pages/maps/@id/edit/override.sass @@ -0,0 +1,7 @@ + +// Need this to get the popups in front of the sidebar +.bp4-portal + z-index: 101 + +.bp4-menu + padding: 0px \ No newline at end of file diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts new file mode 100644 index 00000000..9c1e578d --- /dev/null +++ b/src/pages/maps/@id/edit/table-util.ts @@ -0,0 +1,51 @@ +import {ColumnOperators, Filters} from "./table"; + + +export class Filter { + readonly column_name: string; + readonly operator: ColumnOperators | undefined; + readonly value: string; + + constructor(column_name: string, operator: ColumnOperators | undefined, value: string){ + this.column_name = column_name + this.operator = operator + this.value = value + } + + get formattedValue(){ + switch (this.operator) { + case "in": + return `(${this.value})` + default: + return this.value + } + } + + is_valid = () => { + if(this.operator == undefined || this.value == ""){ + return false + } + return true + } + + to_array = () => { + return [this.column_name, this.operator + "." + this.formattedValue] + } + +} + + + +export function buildURL(baseURL: string, filters: Filter[]){ + let updateURL = new URL(baseURL) + + for(const filter of filters){ + + if(filter.is_valid()) { + const [key, value] = filter.to_array() + updateURL.searchParams.append(key, value) + } + } + + return updateURL +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/table.d.ts b/src/pages/maps/@id/edit/table.d.ts new file mode 100644 index 00000000..a57a7077 --- /dev/null +++ b/src/pages/maps/@id/edit/table.d.ts @@ -0,0 +1,21 @@ +import {Filter} from "./table-util.ts"; + + +export type ColumnOperators = "eq" | "lt" | "le" | "gt" | "ge" | "ne" | "like" | "in" | "is"; + +export interface ColumnOperatorOption { + key: ColumnOperators; + value: string; + verbose: string; + placeholder?: string; +} + +export interface OperatorQueryParameter { + operator: ColumnOperators | undefined; + value: string; +} + +interface Filters { + [key: string]: Filter; +} + diff --git a/yarn.lock b/yarn.lock index 64e704cf..f2a4f9c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1707,7 +1707,7 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/colors@npm:^4.0.0-alpha.3, @blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": +"@blueprintjs/colors@npm:^4.1.22, @blueprintjs/colors@npm:^4.2.1": version: 4.2.1 resolution: "@blueprintjs/colors@npm:4.2.1" dependencies: @@ -1716,33 +1716,7 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/core@npm:^3.52.0": - version: 3.54.0 - resolution: "@blueprintjs/core@npm:3.54.0" - dependencies: - "@blueprintjs/colors": ^4.0.0-alpha.3 - "@blueprintjs/icons": ^3.33.0 - "@juggle/resize-observer": ^3.3.1 - "@types/dom4": ^2.0.1 - classnames: ^2.2 - dom4: ^2.1.5 - normalize.css: ^8.0.1 - popper.js: ^1.16.1 - react-lifecycles-compat: ^3.0.4 - react-popper: ^1.3.7 - react-transition-group: ^2.9.0 - tslib: ~2.3.1 - peerDependencies: - react: ^15.3.0 || 16 || 17 - react-dom: ^15.3.0 || 16 || 17 - bin: - upgrade-blueprint-2.0.0-rename: scripts/upgrade-blueprint-2.0.0-rename.sh - upgrade-blueprint-3.0.0-rename: scripts/upgrade-blueprint-3.0.0-rename.sh - checksum: 97b8811bfc32284bb36e62a44210e84d5abe164ef553670866e0628718db4a98c79b9665f73014b1474f534a3d3260e94af274e669fb0ebfeb323305a81b5375 - languageName: node - linkType: hard - -"@blueprintjs/core@npm:^3||^4, @blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": +"@blueprintjs/core@npm:^4.14.1, @blueprintjs/core@npm:^4.17.8, @blueprintjs/core@npm:^4.18.0, @blueprintjs/core@npm:^4.20.2": version: 4.20.2 resolution: "@blueprintjs/core@npm:4.20.2" dependencies: @@ -1790,16 +1764,6 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/icons@npm:^3.33.0": - version: 3.33.0 - resolution: "@blueprintjs/icons@npm:3.33.0" - dependencies: - classnames: ^2.2 - tslib: ~2.3.1 - checksum: 9b1485a3ce17a97596b7fa7276ddbe85e33c56f061358351a626d353bf3eab6ab1b36a1860aec2feb7933ef0293c5f8e1f3342a89051720d1953343aab753cb3 - languageName: node - linkType: hard - "@blueprintjs/icons@npm:^4.14.5, @blueprintjs/icons@npm:^4.16.0": version: 4.16.0 resolution: "@blueprintjs/icons@npm:4.16.0" @@ -1811,23 +1775,6 @@ __metadata: languageName: node linkType: hard -"@blueprintjs/popover2@npm:^0.12.9": - version: 0.12.9 - resolution: "@blueprintjs/popover2@npm:0.12.9" - dependencies: - "@blueprintjs/core": ^3.52.0 - "@popperjs/core": ^2.5.4 - classnames: ^2.2 - dom4: ^2.1.5 - react-popper: ^2.2.4 - resize-observer-polyfill: ^1.5.1 - tslib: ~1.13.0 - peerDependencies: - react: ^16.8.0 || ^17 - checksum: f782600f3cd372abbdd47b9ca3ab9265c1e62e4b3b353d4b06c11c3663d62b01d597df11ba21aff9885a825c7960af878d8448922944489de4bc80c5019981de - languageName: node - linkType: hard - "@blueprintjs/popover2@npm:^1.13.12, @blueprintjs/popover2@npm:^1.14.11": version: 1.14.11 resolution: "@blueprintjs/popover2@npm:1.14.11" @@ -3515,12 +3462,15 @@ __metadata: version: 0.0.0-use.local resolution: "@macrostrat/data-sheet@workspace:deps/web-components/packages/data-sheet" dependencies: - "@blueprintjs/core": ^3||^4 - "@blueprintjs/popover2": ^0.12.9 + "@blueprintjs/core": ^4.17.8 + "@blueprintjs/popover2": ^1.13.12 "@macrostrat/hyper": ^1.2.12 + chroma-js: ^2.4.2 + classnames: ^2.3.1 d3-array: ^2.12.1 immutability-helper: ^3.1.1 - react: ^17.0.2 + react: ^17.0.2||^18 + react-color: ^2.19.3 react-datasheet: ^1.4.9 react-dnd: ^14.0.2 react-dnd-html5-backend: ^14.0.0 @@ -3954,6 +3904,7 @@ __metadata: transition-hook: ^1.5.2 ts-node: ^10.9.1 typescript: ^5.1.6 + use-debounce: ^9.0.4 use-react-router-breadcrumbs: ^3.2.1 use-resize-observer: ^9.1.0 vite: ^4.4.9 @@ -5447,7 +5398,7 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.5.4, @popperjs/core@npm:^2.9.3": +"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 @@ -6681,13 +6632,6 @@ __metadata: languageName: node linkType: hard -"@types/dom4@npm:^2.0.1": - version: 2.0.3 - resolution: "@types/dom4@npm:2.0.3" - checksum: b8b3d73b4c3fafa0080feb44bb3aeb1718ce0878d18f5ae86ed480bf012d16185afcd82afcd965be4636176de94c351ee3fe74b40430385c3ac8f9192ef86b1b - languageName: node - linkType: hard - "@types/dom4@npm:^2.0.2": version: 2.0.2 resolution: "@types/dom4@npm:2.0.2" @@ -10249,7 +10193,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:*, classnames@npm:^2.2, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": +"classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.0, classnames@npm:^2.3.1, classnames@npm:^2.3.2": version: 2.3.2 resolution: "classnames@npm:2.3.2" checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e @@ -24931,7 +24875,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^1.3.11, react-popper@npm:^1.3.7": +"react-popper@npm:^1.3.11": version: 1.3.11 resolution: "react-popper@npm:1.3.11" dependencies: @@ -24948,7 +24892,7 @@ __metadata: languageName: node linkType: hard -"react-popper@npm:^2.2.4, react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": +"react-popper@npm:^2.2.5, react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" dependencies: @@ -25878,13 +25822,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094 - languageName: node - linkType: hard - "resolve-alpn@npm:^1.0.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -28727,20 +28664,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:~1.13.0": - version: 1.13.0 - resolution: "tslib@npm:1.13.0" - checksum: 50e9327361f94f328c0715582a7f725f69838ab3c2559d143643c5367262fe14552768ba8cfc65bc7dc924a619aea599b3a28b6653458cdca77bbebaf9bc8df4 - languageName: node - linkType: hard - -"tslib@npm:~2.3.1": - version: 2.3.1 - resolution: "tslib@npm:2.3.1" - checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 - languageName: node - linkType: hard - "tslib@npm:~2.5.0": version: 2.5.3 resolution: "tslib@npm:2.5.3" @@ -29610,6 +29533,15 @@ __metadata: languageName: node linkType: hard +"use-debounce@npm:^9.0.4": + version: 9.0.4 + resolution: "use-debounce@npm:9.0.4" + peerDependencies: + react: ">=16.8.0" + checksum: 37da4ecbe4e10a6230580cac03a8cae1788ea3e417dfdd92fcf654325458cf1b4567fd57bebf888edab62701a6abe47059a585008fd04228784f223f94d66ce4 + languageName: node + linkType: hard + "use-element-dimensions@npm:^2.1.3": version: 2.1.3 resolution: "use-element-dimensions@npm:2.1.3" From 7056dc2c73d8e113872327f000ab1fdf53f41a27 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Sun, 5 Nov 2023 10:36:59 -0600 Subject: [PATCH 16/52] Add in ability to group columns --- src/pages/maps/@id/edit/edit-table.ts | 25 ++++++++++++++------- src/pages/maps/@id/edit/table-menu.ts | 32 ++++++++++++++++++++------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index b8e55376..930b98f6 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -10,6 +10,7 @@ import { RowHeaderCell2, ColumnHeaderCell2, SelectionModes, + RegionCardinality } from "@blueprintjs/table"; import update from "immutability-helper"; @@ -54,6 +55,7 @@ export default function EditTable({ url }) { const [inputValue, setInputValue] = useState<string>(""); const [error, setError] = useState<string | undefined>(undefined) const [filters, setFilters] = useState<Filters>({}) + const [group, setGroup] = useState<string | undefined>(undefined) const [tableSelection, setTableSelection] = useState<TableSelection>({columns: [], filter: new Filter("_pkid", "in", "")}) @@ -92,7 +94,7 @@ export default function EditTable({ url }) { const columnName: string = Object.keys(data[0])[columnIndex] - const onChange = (param: OperatorQueryParameter) => { + const onFilterChange = (param: OperatorQueryParameter) => { const columnFilter = new Filter(columnName, param.operator, param.value) @@ -103,10 +105,10 @@ export default function EditTable({ url }) { return h(ColumnHeaderCell2, { - menuRenderer: () => h(TableMenu, {"onChange": onChange, filter}), + menuRenderer: () => h(TableMenu, {"onFilterChange": onFilterChange, filter, "onGroupChange": setGroup, group}), name: columnName, style: { - backgroundColor: filter.is_valid() ? "#ff1b651f" : "#ffffff00" + backgroundColor: filter.is_valid() || group == columnName ? "rgba(27,187,255,0.12)" : "#ffffff00" } }, []) } @@ -130,7 +132,7 @@ export default function EditTable({ url }) { let getData = async () => { - const dataURL = buildURL(url, Object.values(filters)) + const dataURL = buildURL(url, Object.values(filters), group) const response = await fetch(dataURL) const newData = await response.json() @@ -168,7 +170,7 @@ export default function EditTable({ url }) { return } getData() - }, [dataToggle, filters]) + }, [dataToggle, filters, group]) if(data.length == 0 && error == undefined){ return h(Spinner) @@ -212,14 +214,21 @@ export default function EditTable({ url }) { } const rowHeaderCellRenderer = (rowIndex: number) => { - return h(RowHeaderCell2, { name: data[rowIndex]["_pkid"] }, []); + const headerKey = group ? group : "_pkid" + let name = data[rowIndex][headerKey] + if(name.length > 47){ + name = name.slice(0, 47) + "..." + } + + + return h(RowHeaderCell2, { "name": name }, []); }; const submitChange = async (value: string) => { for (const column of tableSelection.columns) { let updateURL = new URL(url); - for (const filter: Filter of Object.values(tableSelection.filters)) { + for (const filter of Object.values(tableSelection.filters)) { updateURL.searchParams.append(...filter.to_array()); } @@ -274,7 +283,7 @@ export default function EditTable({ url }) { h( Table2, { - selectionModes: SelectionModes.COLUMNS_AND_CELLS, + selectionModes: group ? RegionCardinality.CELLS : SelectionModes.COLUMNS_AND_CELLS, rowHeaderCellRenderer: rowHeaderCellRenderer, onSelection: (selections: Selection[]) => getSelectionValues(selections), diff --git a/src/pages/maps/@id/edit/table-menu.ts b/src/pages/maps/@id/edit/table-menu.ts index 78cc46bb..8a948c31 100644 --- a/src/pages/maps/@id/edit/table-menu.ts +++ b/src/pages/maps/@id/edit/table-menu.ts @@ -11,7 +11,7 @@ import {OperatorQueryParameter, ColumnOperatorOption} from "./table"; import "@blueprintjs/core/lib/css/blueprint.css" import "@blueprintjs/select/lib/css/blueprint-select.css"; import styles from "./edit-table.module.sass"; -import {Filter} from "./table-util.ts"; +import {Filter} from "./table-util"; const h = hyper.styled(styles); @@ -47,11 +47,13 @@ const OperatorFilterOption: ItemRenderer<ColumnOperatorOption> = (column, { hand } interface TableMenuProps { - onChange: (query: OperatorQueryParameter) => void; + onFilterChange: (query: OperatorQueryParameter) => void; filter: Filter; + onGroupChange: (group: string | undefined) => void; + group: string | undefined; } -const TableMenu = ({onChange, filter} : TableMenuProps) => { +const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuProps) => { const [menuOpen, setMenuOpen] = React.useState<boolean>(false); const [inputPlaceholder, setInputPlaceholder] = React.useState<string>(""); @@ -59,15 +61,17 @@ const TableMenu = ({onChange, filter} : TableMenuProps) => { // Create a debounced version of the text state const [inputValue, setInputValue] = React.useState<string>(filter.value); const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - console.log("Its morphin time!") setMenuOpen(false); - onChange({operator: filter.operator, value: e.target.value}) + onFilterChange({operator: filter.operator, value: e.target.value}) } const debouncedInputChange = useDebouncedCallback(onInputChange, 1000); // Set the expression current value from the parent filter const selectedExpression = validExpressions.find((expression) => expression.key === filter.operator); + // Set if this group is active + const groupActive: boolean = group === filter.column_name; + return h(Menu, {}, [ h("div.filter-container", {}, [ h("div.filter-header", {}, ["Filter"]), @@ -82,7 +86,7 @@ const TableMenu = ({onChange, filter} : TableMenuProps) => { onItemSelect: (operator: ColumnOperatorOption) => { setMenuOpen(false); setInputPlaceholder(operator.placeholder || ""); - onChange({operator: operator.key, value: filter.value}) + onFilterChange({operator: operator.key, value: filter.value}) }, noResults: h(MenuItem, {disabled: true, text: "No results.", roleStructure: "listoption"}, []), }, [ @@ -102,9 +106,21 @@ const TableMenu = ({onChange, filter} : TableMenuProps) => { "value": inputValue, className: "update-input-group", placeholder: inputPlaceholder, - onChange: (e: React.ChangeEvent<HTMLInputElement>) => {setInputValue(e.target.value); debouncedInputChange(e)} + onFilterChange: (e: React.ChangeEvent<HTMLInputElement>) => {setInputValue(e.target.value); debouncedInputChange(e)} }, []) - ]) + ]), + h("div.filter-header", {}, ["Group"]), + h("div.filter-select", {}, [ + h(Button, + { + rightIcon: groupActive ? "tick" : "disable", + alignText: "left", + intent: groupActive ? "success" : "warning", + text: groupActive ? "Active" : "Inactive", + fill: true, + onClick: () => onGroupChange(filter.column_name) + }, []) + ]), ]) ]) } From ebf765fea79e010bb67ac7efad4e58edae51ec2b Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Sun, 5 Nov 2023 10:49:48 -0600 Subject: [PATCH 17/52] Color id the columns used for final update --- src/pages/maps/@id/edit/edit-table.ts | 15 +++++++++++++++ src/pages/maps/@id/edit/override.sass | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 930b98f6..cf0d5776 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -44,6 +44,20 @@ interface TableSelection { filters: Filters; } +const FINAL_COLUMNS = [ + "source_id", + "orig_id", + "descrip", + "ready", + "name", + "strat_name", + "age", + "lith", + "comments", + "t_interval", + "b_interval" +] + export default function EditTable({ url }) { // Table values const [page, setPage] = useState(0); @@ -179,6 +193,7 @@ export default function EditTable({ url }) { const columns = Object.keys(data[0]).filter(x => x != "_pkid").map((key) => { return h(Column, { name: key, + className: FINAL_COLUMNS.includes(key) ? "final-column" : "", columnHeaderCellRenderer: columnHeaderCellRenderer, cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), "key": key diff --git a/src/pages/maps/@id/edit/override.sass b/src/pages/maps/@id/edit/override.sass index 15352a31..840bb7ef 100644 --- a/src/pages/maps/@id/edit/override.sass +++ b/src/pages/maps/@id/edit/override.sass @@ -4,4 +4,8 @@ z-index: 101 .bp4-menu - padding: 0px \ No newline at end of file + padding: 0px + +// For the table column highlighting +.final-column + box-shadow: inset 0 -1px 0 rgba(39, 201, 236, 0.55), inset -1px 0 0 rgba(39, 201, 236, 0.55) !important \ No newline at end of file From 215a7bcda1161cf1095199e55d81748f37f4f74e Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Sun, 5 Nov 2023 11:17:38 -0600 Subject: [PATCH 18/52] Sort by _pkid on default and fix group bug --- src/pages/maps/@id/edit/edit-table.ts | 13 ++++++++++++- src/pages/maps/@id/edit/table-util.ts | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index cf0d5776..c549426d 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -148,6 +148,13 @@ export default function EditTable({ url }) { const dataURL = buildURL(url, Object.values(filters), group) + if(group == undefined){ + dataURL.searchParams.append("_pkid", "order_by" ) + } + + dataURL.searchParams.append("page", page.toString()); + dataURL.searchParams.append("page_size", pageSize.toString()); + const response = await fetch(dataURL) const newData = await response.json() @@ -158,6 +165,7 @@ export default function EditTable({ url }) { setError(undefined) setData(newData) + setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))); } return newData @@ -231,7 +239,10 @@ export default function EditTable({ url }) { const rowHeaderCellRenderer = (rowIndex: number) => { const headerKey = group ? group : "_pkid" let name = data[rowIndex][headerKey] - if(name.length > 47){ + + if (name == null) { + name = "NULL"; + } else if(name.length > 47){ name = name.slice(0, 47) + "..." } diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts index 9c1e578d..8a20aabd 100644 --- a/src/pages/maps/@id/edit/table-util.ts +++ b/src/pages/maps/@id/edit/table-util.ts @@ -36,7 +36,7 @@ export class Filter { -export function buildURL(baseURL: string, filters: Filter[]){ +export function buildURL(baseURL: string, filters: Filter[], group: string | undefined){ let updateURL = new URL(baseURL) for(const filter of filters){ @@ -47,5 +47,9 @@ export function buildURL(baseURL: string, filters: Filter[]){ } } + if(group != undefined){ + updateURL.searchParams.append(group, "group_by") + } + return updateURL } \ No newline at end of file From 29a90c2f8ff97aecca6f5b3bba47d75123cc9b29 Mon Sep 17 00:00:00 2001 From: Daven Quinn <dev@davenquinn.com> Date: Mon, 6 Nov 2023 13:23:44 -0600 Subject: [PATCH 19/52] Made data sheet the dominant object on the page --- .vscode/settings.json | 30 +++++- deps/web-components | 2 +- src/components/map-navbar/index.ts | 25 ++++- src/components/map-navbar/main.module.sass | 8 ++ src/pages/maps/@id/edit/components/index.ts | 1 + .../maps/@id/edit/components/main.module.sass | 22 +++++ src/pages/maps/@id/edit/components/panels.ts | 80 ++++++++++++++++ src/pages/maps/@id/edit/edit-interface.ts | 89 ------------------ ...menu.module.sass => edit-page.module.sass} | 41 +++++---- src/pages/maps/@id/edit/edit-page.ts | 92 +++++++++++++++++++ src/pages/maps/@id/edit/index.page.ts | 4 +- src/pages/maps/@id/edit/main.module.sass | 4 +- src/pages/maps/@id/edit/map-interface.ts | 12 +-- 13 files changed, 285 insertions(+), 125 deletions(-) create mode 100644 src/pages/maps/@id/edit/components/index.ts create mode 100644 src/pages/maps/@id/edit/components/main.module.sass create mode 100644 src/pages/maps/@id/edit/components/panels.ts delete mode 100644 src/pages/maps/@id/edit/edit-interface.ts rename src/pages/maps/@id/edit/{edit-menu.module.sass => edit-page.module.sass} (55%) create mode 100644 src/pages/maps/@id/edit/edit-page.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 97569907..211abe2d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,33 @@ }, "typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, - "prettier.prettierPath": ".yarn/sdks/prettier/index.js" + "prettier.prettierPath": ".yarn/sdks/prettier/index.js", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/__pycache__": true, + "[!s]*/**": true, + "s[!r]*/**": true, + "sr[!c]*/**": true, + "src/[!p]*/**": true, + "src/p[!a]*/**": true, + "src/pa[!g]*/**": true, + "src/pag[!e]*/**": true, + "src/page[!s]*/**": true, + "src/pages/[!m]*/**": true, + "src/pages/m[!a]*/**": true, + "src/pages/ma[!p]*/**": true, + "src/pages/map[!s]*/**": true, + "src/pages/maps/[!@]*/**": true, + "src/pages/maps/@[!i]*/**": true, + "src/pages/maps/@i[!d]*/**": true, + "src/pages/maps/@id/[!e]*/**": true, + "src/pages/maps/@id/e[!d]*/**": true, + "src/pages/maps/@id/ed[!i]*/**": true, + "src/pages/maps/@id/edi[!t]*/**": true + } } diff --git a/deps/web-components b/deps/web-components index 3db80aee..848169e0 160000 --- a/deps/web-components +++ b/deps/web-components @@ -1 +1 @@ -Subproject commit 3db80aee7cf806011ef21fad4ae4d08ae5de956d +Subproject commit 848169e012f9366dff72aeb349b4a88d244570f7 diff --git a/src/components/map-navbar/index.ts b/src/components/map-navbar/index.ts index 315050b4..f0c9111d 100644 --- a/src/components/map-navbar/index.ts +++ b/src/components/map-navbar/index.ts @@ -20,7 +20,16 @@ export function ParentRouteButton({ return h(LinkButton, { to: "..", icon, minimal: true, ...rest }); } -export function MapNavbar({ title, isOpen, setOpen, parentRoute }) { +export function MapNavbar({ + title, + isOpen, + setOpen, + parentRoute, + minimal = false, +}) { + if (minimal) { + return MapMinimalNavbar({ isOpen, setOpen }); + } const { isLoading } = useMapStatus(); return h(FloatingNavbar, { className: "searchbar map-navbar" }, [ h([h(ParentRouteButton, { parentRoute }), h("h2.map-title", title)]), @@ -32,3 +41,17 @@ export function MapNavbar({ title, isOpen, setOpen, parentRoute }) { }), ]); } + +function MapMinimalNavbar({ isOpen, setOpen }) { + const { isLoading } = useMapStatus(); + return h("div.map-minimal-navbar map-navbar", [ + h(FloatingNavbar, { className: "searchbar" }, [ + h(MapLoadingButton, { + active: isOpen, + onClick: () => setOpen(!isOpen), + isLoading, + }), + ]), + h("div.spacer"), + ]); +} diff --git a/src/components/map-navbar/main.module.sass b/src/components/map-navbar/main.module.sass index 3ad0a2a9..2be9f8e1 100644 --- a/src/components/map-navbar/main.module.sass +++ b/src/components/map-navbar/main.module.sass @@ -12,3 +12,11 @@ .map-title // Allow the title to wrap on hover overflow-x: visible + +.map-minimal-navbar + display: flex + flex-direction: row + &>.spacer + flex-grow: 1 + .map-navbar + min-width: 0 \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/index.ts b/src/pages/maps/@id/edit/components/index.ts new file mode 100644 index 00000000..58584ec0 --- /dev/null +++ b/src/pages/maps/@id/edit/components/index.ts @@ -0,0 +1 @@ +export * from "./panels"; diff --git a/src/pages/maps/@id/edit/components/main.module.sass b/src/pages/maps/@id/edit/components/main.module.sass new file mode 100644 index 00000000..a90171cc --- /dev/null +++ b/src/pages/maps/@id/edit/components/main.module.sass @@ -0,0 +1,22 @@ +.width-adjustable-panel + transition: max-width 0.1s ease-in-out + height: 100% + display: flex + flex-direction: row + position: relative + +.width-adjustable-panel-content + overflow: scroll + height: 100% + flex-grow: 1 + padding: 1em + +.width-adjuster + cursor: col-resize + width: 6px + height: 100% + // Not sure why this defaults to shrinking + flex-shrink: 0 + background-color: #efefef + &:hover + background-color: #ddd \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/panels.ts b/src/pages/maps/@id/edit/components/panels.ts new file mode 100644 index 00000000..7e59e452 --- /dev/null +++ b/src/pages/maps/@id/edit/components/panels.ts @@ -0,0 +1,80 @@ +import { ReactNode, useEffect } from "react"; +import { useRef } from "react"; +import { useStoredState } from "@macrostrat/ui-components"; +import hyper from "@macrostrat/hyper"; +import styles from "./main.module.sass"; +import { on } from "events"; +export const h = hyper.styled(styles); + +export enum AdjustSide { + LEFT = "left", + RIGHT = "right", +} + +export function WidthAdjustablePanel({ + children, + adjustSide = AdjustSide.RIGHT, + expand, + className, + storageID = null, +}: { + children: ReactNode; + adjustSide?: AdjustSide; + expand?: boolean; + className?: string; + storageID?: string; +}) { + const [maxWidth, setMaxWidth] = useStoredState( + storageID, + 0, + (v) => typeof v === "number" + ); + + useEffect(() => { + if (typeof window === "undefined") return; + setMaxWidth(window.innerWidth / 2); + }, []); + + if (expand) { + return h("div.width-adjustable-panel", { className }, [ + h("div.width-adjustable-panel-content", {}, children), + ]); + } + return h( + "div.width-adjustable-panel", + { style: { maxWidth: maxWidth + "px" }, className }, + [ + h.if(adjustSide == AdjustSide.LEFT)(WidthAdjuster, { + onAdjust: (dx) => { + const newMaxWidth = maxWidth - dx; + setMaxWidth(newMaxWidth); + }, + }), + h("div.width-adjustable-panel-content", {}, children), + h.if(adjustSide == AdjustSide.RIGHT)(WidthAdjuster, { + onAdjust: (dx) => { + const newMaxWidth = maxWidth + dx; + setMaxWidth(newMaxWidth); + }, + }), + ] + ); +} + +function WidthAdjuster({ onAdjust }: { onAdjust: (dx: number) => void }) { + const startPosition = useRef(0); + return h( + "div.width-adjuster", + { + onDragStart: (e) => { + startPosition.current = e.clientX; + }, + onDragEnd: (e) => { + const dx = e.clientX - startPosition.current; + onAdjust(dx); + }, + draggable: true, + }, + [] + ); +} diff --git a/src/pages/maps/@id/edit/edit-interface.ts b/src/pages/maps/@id/edit/edit-interface.ts deleted file mode 100644 index 38993e2d..00000000 --- a/src/pages/maps/@id/edit/edit-interface.ts +++ /dev/null @@ -1,89 +0,0 @@ -import hyper from "@macrostrat/hyper"; -import { ReactNode, useEffect } from "react"; -import styles from "./edit-menu.module.sass"; -import { useState } from "react"; -import "~/styles/global.styl"; -import EditTable from "./edit-table"; -import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; -import { LinkButton } from "~/map-interface/components/buttons"; - -const h = hyper.styled(styles); - -interface TableProps {} - -interface EditMenuProps { - setActivePage: (page: string) => void; -} - -function EditMenu({ setActivePage }: EditMenuProps) { - return h("div.edit-menu", {}, [ - h(LinkButton, { - icon: "polygon-filter", - text: "Polygons", - large: true, - to: "polygons", - }), - ]); -} - -function WidthAdjustablePanel({ children }: { children: ReactNode }) { - const [maxWidth, setMaxWidth] = useState(0); - const [startPosition, setStartPosition] = useState(0); - - useEffect(() => { - if (typeof window === "undefined") return; - setMaxWidth(window.innerWidth / 2); - }, []); - - return h( - "div.width-adjustable-panel", - { style: { maxWidth: maxWidth + "px" } }, - [ - h( - "div.width-adjuster", - { - onDragStart: (e) => { - setStartPosition(e.clientX); - }, - onDragEnd: (e) => { - const dx = e.clientX - startPosition; - const newMaxWidth = maxWidth - dx; - setMaxWidth(newMaxWidth); - }, - draggable: true, - }, - [] - ), - h("div.width-adjustable-panel-content", {}, children), - ] - ); -} - -interface EditInterfaceProps { - title?: string; - parentRoute?: string; - source_id?: number; -} - -export default function EditInterface({ source_id }: EditInterfaceProps) { - const [activePage, setActivePage] = useState(null); - - return h( - WidthAdjustablePanel, - // TODO: make this basename dynamic - h(Router, { basename: `/maps/${source_id}/edit` }, [ - h(Routes, [ - h(Route, { - path: "", - element: h(EditMenu, { setActivePage }), - }), - h(Route, { - path: "polygons", - element: h(EditTable, { - url: `http://localhost:8000/sources/${source_id}/polygons`, - }), - }), - ]), - ]) - ); -} diff --git a/src/pages/maps/@id/edit/edit-menu.module.sass b/src/pages/maps/@id/edit/edit-page.module.sass similarity index 55% rename from src/pages/maps/@id/edit/edit-menu.module.sass rename to src/pages/maps/@id/edit/edit-page.module.sass index 3c943aa3..8c329d4d 100644 --- a/src/pages/maps/@id/edit/edit-menu.module.sass +++ b/src/pages/maps/@id/edit/edit-page.module.sass @@ -1,3 +1,24 @@ +.edit-page + display: flex + flex-direction: row + height: 100vh + width: 100vw + overflow: hidden + +.edit-page-header + display: flex + flex-direction: row + +.spacer + flex-grow: 1 + +.map-panel-container, .edit-page-content + flex: 1 + min-width: 0 + +.edit-menu + flex-grow: 1 + div.interface height: 100% background-color: #efefef @@ -17,28 +38,8 @@ button.icon-button .icon-label padding-bottom: 10px -.width-adjustable-panel - transition: max-width 0.1s ease-in-out - height: 100% - display: flex - flex-direction: row - position: relative -.width-adjustable-panel-content - overflow: scroll - height: 100% - flex-grow: 1 - padding: 1em div.edit-table-wrapper overflow: scroll width: 100% - -div.width-adjuster - cursor: col-resize - width: 6px - height: 100% - // Not sure why this defaults to shrinking - flex-shrink: 0 - &:hover - background-color: #efefef \ No newline at end of file diff --git a/src/pages/maps/@id/edit/edit-page.ts b/src/pages/maps/@id/edit/edit-page.ts new file mode 100644 index 00000000..39f2b082 --- /dev/null +++ b/src/pages/maps/@id/edit/edit-page.ts @@ -0,0 +1,92 @@ +import hyper from "@macrostrat/hyper"; +import styles from "./edit-page.module.sass"; +import { useState } from "react"; +import EditTable from "./edit-table"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import { LinkButton } from "~/map-interface/components/buttons"; +import { WidthAdjustablePanel } from "./components"; +import MapInterface from "./map-interface"; +import { useStoredState } from "@macrostrat/ui-components"; +import { ParentRouteButton } from "~/components/map-navbar"; +import { Button } from "@blueprintjs/core"; + +export const h = hyper.styled(styles); + +function EditMenu() { + return h("div.edit-menu", {}, [ + h(LinkButton, { + icon: "polygon-filter", + text: "Polygons", + large: true, + to: "polygons", + }), + ]); +} + +interface EditInterfaceProps { + title?: string; + parentRoute?: string; + source_id?: number; + mapBounds?: any; +} + +export default function EditInterface({ + source_id, + mapBounds, +}: EditInterfaceProps) { + const [showMap, setShowMap] = useStoredState( + "edit:showMap", + true, + // Check if is valid boolean + (v) => typeof v === "boolean" + ); + + const title = mapBounds.properties.name; + + return h("div.edit-page", [ + h( + WidthAdjustablePanel, + { + expand: !showMap, + className: "edit-page-content", + storageID: "edit-panel-width", + }, + // TODO: make this basename dynamic + h([ + h("div.edit-page-header", [ + h(ParentRouteButton, { parentRoute: "/maps/" }), + h("h2", title), + h("div.spacer"), + h("div.edit-page-buttons", [ + h(ShowMapButton, { showMap, setShowMap }), + ]), + ]), + h(Router, { basename: `/maps/${source_id}/edit` }, [ + h(Routes, [ + h(Route, { + path: "", + element: h(EditMenu), + }), + h(Route, { + path: "polygons", + element: h(EditTable, { + url: `http://localhost:8000/sources/${source_id}/polygons`, + }), + }), + ]), + ]), + ]) + ), + h.if(showMap)(MapInterface, { id: source_id, map: mapBounds }), + ]); +} + +function ShowMapButton({ showMap, setShowMap }) { + return h(Button, { + minimal: true, + icon: "map", + large: true, + intent: showMap ? "primary" : "none", + onClick: () => setShowMap(!showMap), + }); +} diff --git a/src/pages/maps/@id/edit/index.page.ts b/src/pages/maps/@id/edit/index.page.ts index 209e6352..559f8e79 100644 --- a/src/pages/maps/@id/edit/index.page.ts +++ b/src/pages/maps/@id/edit/index.page.ts @@ -30,11 +30,11 @@ export async function onBeforeRender(pageContext: PageContextBuiltInServer) { }; } -const MapInterface = () => import("./map-interface"); +const EditInterface = () => import("./edit-page"); export function Page({ id, map }) { return h( "div.single-map", - h(ClientOnly, { component: MapInterface, id, map }) + h(ClientOnly, { component: EditInterface, source_id: id, mapBounds: map }) ); } diff --git a/src/pages/maps/@id/edit/main.module.sass b/src/pages/maps/@id/edit/main.module.sass index f5cf3b3d..6a9080ad 100644 --- a/src/pages/maps/@id/edit/main.module.sass +++ b/src/pages/maps/@id/edit/main.module.sass @@ -6,8 +6,8 @@ body --map-detail-stack-width: fit-content .single-map - width: 100vh - height: 100vh + flex: 1 + height: 100% margin: 0 --map-context-stack-width: 16em diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index e633d18c..cb376e55 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -16,7 +16,6 @@ import { MapNavbar } from "~/components/map-navbar"; import { SETTINGS } from "~/map-interface/settings"; import "~/styles/global.styl"; import { s3Address, tempImageIndex } from "../../raster-images"; -import EditInterface from "./edit-interface"; import styles from "./main.module.sass"; const h = hyper.styled(styles); @@ -205,16 +204,11 @@ export default function MapInterface({ id, map }) { MapAreaContainer, { className: "single-map", - navbar: h(MapNavbar, { title, parentRoute: "/maps", isOpen, setOpen }), + navbar: h(MapNavbar, { isOpen, setOpen, minimal: true }), contextPanel, contextPanelOpen: isOpen, - detailPanelOpen: true, - detailPanelStyle: "fixed", - detailPanel: h( - EditInterface, - { title: "Source 1", parentRoute: "/maps/", source_id: id }, - [] - ), + detailPanelOpen: false, + fitViewport: false, }, [ h( From f98c485fa0b61fcc30ffb8c6a858f0ff19dd2827 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 7 Nov 2023 08:42:31 -0600 Subject: [PATCH 20/52] Add Dockerfile --- Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..956ec398 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20 AS build + +ENV NODE_ENV=production + +WORKDIR /usr/src/app +COPY . ./ + +RUN yarn cache clean +RUN yarn add + +CMD ["sh", "server/server.sh"] From 45a6d54b5318a80151fc85d2f4917f0ad1455bd7 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 7 Nov 2023 12:12:28 -0600 Subject: [PATCH 21/52] Add variable for url --- .env.example | 1 + src/pages/maps/@id/edit/edit-page.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index f15d8604..c2d4a216 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,6 @@ VITE_MACROSTRAT_TILESERVER_V2='https://dev.macrostrat.org/tiles' VITE_MACROSTRAT_TILESERVER_V1='https://tiles.macrostrat.org' VITE_MACROSTRAT_TILESERVER_DOMAIN='https://dev.macrostrat.org/tiles' VITE_MACROSTRAT_API_DOMAIN='https://macrostrat.org' +VITE_MACROSTRAT_INGEST_API=https://dev.macrostrat.org/api/ingest VITE_CORELLE_API_DOMAIN='https://rotate.macrostrat.org' PUBLIC_URL='/' \ No newline at end of file diff --git a/src/pages/maps/@id/edit/edit-page.ts b/src/pages/maps/@id/edit/edit-page.ts index 39f2b082..5f839ebd 100644 --- a/src/pages/maps/@id/edit/edit-page.ts +++ b/src/pages/maps/@id/edit/edit-page.ts @@ -70,7 +70,7 @@ export default function EditInterface({ h(Route, { path: "polygons", element: h(EditTable, { - url: `http://localhost:8000/sources/${source_id}/polygons`, + url: `${import.meta.env.VITE_MACROSTRAT_INGEST_API}/sources/${source_id}/polygons`, }), }), ]), From 29deba5c9826ae945c37606d6c8d368cb8f52cd2 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Wed, 8 Nov 2023 09:12:23 -0600 Subject: [PATCH 22/52] Fix Filter and Grouping --- src/pages/maps/@id/edit/table-menu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/maps/@id/edit/table-menu.ts b/src/pages/maps/@id/edit/table-menu.ts index 8a948c31..3fb02851 100644 --- a/src/pages/maps/@id/edit/table-menu.ts +++ b/src/pages/maps/@id/edit/table-menu.ts @@ -106,7 +106,7 @@ const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuPro "value": inputValue, className: "update-input-group", placeholder: inputPlaceholder, - onFilterChange: (e: React.ChangeEvent<HTMLInputElement>) => {setInputValue(e.target.value); debouncedInputChange(e)} + onChange: (e: React.ChangeEvent<HTMLInputElement>) => {setInputValue(e.target.value); debouncedInputChange(e)} }, []) ]), h("div.filter-header", {}, ["Group"]), @@ -118,7 +118,7 @@ const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuPro intent: groupActive ? "success" : "warning", text: groupActive ? "Active" : "Inactive", fill: true, - onClick: () => onGroupChange(filter.column_name) + onClick: () => onGroupChange(group ? "" : filter.column_name) }, []) ]), ]) From 2cfaf10a4cd1933010e43c8fd0090448751aa6ab Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Wed, 8 Nov 2023 09:54:43 -0600 Subject: [PATCH 23/52] Fix the off by one error - _pkid was still used in columns but not in cell values - Allow one click group change --- src/pages/maps/@id/edit/edit-table.ts | 11 ++++++++--- src/pages/maps/@id/edit/table-menu.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index c549426d..06c74cdd 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -1,6 +1,6 @@ import hyper from "@macrostrat/hyper"; -import { useState, useEffect, useCallback, useRef, useLayoutEffect } from "react"; +import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner, ButtonGroup } from "@blueprintjs/core"; import { @@ -76,6 +76,11 @@ export default function EditTable({ url }) { // Sparse array to hold edited data const [editedData, setEditedData] = useState(new Array()); + // Memoize non-id columns + const nonIdColumns = useMemo(() => { + return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] + }, [data]) + const onChange = (key, row, text) => { let rowSpec = {}; if (text == data[row][key] || (text == "" && data[row][key] == null)) { @@ -106,7 +111,7 @@ export default function EditTable({ url }) { const columnHeaderCellRenderer = (columnIndex: number) => { - const columnName: string = Object.keys(data[0])[columnIndex] + const columnName: string = nonIdColumns[columnIndex] const onFilterChange = (param: OperatorQueryParameter) => { @@ -198,7 +203,7 @@ export default function EditTable({ url }) { return h(Spinner) } - const columns = Object.keys(data[0]).filter(x => x != "_pkid").map((key) => { + const columns = nonIdColumns.map((key) => { return h(Column, { name: key, className: FINAL_COLUMNS.includes(key) ? "final-column" : "", diff --git a/src/pages/maps/@id/edit/table-menu.ts b/src/pages/maps/@id/edit/table-menu.ts index 3fb02851..fa848c1d 100644 --- a/src/pages/maps/@id/edit/table-menu.ts +++ b/src/pages/maps/@id/edit/table-menu.ts @@ -118,7 +118,7 @@ const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuPro intent: groupActive ? "success" : "warning", text: groupActive ? "Active" : "Inactive", fill: true, - onClick: () => onGroupChange(group ? "" : filter.column_name) + onClick: () => onGroupChange(group == filter.column_name ? undefined : filter.column_name) }, []) ]), ]) From 130e8cfc617091dad0a342dfa677636d464358b5 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 30 Nov 2023 09:49:19 -0600 Subject: [PATCH 24/52] Add back in the ability to edit the table --- package.json | 1 + packages/security/package.json | 6 + packages/security/src/index.ts | 15 ++ .../maps/@id/edit/components/cell/basic.ts | 13 ++ .../maps/@id/edit/components/cell/index.d.ts | 4 + .../@id/edit/components/cell/main.module.sass | 55 ++++++ .../progress-popover/main.module.sass | 15 ++ .../progress-popover/progress-popover.ts | 26 +++ src/pages/maps/@id/edit/edit-table.ts | 160 +++++++----------- src/pages/maps/@id/edit/table-util.ts | 88 +++++++++- src/pages/maps/@id/edit/table.d.ts | 21 +++ yarn.lock | 7 + 12 files changed, 313 insertions(+), 98 deletions(-) create mode 100644 packages/security/package.json create mode 100644 packages/security/src/index.ts create mode 100644 src/pages/maps/@id/edit/components/cell/basic.ts create mode 100644 src/pages/maps/@id/edit/components/cell/index.d.ts create mode 100644 src/pages/maps/@id/edit/components/cell/main.module.sass create mode 100644 src/pages/maps/@id/edit/components/progress-popover/main.module.sass create mode 100644 src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts diff --git a/package.json b/package.json index 8ac99d44..c5d626bb 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@loadable/component": "^5.14.1", "@macrostrat-web/data-sheet-test": "workspace:*", "@macrostrat-web/globe": "workspace:*", + "@macrostrat-web/security": "workspace:*", "@macrostrat/api-utils": "workspace:*", "@macrostrat/api-views": "workspace:*", "@macrostrat/column-components": "workspace:*", diff --git a/packages/security/package.json b/packages/security/package.json new file mode 100644 index 00000000..e72d4dd5 --- /dev/null +++ b/packages/security/package.json @@ -0,0 +1,6 @@ +{ + "name": "@macrostrat-web/security", + "private": true, + "main": "src/index.ts", + "license": "ISC" +} diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts new file mode 100644 index 00000000..1f5a740c --- /dev/null +++ b/packages/security/src/index.ts @@ -0,0 +1,15 @@ + +// Handles fetch requests that require authentication +export const secureFetch = async (url, options) => { + + console.log(url, options) + + const response = await fetch(url, options); + + if (response.status === 401) { + window.open(`${import.meta.env.VITE_MACROSTRAT_INGEST_API}/security/login`, '_blank').focus(); + throw {name: "UnauthorizedError", message: "User is not logged in"} + } + + return response +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/cell/basic.ts b/src/pages/maps/@id/edit/components/cell/basic.ts new file mode 100644 index 00000000..ae0419b7 --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/basic.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +import {Cell} from "@blueprintjs/table"; + +import hyper from "@macrostrat/hyper"; +import styles from "./main.module.sass"; + +export const h = hyper.styled(styles); + + +export const BasicCell = (...props) => { + return h(Cell, {...props}); +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/cell/index.d.ts b/src/pages/maps/@id/edit/components/cell/index.d.ts new file mode 100644 index 00000000..802d0d2b --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/index.d.ts @@ -0,0 +1,4 @@ +interface CellProps extends React.HTMLProps<HTMLTableCellElement> { + value: string; + onChange: (value: string) => void; +} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/cell/main.module.sass b/src/pages/maps/@id/edit/components/cell/main.module.sass new file mode 100644 index 00000000..91250971 --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/main.module.sass @@ -0,0 +1,55 @@ +@import "@blueprintjs/core/lib/scss/variables.scss" + +.data-sheet-container, .data-sheet-holder + flex: 1 + position: relative + min-height: 0 + +.data-sheet-container + display: flex + flex-direction: column + +.data-sheet + height: 100% + +:global(.bp4-dark) .data-sheet :global(.bp4-table-quadrant) + background-color: $dark-gray1 + +.input-cell + padding: 0 2px !important + input + width: 100% + height: 100% + padding: 0 8px + z-index: 0 + position: relative + border: none + margin: 0 + font-size: 1em + pointer-events: all + background: transparent + &:focus + outline: none + +.hidden-input + opacity: 0 + position: absolute + width: 0 + +.corner-drag-handle + position: absolute + bottom: 0 + right: 0 + width: 8px + height: 8px + background-color: $dark-gray1 + cursor: ns-resize + background-color: dodgerblue + +.data-sheet-toolbar + display: flex + flex-direction: row + margin-bottom: 4px + +.spacer + flex-grow: 1 \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/progress-popover/main.module.sass b/src/pages/maps/@id/edit/components/progress-popover/main.module.sass new file mode 100644 index 00000000..4b4f7a21 --- /dev/null +++ b/src/pages/maps/@id/edit/components/progress-popover/main.module.sass @@ -0,0 +1,15 @@ +.progress-popover + z-index: 9999 + position: absolute + bottom: 0px + background: white + padding: 10px + border-radius: 5px + box-shadow: #ececec 5px 5px 5px + width: 200px + left: 50% + transform: translate(-50%, -50%) + + .progress-popover-text + padding-top: 10px + text-align: center \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts b/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts new file mode 100644 index 00000000..062ab025 --- /dev/null +++ b/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts @@ -0,0 +1,26 @@ +import { ProgressBar, ProgressBarProps } from "@blueprintjs/core"; + +import hyper from "@macrostrat/hyper"; + +import styles from "./main.module.sass"; +const h = hyper.styled(styles); + +interface ProgressPopoverProps extends React.HTMLProps<HTMLDivElement> { + text: string; + value: number; + progressBarProps?: ProgressBarProps; +} + +export default function ProgressPopover({text, value, progressBarProps}: ProgressPopoverProps) { + return h("div", { + className: "progress-popover" + }, [ + h(ProgressBar, { + value: value, + ...progressBarProps + }), + h("div", { + className: "progress-popover-text" + }, text) + ]); +} diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 06c74cdd..1eb3b385 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -1,5 +1,7 @@ import hyper from "@macrostrat/hyper"; + + import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner, ButtonGroup } from "@blueprintjs/core"; @@ -14,9 +16,10 @@ import { } from "@blueprintjs/table"; import update from "immutability-helper"; -import { OperatorQueryParameter } from "~/pages/maps/@id/edit/table"; -import { buildURL, Filter } from "~/pages/maps/@id/edit/table-util"; +import { Filters, OperatorQueryParameter, TableUpdate, TableSelection, Selection } from "~/pages/maps/@id/edit/table"; +import { buildURL, Filter, isEmptyArray, submitChange, getTableUpdate } from "~/pages/maps/@id/edit/table-util"; import TableMenu from "~/pages/maps/@id/edit/table-menu"; +import ProgressPopover from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; import "./override.sass" import "@blueprintjs/table/lib/css/table.css"; @@ -29,21 +32,6 @@ const range = (start, stop, step = 1) => .fill(start) .map((x, y) => x + y * step); -interface Selection { - cols: number[]; - rows: number[]; -} - - -interface Filters { - [key: string]: Filter; -} - -interface TableSelection { - columns: string[]; - filters: Filters; -} - const FINAL_COLUMNS = [ "source_id", "orig_id", @@ -59,6 +47,7 @@ const FINAL_COLUMNS = [ ] export default function EditTable({ url }) { + // Table values const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(999999); @@ -66,12 +55,12 @@ export default function EditTable({ url }) { const [data, setData] = useState<any[]>([]); const [dataToggle, setDataToggle] = useState<boolean>(false); - const [inputValue, setInputValue] = useState<string>(""); const [error, setError] = useState<string | undefined>(undefined) const [filters, setFilters] = useState<Filters>({}) const [group, setGroup] = useState<string | undefined>(undefined) const [tableSelection, setTableSelection] = useState<TableSelection>({columns: [], filter: new Filter("_pkid", "in", "")}) - + const [tableUpdates, setTableUpdates] = useState<TableUpdate[]>([]) + const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) // Sparse array to hold edited data const [editedData, setEditedData] = useState(new Array()); @@ -81,13 +70,14 @@ export default function EditTable({ url }) { return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] }, [data]) - const onChange = (key, row, text) => { + const onChange = (column, row, text) => { + let rowSpec = {}; - if (text == data[row][key] || (text == "" && data[row][key] == null)) { - rowSpec = { $unset: [key] }; + if (text == data[row][column] || (text == "" && data[row][column] == null)) { + rowSpec = { $unset: [column] }; } else { const rowOp = editedData[row] == null ? "$set" : "$merge"; - rowSpec = { [rowOp]: { [key]: text } }; + rowSpec = { [rowOp]: { [column]: text } }; } const newData = update(editedData, { @@ -133,15 +123,17 @@ export default function EditTable({ url }) { } const cellRenderer = useCallback( - ({ key, row, cell }) => { + ({ columnName, rowIndex, cell }) => { return h( EditableCell2, { onConfirm: (value) => { - onChange(key, row, value); + const tableUpdate = getTableUpdate(value, columnName, rowIndex, data, filters, group) + setTableUpdates([...tableUpdates, tableUpdate]) + onChange(columnName, rowIndex, value); }, - value: editedData[row]?.[key] ?? data[row][key], - intent: intentForCell(key, row), + value: editedData[rowIndex]?.[columnName] ?? data[rowIndex][columnName], + intent: intentForCell(columnName, rowIndex), }, [] ); @@ -203,13 +195,38 @@ export default function EditTable({ url }) { return h(Spinner) } - const columns = nonIdColumns.map((key) => { + const submitTableUpdates = async () => { + + setUpdateProgress(0) + + let index = 0 + for(const update of tableUpdates){ + + try { + await submitChange(url, update) + } catch (e) { + + setUpdateProgress(undefined) + return // If there is an error, stop submitting + } + + index += 1 + setUpdateProgress(index / tableUpdates.length) + } + + setTableUpdates([]) + setEditedData([]) + setDataToggle(!dataToggle) + setUpdateProgress(undefined) + } + + const columns = nonIdColumns.map((columnName) => { return h(Column, { - name: key, - className: FINAL_COLUMNS.includes(key) ? "final-column" : "", + name: columnName, + className: FINAL_COLUMNS.includes(columnName) ? "final-column" : "", columnHeaderCellRenderer: columnHeaderCellRenderer, - cellRenderer: (row, cell) => cellRenderer({"key": key, "row": row, "cell": cell}), - "key": key + cellRenderer: (rowIndex, cell) => cellRenderer({"columnName": columnName, "rowIndex": rowIndex, "cell": cell}), + "key": columnName }) }) @@ -230,12 +247,12 @@ export default function EditTable({ url }) { let selection: TableSelection if(rows == undefined){ - selection = {filter: new Filter("_pkid", "in", ""), columns: selectedColumnKeys} + selection = {filters: new Filter("_pkid", "in", ""), columns: selectedColumnKeys} } else { const dbIds = selectedRowIndices.map((row) => data[row]['_pkid']) const filter = new Filter("_pkid", "in", "(" + dbIds.join(",") + ")") - selection = {columns: selectedColumnKeys, "filter": filter} + selection = {columns: selectedColumnKeys, "filters": [filter]} } setTableSelection(selection) @@ -251,50 +268,21 @@ export default function EditTable({ url }) { name = name.slice(0, 47) + "..." } - return h(RowHeaderCell2, { "name": name }, []); }; - const submitChange = async (value: string) => { - for (const column of tableSelection.columns) { - let updateURL = new URL(url); - - for (const filter of Object.values(tableSelection.filters)) { - updateURL.searchParams.append(...filter.to_array()); - } - - let patch = { [column]: value }; - console.log(patch, JSON.stringify(patch)); - - let response = await fetch(updateURL, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(patch), - }); - - if (response.status != 204) { - console.error("Failed to update", response); - } - } - setDataToggle(!dataToggle); - }; - return h(HotkeysProvider, {}, [ h("div.table-container", {}, [ - h.if(error)("div.warning", {}, [error]), + h.if(error != undefined)("div.warning", {}, [error]), h("div.input-form", {}, [ - h(InputGroup, { - value: inputValue, - className: "update-input-group", - onChange: (e) => setInputValue(e.target.value), - }), h(ButtonGroup, [ h( Button, { - onClick: () => setEditedData(new Array()), + onClick: () => { + setTableUpdates([]); + setEditedData([]); + }, disabled: isEmptyArray(editedData), }, ["Clear changes"] @@ -303,7 +291,7 @@ export default function EditTable({ url }) { Button, { type: "submit", - onClick: () => submitChange(inputValue), + onClick: submitTableUpdates, disabled: isEmptyArray(editedData), intent: "success", }, @@ -323,35 +311,17 @@ export default function EditTable({ url }) { cellRendererDependencies: [editedData], }, columns + ), + h.if(updateProgress != undefined)( + ProgressPopover, + { + text: "Submitting Changes", + value: updateProgress, + progressBarProps: { intent: "success" }, + } ) ]), ]); } -function isEmptyArray(arr) { - return arr.length == 0 || arr.every((x) => x == null); -} - -class TableDataManager { - /** Low-level manager for windowed loading of table data. This will eventually be how - * we work with the data, hopefully. */ - baseURL: string; - totalCount: number; - chunkSize: number = 100; - - init(baseURL: string) { - this.baseURL = baseURL; - } - - async getData(page: number) { - let dataURL = new URL(this.baseURL); - dataURL.searchParams.append("page", page.toString()); - dataURL.searchParams.append("page_size", this.chunkSize.toString()); - - let response = await fetch(dataURL); - let data = await response.json(); - - this.totalCount = Number.parseInt(response.headers.get("X-Total-Count")); - } -} diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts index 8a20aabd..95b3e71a 100644 --- a/src/pages/maps/@id/edit/table-util.ts +++ b/src/pages/maps/@id/edit/table-util.ts @@ -1,4 +1,5 @@ -import {ColumnOperators, Filters} from "./table"; +import { ColumnOperators, Filters, TableSelection, TableUpdate } from "./table"; +import {secureFetch} from "@macrostrat-web/security"; export class Filter { @@ -34,8 +35,6 @@ export class Filter { } - - export function buildURL(baseURL: string, filters: Filter[], group: string | undefined){ let updateURL = new URL(baseURL) @@ -52,4 +51,87 @@ export function buildURL(baseURL: string, filters: Filter[], group: string | und } return updateURL +} + +/** + * Builds a table update from the current table state + */ +export const getTableUpdate = ( + value: string, + columnName: string, + rowIndex: number, + data: any[], + filters: Filters, + group: string | undefined +): TableUpdate => { + + filters = {...filters} + if( group != undefined){ + filters[group] = new Filter(group, "eq", data[rowIndex][group]) + } else { + filters["_pkid"] = new Filter("_pkid", "eq", data[rowIndex]["_pkid"]) + } + + const selection: TableSelection = { + columns: [columnName], + filters: filters + } + + return { + selection, + value: value + } +} + +export const submitChanges = async (url: string, updates: TableUpdate[]) => { + for(const update of updates){ + console.log("Update: ", update) + + // await submitChange(url, update) + } +} + +export const submitChange = async (url: string, {selection, value}: TableUpdate) => { + + // Query per column + for (const column of selection.columns) { + + let updateURL = new URL(url); + + // Add the filters to the query parameters + for (const filter of Object.values(selection.filters)) { + + // Check that the filter is valid + if(!filter.is_valid()){ + continue + } + + console.log("Filter: ", filter) + + const [searchTerm, searchValue] = filter.to_array() + updateURL.searchParams.append(searchTerm, searchValue); + } + + // Create the request body + let patch = { [column]: value }; + + // Send the request + let response = await secureFetch(updateURL, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(patch), + }); + + if (response.status != 204) { + + // Stop execution if the request failed + throw Error("Failed to update"); + } + } +}; + +export function isEmptyArray(arr) { + return arr.length == 0 || arr.every((x) => x == null); } \ No newline at end of file diff --git a/src/pages/maps/@id/edit/table.d.ts b/src/pages/maps/@id/edit/table.d.ts index a57a7077..76f3afa5 100644 --- a/src/pages/maps/@id/edit/table.d.ts +++ b/src/pages/maps/@id/edit/table.d.ts @@ -19,3 +19,24 @@ interface Filters { [key: string]: Filter; } +interface Selection { + cols: number[]; + rows: number[]; +} + + +interface Filters { + [key: string]: Filter; +} + +// An object that represents a selection of rows and columns +interface TableSelection { + columns: string[]; + filters: Filters; +} + +// An object that represents a value update made on top of a specific TableSelection +interface TableUpdate { + selection: TableSelection; + value: string; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0f6745fe..8d8434d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3314,6 +3314,12 @@ __metadata: languageName: unknown linkType: soft +"@macrostrat-web/security@workspace:*, @macrostrat-web/security@workspace:packages/security": + version: 0.0.0-use.local + resolution: "@macrostrat-web/security@workspace:packages/security" + languageName: unknown + linkType: soft + "@macrostrat/api-types@workspace:deps/web-components/packages/api-types": version: 0.0.0-use.local resolution: "@macrostrat/api-types@workspace:deps/web-components/packages/api-types" @@ -3841,6 +3847,7 @@ __metadata: "@loadable/component": ^5.14.1 "@macrostrat-web/data-sheet-test": "workspace:*" "@macrostrat-web/globe": "workspace:*" + "@macrostrat-web/security": "workspace:*" "@macrostrat/api-utils": "workspace:*" "@macrostrat/api-views": "workspace:*" "@macrostrat/column-components": "workspace:*" From 6255704006ae7f7136c59cbefe7cc86c6d4198c4 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 30 Nov 2023 10:12:06 -0600 Subject: [PATCH 25/52] Add data to cell renderer dependency --- packages/security/src/index.ts | 5 ++++- src/pages/maps/@id/edit/edit-table.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index 1f5a740c..3d9365b8 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -2,7 +2,10 @@ // Handles fetch requests that require authentication export const secureFetch = async (url, options) => { - console.log(url, options) + options = { + credentials: "include", + ...options, + } const response = await fetch(url, options); diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 1eb3b385..0d10a3fa 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -158,7 +158,6 @@ export default function EditTable({ url }) { if(newData.length == 0){ setError("Warning: No results matched query") } else { - console.log("Data fetched successfully") setError(undefined) setData(newData) @@ -308,7 +307,7 @@ export default function EditTable({ url }) { getSelectionValues(selections), numRows: data.length, // Dumb hacks to try to get the table to rerender on changes - cellRendererDependencies: [editedData], + cellRendererDependencies: [editedData, data], }, columns ), From b5fcab818a2a9ce27d1e183a1f771cca748092fa Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 30 Nov 2023 12:10:29 -0600 Subject: [PATCH 26/52] Add ability to lock down pages --- package.json | 4 +++- server/index.js | 25 ++++++++++++++++++++- src/pages/dev/security/index.page.route.ts | 20 +++++++++++++++++ src/pages/dev/security/index.page.ts | 9 ++++++++ yarn.lock | 26 ++++++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/pages/dev/security/index.page.route.ts create mode 100644 src/pages/dev/security/index.page.ts diff --git a/package.json b/package.json index c5d626bb..b7c3e935 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "bootstrap": "yarn", "dev": "yarn run server:dev", "build": "vite build", - "server": "node ./server/index.js", + "server": "node --env-file=.env ./server/index.js", "server:dev": "yarn run server", "server:prod": "cross-env NODE_ENV=production npm run server" }, @@ -90,6 +90,7 @@ "chroma-js": "^2.4.2", "classnames": "^2.2.6", "compression": "^1.7.4", + "cookie-parser": "^1.4.6", "cross-env": "^7.0.3", "d3-array": "^3.1.1", "d3-axis": "^3.0.0", @@ -102,6 +103,7 @@ "express": "^4.18.2", "history": "^5.3.0", "immutability-helper": "^3.1.1", + "jose": "^5.1.2", "mapbox-gl": "^2.15.0", "new-github-issue-url": "^1.0.0", "pbf": "^3.2.1", diff --git a/server/index.js b/server/index.js index 0a9e24ce..6fb8c4ae 100644 --- a/server/index.js +++ b/server/index.js @@ -8,6 +8,11 @@ import express from 'express' import compression from 'compression' import { renderPage } from 'vite-plugin-ssr/server' import { root } from './root.js' + +// Auth imports +import cookieParser from 'cookie-parser' +import * as jose from 'jose' + const isProduction = process.env.NODE_ENV === 'production' startServer() @@ -16,6 +21,7 @@ async function startServer() { const app = express() app.use(compression()) + app.use(cookieParser()) // Vite integration if (isProduction) { @@ -45,8 +51,25 @@ async function startServer() { // catch-all middleware superseding any middleware placed after it). app.get('*', async (req, res, next) => { + // Pull out the authorization cookie and decrypt it + let user = undefined + + try { + const authHeader = req.cookies?.Authorization + const secret = new TextEncoder().encode( + process.env.SECRET_KEY + ); + const jwt = authHeader.substring(7, authHeader.length) + user = (await jose.jwtVerify(jwt, secret)).payload + + + } catch (e) { + // I don't care if it fails, it just means the user isn't logged in + } + const pageContextInit = { - urlOriginal: req.originalUrl + urlOriginal: req.originalUrl, + user: user } const pageContext = await renderPage(pageContextInit) diff --git a/src/pages/dev/security/index.page.route.ts b/src/pages/dev/security/index.page.route.ts new file mode 100644 index 00000000..7ffb09e9 --- /dev/null +++ b/src/pages/dev/security/index.page.route.ts @@ -0,0 +1,20 @@ +import { render, redirect } from 'vite-plugin-ssr/abort' + +export const guard = (pageContext) => { + const { user } = pageContext + + console.log("User: ", user) + + if (user === undefined) { + // Render the login page while preserving the URL. (This is novel technique + // which we explain down below.) + throw redirect(`${import.meta.env.VITE_MACROSTRAT_INGEST_API}/security/login?return_url=${pageContext.url}`) + /* The more traditional way, redirect the user: + throw redirect('/login') + */ + } + if (!user.groups.includes("admin")) { + // Render the error page and show message to the user + throw render(403, 'Only admins are allowed to access this page.') + } +} \ No newline at end of file diff --git a/src/pages/dev/security/index.page.ts b/src/pages/dev/security/index.page.ts new file mode 100644 index 00000000..7dd66255 --- /dev/null +++ b/src/pages/dev/security/index.page.ts @@ -0,0 +1,9 @@ +import {default as h} from "@macrostrat/hyper"; + +export function Page() { + + return h("div", [ + h("h1", "Secure Page") + ]); +} + diff --git a/yarn.lock b/yarn.lock index 8d8434d4..97dc8af7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3888,6 +3888,7 @@ __metadata: chroma-js: ^2.4.2 classnames: ^2.2.6 compression: ^1.7.4 + cookie-parser: ^1.4.6 cross-env: ^7.0.3 d3-array: ^3.1.1 d3-axis: ^3.0.0 @@ -3900,6 +3901,7 @@ __metadata: express: ^4.18.2 history: ^5.3.0 immutability-helper: ^3.1.1 + jose: ^5.1.2 mapbox-gl: ^2.15.0 new-github-issue-url: ^1.0.0 pbf: ^3.2.1 @@ -10918,6 +10920,16 @@ __metadata: languageName: node linkType: hard +"cookie-parser@npm:^1.4.6": + version: 1.4.6 + resolution: "cookie-parser@npm:1.4.6" + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + checksum: 1e5a63aa82e8eb4e02d2977c6902983dee87b02e87ec5ec43ac3cb1e72da354003716570cd5190c0ad9e8a454c9d3237f4ad6e2f16d0902205a96a1c72b77ba5 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -10925,6 +10937,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.4.1": + version: 0.4.1 + resolution: "cookie@npm:0.4.1" + checksum: bd7c47f5d94ab70ccdfe8210cde7d725880d2fcda06d8e375afbdd82de0c8d3b73541996e9ce57d35f67f672c4ee6d60208adec06b3c5fc94cebb85196084cf8 + languageName: node + linkType: hard + "cookie@npm:0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -18335,6 +18354,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^5.1.2": + version: 5.1.2 + resolution: "jose@npm:5.1.2" + checksum: 035aff9c3413c2dbcb4fe240f14249e59c91225575063c1e27a0944e6b78a24b20b61f3b687ccff2012ff430b335c002f4af7e3599e284475b451752866041a0 + languageName: node + linkType: hard + "jpeg-js@npm:^0.4.1": version: 0.4.4 resolution: "jpeg-js@npm:0.4.4" From c493f5a31aa87432b2e075e9848cf5224493b869 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 30 Nov 2023 15:49:59 -0600 Subject: [PATCH 27/52] Update to include another non auth'd https code --- packages/security/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index 3d9365b8..ad9978cc 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -9,7 +9,7 @@ export const secureFetch = async (url, options) => { const response = await fetch(url, options); - if (response.status === 401) { + if (response.status === 401 || response.status === 403) { window.open(`${import.meta.env.VITE_MACROSTRAT_INGEST_API}/security/login`, '_blank').focus(); throw {name: "UnauthorizedError", message: "User is not logged in"} } From 2152447db3687c606998c372f5d78abd288e9cb8 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 30 Nov 2023 21:01:28 -0600 Subject: [PATCH 28/52] Remove progress bar only on data reload --- src/pages/maps/@id/edit/edit-table.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 0d10a3fa..810d44bb 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -164,6 +164,9 @@ export default function EditTable({ url }) { setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))); } + // Remove the progress bar on data reload + setUpdateProgress(undefined) + return newData } @@ -216,7 +219,6 @@ export default function EditTable({ url }) { setTableUpdates([]) setEditedData([]) setDataToggle(!dataToggle) - setUpdateProgress(undefined) } const columns = nonIdColumns.map((columnName) => { From d9eb0d5bafa53528db92e74f3fd1048692313040 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 5 Dec 2023 09:50:35 -0600 Subject: [PATCH 29/52] Update the dev build system --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index c47a3fc8..d911a128 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,7 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb - type=ref,enable=true,prefix=pr-,suffix=-{{date 'YYYYMMDDHHmmss'}},event=pr + type=ref,enable=true,prefix=main-,suffix=-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} - name: Set up Docker Buildx From 71808984c1d56b445a23ed0795a1f07722d4fcf6 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 5 Dec 2023 09:56:21 -0600 Subject: [PATCH 30/52] Update the dev build system --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index d911a128..b913be68 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,7 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb - type=ref,enable=true,prefix=main-,suffix=-{{date 'YYYYMMDDHHmmss'}} + type=ref,enable=true,prefix=main-,suffix=-{{date 'YYYYMMDDHHmmss'}},event=tag type=raw,value=sha-{{sha}} - name: Set up Docker Buildx From c8d91693731989e6c61b0c2dc03813f475d5e595 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 5 Dec 2023 10:03:54 -0600 Subject: [PATCH 31/52] Update the dev build system --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index b913be68..c8b8fb71 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,7 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb - type=ref,enable=true,prefix=main-,suffix=-{{date 'YYYYMMDDHHmmss'}},event=tag + type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} - name: Set up Docker Buildx From 2814f9acad151cfdc00178f263c9545ad2895cff Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 5 Dec 2023 14:26:36 -0600 Subject: [PATCH 32/52] Add tag-timestamp image --- .github/workflows/build-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index c8b8fb71..5301dd3f 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,6 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb + type=raw,value={{tag}}-{{date 'YYYYMMDDHHmmss'}} type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} - From 89162d8b4d08a949fd81740f15d8fe25404ffb23 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 11 Dec 2023 16:40:37 -0600 Subject: [PATCH 33/52] Refactor the table and add Interval elements --- .../maps/@id/edit/components/cell/basic.ts | 13 - .../@id/edit/components/cell/editable-cell.ts | 20 ++ .../edit/components/cell/editor-popover.ts | 19 ++ .../components/cell/interval-selection.ts | 123 ++++++++ .../@id/edit/components/table-interface.ts | 260 ++++++++++++++++ src/pages/maps/@id/edit/edit-page.ts | 2 + src/pages/maps/@id/edit/edit-table.ts | 281 ++++++++---------- src/pages/maps/@id/edit/table-menu.ts | 5 +- src/pages/maps/@id/edit/table-util.ts | 137 ++++++--- src/pages/maps/@id/edit/table.d.ts | 17 +- 10 files changed, 660 insertions(+), 217 deletions(-) delete mode 100644 src/pages/maps/@id/edit/components/cell/basic.ts create mode 100644 src/pages/maps/@id/edit/components/cell/editable-cell.ts create mode 100644 src/pages/maps/@id/edit/components/cell/editor-popover.ts create mode 100644 src/pages/maps/@id/edit/components/cell/interval-selection.ts create mode 100644 src/pages/maps/@id/edit/components/table-interface.ts diff --git a/src/pages/maps/@id/edit/components/cell/basic.ts b/src/pages/maps/@id/edit/components/cell/basic.ts deleted file mode 100644 index ae0419b7..00000000 --- a/src/pages/maps/@id/edit/components/cell/basic.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import {Cell} from "@blueprintjs/table"; - -import hyper from "@macrostrat/hyper"; -import styles from "./main.module.sass"; - -export const h = hyper.styled(styles); - - -export const BasicCell = (...props) => { - return h(Cell, {...props}); -} \ No newline at end of file diff --git a/src/pages/maps/@id/edit/components/cell/editable-cell.ts b/src/pages/maps/@id/edit/components/cell/editable-cell.ts new file mode 100644 index 00000000..71fca0e8 --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/editable-cell.ts @@ -0,0 +1,20 @@ +import React from 'react'; + +import {EditableCell2, EditableCell2Props} from "@blueprintjs/table"; + +import hyper from "@macrostrat/hyper"; +import styles from "./main.module.sass"; +import { getTableUpdate } from "~/pages/maps/@id/edit/table-util"; + +export const h = hyper.styled(styles); + + +interface EditableCell extends EditableCell2Props { + columnName: string, + rowIndex: number +} + +export const EditableCell = ({...props}: EditableCell2Props) => { + + return h(EditableCell2, {...props}); +} diff --git a/src/pages/maps/@id/edit/components/cell/editor-popover.ts b/src/pages/maps/@id/edit/components/cell/editor-popover.ts new file mode 100644 index 00000000..f91ef36b --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/editor-popover.ts @@ -0,0 +1,19 @@ +import {Button, MenuItem} from "@blueprintjs/core"; +import {Select2, ItemRenderer} from "@blueprintjs/select"; +import {EditableCell2Props} from "@blueprintjs/table"; +import React, {useEffect, useMemo} from "react"; + +// @ts-ignore +import hyper from "@macrostrat/hyper"; + +import "@blueprintjs/core/lib/css/blueprint.css" +import "@blueprintjs/select/lib/css/blueprint-select.css"; +import styles from "../../edit-table.module.sass"; + + +const h = hyper.styled(styles); + + +export const EditorPopover = () => { + +} diff --git a/src/pages/maps/@id/edit/components/cell/interval-selection.ts b/src/pages/maps/@id/edit/components/cell/interval-selection.ts new file mode 100644 index 00000000..ee2237b9 --- /dev/null +++ b/src/pages/maps/@id/edit/components/cell/interval-selection.ts @@ -0,0 +1,123 @@ +import {Button, MenuItem} from "@blueprintjs/core"; +import {Select2, ItemRenderer} from "@blueprintjs/select"; +import {EditableCell2Props, EditableCell2, Cell} from "@blueprintjs/table"; +import React, {useEffect, useMemo} from "react"; + +// @ts-ignore +import hyper from "@macrostrat/hyper"; + +import "@blueprintjs/core/lib/css/blueprint.css" +import "@blueprintjs/select/lib/css/blueprint-select.css"; +import styles from "../../edit-table.module.sass"; + + +const h = hyper.styled(styles); + +interface Timescale { + timescale_id: number + name: string +} + +export interface Interval { + int_id: number + name: string + abbrev: string + t_age: number + b_age: number + int_type: string + timescales: Timescale[] + color: string +} + +const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClick, handleFocus, modifiers }) => { + + if (interval == null) { + return h(MenuItem, { + shouldDismissPopover: false, + active: modifiers.active, + disabled: modifiers.disabled, + key: "", + label: "", + onClick: handleClick, + onFocus: handleFocus, + text: "", + roleStructure:"listoption" + }, []) + } + + return h(MenuItem, { + style: {backgroundColor: interval.color}, + shouldDismissPopover: false, + active: modifiers.active, + disabled: modifiers.disabled, + key: interval.int_id, + label: interval.name, + onClick: handleClick, + onFocus: handleFocus, + text: interval.name, + roleStructure:"listoption" + }, []) +} + + +const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2Props) => { + + const [menuOpen, setMenuOpen] = React.useState<boolean>(false); + const [intervalValues, setIntervalValues] = React.useState<Interval[]>([]); + + async function getIntervals() { + let response = await fetch(`https://macrostrat.org/api/defs/intervals?tilescale_id=11`) + + if (response.ok) { + let response_data = await response.json(); + setIntervalValues(response_data.success.data); + } + } + + const interval = useMemo(() => { + if(intervalValues.length == 0){ + return null + } else { + return intervalValues.filter((interval) => interval.int_id == parseInt(value))[0] + } + }, [value, intervalValues]) + + useEffect(() => { + getIntervals() + }, []) + + console.log(interval) + + return h(Cell, { + style: {padding: 0}, + ...props + }, [ + h(Select2<Interval>, { + fill: true, + items: intervalValues, + className: "update-input-group", + filterable: false, + popoverProps: {isOpen: menuOpen}, + itemRenderer: IntervalOption, + onItemSelect: (interval: Interval) => { + setMenuOpen(false); + onConfirm(interval.int_id.toString()) + }, + noResults: h(MenuItem, {disabled: true, text: "No results.", roleStructure: "listoption"}, []), + }, [ + h(Button, { + style: {backgroundColor: interval?.color ?? "white", fontSize: "12px", minHeight: "0px", padding: "1.7px 10px", boxShadow: "none"}, + fill: true, + onClick: () => setMenuOpen(!menuOpen), + alignText: "left", + text: interval?.name ?? "Select an Interval", + rightIcon: "double-caret-vertical", + className: "update-input-group", + placeholder: "Select A Filter" + }, []) + ]), + ]) +} + + +export default IntervalSelection; diff --git a/src/pages/maps/@id/edit/components/table-interface.ts b/src/pages/maps/@id/edit/components/table-interface.ts new file mode 100644 index 00000000..643a4eaf --- /dev/null +++ b/src/pages/maps/@id/edit/components/table-interface.ts @@ -0,0 +1,260 @@ +import hyper from "@macrostrat/hyper"; + + + +import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo, FunctionComponent } from "react"; +import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; +import { Spinner, ButtonGroup } from "@blueprintjs/core"; +import { + Column, + Table2, + EditableCell2, + RowHeaderCell2, + ColumnHeaderCell2, + SelectionModes, + RegionCardinality +} from "@blueprintjs/table"; +import update from "immutability-helper"; + +import { Filters, OperatorQueryParameter, TableUpdate, TableSelection, Selection, DataParameters } from "~/pages/maps/@id/edit/table"; +import { + buildURL, + Filter, + isEmptyArray, + submitChange, + getTableUpdate, + range, + applyTableUpdates +} from "~/pages/maps/@id/edit/table-util"; +import TableMenu from "~/pages/maps/@id/edit/table-menu"; +import IntervalSelection from "../components/cell/interval-selection"; +import ProgressPopover from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; + +import "./override.sass" +import "@blueprintjs/table/lib/css/table.css"; +import styles from "./edit-table.module.sass"; +import { EditableCell } from "~/pages/maps/@id/edit/components/cell/editable-cell"; +import EditTable from "~/pages/maps/@id/edit/edit-table"; + +const h = hyper.styled(styles); + +const FINAL_COLUMNS = [ + "source_id", + "orig_id", + "descrip", + "ready", + "name", + "strat_name", + "age", + "lith", + "comments", + "t_interval", + "b_interval" +] + +interface EditTableProps { + url: string; + data: { + [key: string]: any + }; +} + +interface TableState { + error: string | undefined; + filters: Filters; + group: string | undefined; + tableSelection: TableSelection; +} + +export default function TableInterface({ url }: EditTableProps) { + + // Data State + const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "999999"}}) + const [data, setData] = useState<any[]>([]) + const [dataToggle, setDataToggle] = useState<boolean>(false); + + // Error State + const [error, setError] = useState<string | undefined>(undefined) + + // Table Update State + const [tableUpdates, setTableUpdates] = useState<TableUpdate[]>([]) + const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) + + // Memoize non-id columns + const nonIdColumnNames = useMemo(() => { + return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] + }, [data]) + + + let getData = async () => { + + const dataURL = buildURL(url, dataParameters) + + const response = await fetch(dataURL) + const data = await response.json() + + if(data.length == 0){ + setError("Warning: No results matched query") + } else { + + setError(undefined) + setData(data) + } + + // Remove the progress bar on data reload + setUpdateProgress(undefined) + + return data + } + + // On mount get data + useEffect(() => { + getData() + }, [dataParameters]) + + if(data.length == 0 && error == undefined){ + return h(Spinner) + } + + const submitTableUpdates = async () => { + + setUpdateProgress(0) + + let index = 0 + for(const tableUpdate of tableUpdates){ + + try { + await tableUpdate.execute() + } catch (e) { + + setUpdateProgress(undefined) + return // If there is an error, stop submitting + } + + index += 1 + setUpdateProgress(index / tableUpdates.length) + } + + setTableUpdates([]) + setDataToggle(!dataToggle) + } + + const columnHeaderCellRenderer = (columnIndex: number) => { + + const columnName: string = nonIdColumnNames[columnIndex] + + const onFilterChange = (param: OperatorQueryParameter) => { + const columnFilter = new Filter(columnName, param.operator, param.value) + setDataParameters({...dataParameters, filter: {...dataParameters.filter, [columnName]: columnFilter}}) + } + + const filter = dataParameters.filter[columnName] + + const setGroup = (group: string | undefined) => { + setDataParameters({...dataParameters, group: group}) + } + + return h(ColumnHeaderCell2, { + menuRenderer: () => h(TableMenu, {"columnName": columnName, "onFilterChange": onFilterChange, "filter": filter, "onGroupChange": setGroup, "group": dataParameters?.group}), + name: columnName, + style: { + backgroundColor: filter.is_valid() || dataParameters?.group == columnName ? "rgba(27,187,255,0.12)" : "#ffffff00" + } + }, []) + } + + + const defaultColumnConfig = Object.entries(nonIdColumnNames).map(([columnName, value], index) => { + return h(Column, { + name: columnName, + className: FINAL_COLUMNS.includes(columnName) ? "final-column" : "", + columnHeaderCellRenderer: columnHeaderCellRenderer, + cellRenderer: (rowIndex) => h(EditableCell, { + onConfirm: (value) => { + const tableUpdate = getTableUpdate(url, value, columnName, rowIndex, data, dataParameters) + setTableUpdates([...tableUpdates, tableUpdate]) + }, + value: applyTableUpdates(data[rowIndex], columnName, tableUpdates) + }), + "key": columnName + }) + }) + + const columnConfig = { + ...defaultColumnConfig, + "t_interval": h(Column, { + ...defaultColumnConfig["t_interval"], + cellRenderer: (rowIndex) => h(IntervalSelection, { + onConfirm: (value) => { + const tableUpdate = getTableUpdate(url, value, "t_interval", rowIndex, data, dataParameters) + setTableUpdates([...tableUpdates, tableUpdate]) + }, + value: data[rowIndex]["t_interval"] + }) + }) + } + + const rowHeaderCellRenderer = (rowIndex: number) => { + const headerKey = dataParameters?.group ? dataParameters?.group : "_pkid" + let name = data[rowIndex][headerKey] + + if (name == null) { + name = "NULL"; + } else if(name.length > 47){ + name = name.slice(0, 47) + "..." + } + + return h(RowHeaderCell2, { "name": name }, []); + }; + + return h(HotkeysProvider, {}, [ + h("div.table-container", {}, [ + h.if(error != undefined)("div.warning", {}, [error]), + h("div.input-form", {}, [ + h(ButtonGroup, [ + h( + Button, + { + onClick: () => { + setTableUpdates([]); + }, + disabled: isEmptyArray(tableUpdates), + }, + ["Clear changes"] + ), + h( + Button, + { + type: "submit", + onClick: submitTableUpdates, + disabled: isEmptyArray(tableUpdates), + intent: "success", + }, + ["Submit"] + ), + ]), + ]), + h( + Table2, + { + selectionModes: dataParameters?.group ? RegionCardinality.CELLS : SelectionModes.COLUMNS_AND_CELLS, + rowHeaderCellRenderer: rowHeaderCellRenderer, + onSelection: (selections: Selection[]) => + getSelectionValues(selections), + numRows: data.length, + // Dumb hacks to try to get the table to rerender on changes + cellRendererDependencies: [data], + }, + columnConfig + ), + h.if(updateProgress != undefined)( + ProgressPopover, + { + text: "Submitting Changes", + value: updateProgress, + progressBarProps: { intent: "success" }, + } + ) + ]), + ]); +} diff --git a/src/pages/maps/@id/edit/edit-page.ts b/src/pages/maps/@id/edit/edit-page.ts index 5f839ebd..a7bfb41f 100644 --- a/src/pages/maps/@id/edit/edit-page.ts +++ b/src/pages/maps/@id/edit/edit-page.ts @@ -90,3 +90,5 @@ function ShowMapButton({ showMap, setShowMap }) { onClick: () => setShowMap(!showMap), }); } + + diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 810d44bb..cfbf074b 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -2,7 +2,7 @@ import hyper from "@macrostrat/hyper"; -import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo } from "react"; +import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo, FunctionComponent } from "react"; import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; import { Spinner, ButtonGroup } from "@blueprintjs/core"; import { @@ -16,22 +16,28 @@ import { } from "@blueprintjs/table"; import update from "immutability-helper"; -import { Filters, OperatorQueryParameter, TableUpdate, TableSelection, Selection } from "~/pages/maps/@id/edit/table"; -import { buildURL, Filter, isEmptyArray, submitChange, getTableUpdate } from "~/pages/maps/@id/edit/table-util"; +import { Filters, OperatorQueryParameter, TableUpdate, TableSelection, Selection, DataParameters } from "~/pages/maps/@id/edit/table"; +import { + buildURL, + Filter, + isEmptyArray, + submitChange, + getTableUpdate, + range, + applyTableUpdates +} from "~/pages/maps/@id/edit/table-util"; import TableMenu from "~/pages/maps/@id/edit/table-menu"; +import IntervalSelection from "./components/cell/interval-selection"; import ProgressPopover from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; import "./override.sass" import "@blueprintjs/table/lib/css/table.css"; import styles from "./edit-table.module.sass"; +import { EditableCell } from "~/pages/maps/@id/edit/components/cell/editable-cell"; +import EditTable from "~/pages/maps/@id/edit/edit-table"; const h = hyper.styled(styles); -const range = (start, stop, step = 1) => - Array(Math.ceil((stop - start) / step)) - .fill(start) - .map((x, y) => x + y * step); - const FINAL_COLUMNS = [ "source_id", "orig_id", @@ -46,152 +52,67 @@ const FINAL_COLUMNS = [ "b_interval" ] -export default function EditTable({ url }) { +interface EditTableProps { + url: string; + data: { + [key: string]: any + }; +} - // Table values - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(999999); - const [totalCount, setTotalCount] = useState(0); +interface TableState { + error: string | undefined; + filters: Filters; + group: string | undefined; + tableSelection: TableSelection; +} - const [data, setData] = useState<any[]>([]); +export default function TableInterface({ url }: EditTableProps) { + + // Data State + const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "999999"}, filter: {}}) + const [data, setData] = useState<any[]>([]) const [dataToggle, setDataToggle] = useState<boolean>(false); + + // Error State const [error, setError] = useState<string | undefined>(undefined) - const [filters, setFilters] = useState<Filters>({}) - const [group, setGroup] = useState<string | undefined>(undefined) - const [tableSelection, setTableSelection] = useState<TableSelection>({columns: [], filter: new Filter("_pkid", "in", "")}) + + // Table Update State const [tableUpdates, setTableUpdates] = useState<TableUpdate[]>([]) const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) - // Sparse array to hold edited data - const [editedData, setEditedData] = useState(new Array()); - // Memoize non-id columns - const nonIdColumns = useMemo(() => { + const nonIdColumnNames = useMemo(() => { return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] }, [data]) - const onChange = (column, row, text) => { - - let rowSpec = {}; - if (text == data[row][column] || (text == "" && data[row][column] == null)) { - rowSpec = { $unset: [column] }; - } else { - const rowOp = editedData[row] == null ? "$set" : "$merge"; - rowSpec = { [rowOp]: { [column]: text } }; - } - - const newData = update(editedData, { - [row]: rowSpec, - }); - setEditedData(newData); - }; - - const isValid = (key, row, text) => { - // Placeholder for future validation - return true; - }; - - const intentForCell = (key, row) => { - const _val = editedData[row]?.[key]; - if (_val != null) { - return isValid(key, row, _val) ? "success" : "danger"; - } - return "none"; - }; - - const columnHeaderCellRenderer = (columnIndex: number) => { - - const columnName: string = nonIdColumns[columnIndex] - - const onFilterChange = (param: OperatorQueryParameter) => { - - const columnFilter = new Filter(columnName, param.operator, param.value) - - setFilters({...filters, [columnName]: columnFilter}) - } - - let filter = filters[columnName] - + console.log(tableUpdates) - return h(ColumnHeaderCell2, { - menuRenderer: () => h(TableMenu, {"onFilterChange": onFilterChange, filter, "onGroupChange": setGroup, group}), - name: columnName, - style: { - backgroundColor: filter.is_valid() || group == columnName ? "rgba(27,187,255,0.12)" : "#ffffff00" - } - }, []) - } - - const cellRenderer = useCallback( - ({ columnName, rowIndex, cell }) => { - return h( - EditableCell2, - { - onConfirm: (value) => { - const tableUpdate = getTableUpdate(value, columnName, rowIndex, data, filters, group) - setTableUpdates([...tableUpdates, tableUpdate]) - onChange(columnName, rowIndex, value); - }, - value: editedData[rowIndex]?.[columnName] ?? data[rowIndex][columnName], - intent: intentForCell(columnName, rowIndex), - }, - [] - ); - }, - [data, editedData] - ); let getData = async () => { - const dataURL = buildURL(url, Object.values(filters), group) - - if(group == undefined){ - dataURL.searchParams.append("_pkid", "order_by" ) - } - - dataURL.searchParams.append("page", page.toString()); - dataURL.searchParams.append("page_size", pageSize.toString()); + const dataURL = buildURL(url, dataParameters) const response = await fetch(dataURL) - const newData = await response.json() + const data = await response.json() - if(newData.length == 0){ + if(data.length == 0){ setError("Warning: No results matched query") } else { setError(undefined) - setData(newData) - setTotalCount(Number.parseInt(response.headers.get("X-Total-Count"))); + setData(data) } // Remove the progress bar on data reload setUpdateProgress(undefined) - return newData + return data } - // On mount get data and set filters + // On mount get data useEffect(() => { - (async function () { - let data = await getData() - let newFilters: Filters = Object.keys(data[0]).reduce((original, key) => { - let originalFilters: Filters = {...original} - originalFilters[key] = new Filter(key, undefined, "") - return originalFilters - }, {}) - setFilters({...filters, ...newFilters}) - }()); - }, []) - - // Update data on filter change and on data toggle / Not on mount - const dataFetched = useRef(false) - useLayoutEffect(() => { - if(!dataFetched.current){ - dataFetched.current = true - return - } getData() - }, [dataToggle, filters, group]) + }, [dataParameters]) if(data.length == 0 && error == undefined){ return h(Spinner) @@ -202,10 +123,10 @@ export default function EditTable({ url }) { setUpdateProgress(0) let index = 0 - for(const update of tableUpdates){ + for(const tableUpdate of tableUpdates){ try { - await submitChange(url, update) + await tableUpdate.execute() } catch (e) { setUpdateProgress(undefined) @@ -217,50 +138,89 @@ export default function EditTable({ url }) { } setTableUpdates([]) - setEditedData([]) setDataToggle(!dataToggle) } - const columns = nonIdColumns.map((columnName) => { - return h(Column, { - name: columnName, - className: FINAL_COLUMNS.includes(columnName) ? "final-column" : "", - columnHeaderCellRenderer: columnHeaderCellRenderer, - cellRenderer: (rowIndex, cell) => cellRenderer({"columnName": columnName, "rowIndex": rowIndex, "cell": cell}), - "key": columnName - }) - }) + const columnHeaderCellRenderer = (columnIndex: number) => { - const getSelectionValues = (selections: Selection[]) => { + const columnName: string = nonIdColumnNames[columnIndex] - if(selections.length == 0){ - setTableSelection({columns: [], filter: new Filter("_pkid", "in", "")}) - return + const onFilterChange = (param: OperatorQueryParameter) => { + const columnFilter = new Filter(columnName, param.operator, param.value) + setDataParameters({...dataParameters, filter: {...dataParameters.filter, [columnName]: columnFilter}}) } - const rows = selections[0]?.rows - const cols = selections[0]?.cols - const columnsKeys = Object.keys(data[0]) - const selectedColumnKeys: string[] = columnsKeys.slice(cols[0], cols[1] + 1) - const selectedRowIndices: number[] = rows != undefined ? range(rows[0], rows[1] + 1) : range(0, data.length) + let filter = undefined + if(dataParameters.filter != undefined && dataParameters.filter[columnName] != undefined){ + filter = dataParameters.filter[columnName] + } else { + filter = new Filter(columnName, undefined, "") + } - let selection: TableSelection - if(rows == undefined){ + const setGroup = (group: string | undefined) => { + setDataParameters({...dataParameters, group: group}) + } - selection = {filters: new Filter("_pkid", "in", ""), columns: selectedColumnKeys} + return h(ColumnHeaderCell2, { + menuRenderer: () => h(TableMenu, {"columnName": columnName, "onFilterChange": onFilterChange, "filter": filter, "onGroupChange": setGroup, "group": dataParameters?.group}), + name: columnName, + style: { + backgroundColor: filter.is_valid() || dataParameters?.group == columnName ? "rgba(27,187,255,0.12)" : "#ffffff00" + } + }, []) + } - } else { - const dbIds = selectedRowIndices.map((row) => data[row]['_pkid']) - const filter = new Filter("_pkid", "in", "(" + dbIds.join(",") + ")") - selection = {columns: selectedColumnKeys, "filters": [filter]} - } - setTableSelection(selection) + const defaultColumnConfig = nonIdColumnNames.reduce((prev, columnName, index) => { + return { + ...prev, + [columnName]: h(Column, { + name: columnName, + className: FINAL_COLUMNS.includes(columnName) ? "final-column" : "", + columnHeaderCellRenderer: columnHeaderCellRenderer, + cellRenderer: (rowIndex) => h(EditableCell, { + onConfirm: (value) => { + const tableUpdate = getTableUpdate(url, value, columnName, rowIndex, data, dataParameters) + setTableUpdates([...tableUpdates, tableUpdate]) + }, + value: applyTableUpdates(data[rowIndex], columnName, tableUpdates) + }), + "key": columnName + }) + } + }, {}) + + console.log(defaultColumnConfig) + + const columnConfig = { + ...defaultColumnConfig, + "t_interval": h(Column, { + ...defaultColumnConfig["t_interval"].props, + cellRenderer: (rowIndex) => h(IntervalSelection, { + onConfirm: (value) => { + const tableUpdate = getTableUpdate(url, value, "t_interval", rowIndex, data, dataParameters) + setTableUpdates([...tableUpdates, tableUpdate]) + }, + value: applyTableUpdates(data[rowIndex], "t_interval", tableUpdates) + }) + }), + "b_interval": h(Column, { + ...defaultColumnConfig["b_interval"].props, + cellRenderer: (rowIndex) => h(IntervalSelection, { + onConfirm: (value) => { + const tableUpdate = getTableUpdate(url, value, "b_interval", rowIndex, data, dataParameters) + setTableUpdates([...tableUpdates, tableUpdate]) + }, + value: applyTableUpdates(data[rowIndex], "b_interval", tableUpdates) + }) + }) } + + const rowHeaderCellRenderer = (rowIndex: number) => { - const headerKey = group ? group : "_pkid" + const headerKey = dataParameters?.group ? dataParameters?.group : "_pkid" let name = data[rowIndex][headerKey] if (name == null) { @@ -282,9 +242,8 @@ export default function EditTable({ url }) { { onClick: () => { setTableUpdates([]); - setEditedData([]); }, - disabled: isEmptyArray(editedData), + disabled: isEmptyArray(tableUpdates), }, ["Clear changes"] ), @@ -293,7 +252,7 @@ export default function EditTable({ url }) { { type: "submit", onClick: submitTableUpdates, - disabled: isEmptyArray(editedData), + disabled: isEmptyArray(tableUpdates), intent: "success", }, ["Submit"] @@ -303,15 +262,13 @@ export default function EditTable({ url }) { h( Table2, { - selectionModes: group ? RegionCardinality.CELLS : SelectionModes.COLUMNS_AND_CELLS, + selectionModes: dataParameters?.group ? RegionCardinality.CELLS : SelectionModes.COLUMNS_AND_CELLS, rowHeaderCellRenderer: rowHeaderCellRenderer, - onSelection: (selections: Selection[]) => - getSelectionValues(selections), numRows: data.length, // Dumb hacks to try to get the table to rerender on changes - cellRendererDependencies: [editedData, data], + cellRendererDependencies: [data, tableUpdates], }, - columns + Object.values(columnConfig) ), h.if(updateProgress != undefined)( ProgressPopover, diff --git a/src/pages/maps/@id/edit/table-menu.ts b/src/pages/maps/@id/edit/table-menu.ts index fa848c1d..aa06af27 100644 --- a/src/pages/maps/@id/edit/table-menu.ts +++ b/src/pages/maps/@id/edit/table-menu.ts @@ -47,13 +47,14 @@ const OperatorFilterOption: ItemRenderer<ColumnOperatorOption> = (column, { hand } interface TableMenuProps { + columnName: string; onFilterChange: (query: OperatorQueryParameter) => void; filter: Filter; onGroupChange: (group: string | undefined) => void; group: string | undefined; } -const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuProps) => { +const TableMenu = ({columnName, onFilterChange, filter, onGroupChange, group} : TableMenuProps) => { const [menuOpen, setMenuOpen] = React.useState<boolean>(false); const [inputPlaceholder, setInputPlaceholder] = React.useState<string>(""); @@ -70,7 +71,7 @@ const TableMenu = ({onFilterChange, filter, onGroupChange, group} : TableMenuPro const selectedExpression = validExpressions.find((expression) => expression.key === filter.operator); // Set if this group is active - const groupActive: boolean = group === filter.column_name; + const groupActive: boolean = group === columnName; return h(Menu, {}, [ h("div.filter-container", {}, [ diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts index 95b3e71a..a3d4613a 100644 --- a/src/pages/maps/@id/edit/table-util.ts +++ b/src/pages/maps/@id/edit/table-util.ts @@ -1,4 +1,4 @@ -import { ColumnOperators, Filters, TableSelection, TableUpdate } from "./table"; +import { ColumnOperators, Filters, TableSelection, TableUpdate, DataParameters } from "./table"; import {secureFetch} from "@macrostrat-web/security"; @@ -22,6 +22,36 @@ export class Filter { } } + get urlValue() { + return this.operator + "." + this.formattedValue + } + + passes = (data: {[key: string] : string}) => { + const filterValue = data[this.column_name] + switch (this.operator) { + case "eq": + return filterValue == this.value + case "lt": + return filterValue < this.value + case "le": + return filterValue <= this.value + case "gt": + return filterValue > this.value + case "ge": + return filterValue >= this.value + case "ne": + return filterValue != this.value + case "like": + return filterValue.includes(this.value) + case "in": + return this.value.includes(filterValue) + case "is": + return filterValue == this.value + default: + return false + } + } + is_valid = () => { if(this.operator == undefined || this.value == ""){ return false @@ -35,81 +65,107 @@ export class Filter { } -export function buildURL(baseURL: string, filters: Filter[], group: string | undefined){ - let updateURL = new URL(baseURL) - for(const filter of filters){ +export function buildURL(baseURL: string, dataParameters: DataParameters){ + let url = new URL(baseURL) + + // Order by ID if no group is specified + if(dataParameters?.group == undefined){ + url.searchParams.append("_pkid", "order_by" ) - if(filter.is_valid()) { - const [key, value] = filter.to_array() - updateURL.searchParams.append(key, value) + // Otherwise order by group and group by group + } else { + url.searchParams.append(dataParameters.group, "order_by" ) + url.searchParams.append(dataParameters.group, "group_by") + } + + // Add the page and page size + url.searchParams.append("page", dataParameters.select.page); + url.searchParams.append("page_size", dataParameters.select.pageSize); + + // Add the rest of the filters + if(dataParameters?.filter != undefined){ + for(const filter of Object.values(dataParameters?.filter)){ + const [columnName, filterValue] = filter.to_array() + url.searchParams.append(columnName, filterValue); } } - if(group != undefined){ - updateURL.searchParams.append(group, "group_by") + return url +} + +export const applyTableUpdates = (data: {[key: string]: string}, columnName: string, updates: TableUpdate[]) : string => { + + let value = data[columnName] + + for(const update of updates) { + value = update.applyToCell(value, data, columnName) } - return updateURL + return value } /** - * Builds a table update from the current table state + * Wraps around submitChange to filter based on the group */ export const getTableUpdate = ( + url: string, value: string, columnName: string, rowIndex: number, data: any[], - filters: Filters, - group: string | undefined + dataParameters: DataParameters ): TableUpdate => { - filters = {...filters} - if( group != undefined){ - filters[group] = new Filter(group, "eq", data[rowIndex][group]) + dataParameters = structuredClone(dataParameters) + if( dataParameters?.group != undefined){ + dataParameters.filter[dataParameters?.group] = new Filter(dataParameters?.group, "eq", data[rowIndex][dataParameters?.group]) } else { - filters["_pkid"] = new Filter("_pkid", "eq", data[rowIndex]["_pkid"]) + dataParameters.filter["_pkid"] = new Filter("_pkid", "eq", data[rowIndex]["_pkid"]) } - const selection: TableSelection = { - columns: [columnName], - filters: filters - } + const execute = async () => submitChange(url, value, [columnName], dataParameters.filter) - return { - selection, - value: value - } -} + const apply = (currentValue: string, row: {[key: string]: string}, cellColumnName: string) => { + + // If this function does not apply to this column skip it + if (cellColumnName != columnName) { + return currentValue + } -export const submitChanges = async (url: string, updates: TableUpdate[]) => { - for(const update of updates){ - console.log("Update: ", update) + // If this row doesn't pass all the filters skip it + if(dataParameters?.filter != undefined) { + for (const filter of Object.values(dataParameters.filter)) { + if (!filter.passes(row)) { + return currentValue + } + } + } - // await submitChange(url, update) + // Return the new value + return value } + + return {"execute": execute, "applyToCell": apply} as TableUpdate } -export const submitChange = async (url: string, {selection, value}: TableUpdate) => { +export const submitChange = async (url: string, value: string, columns: string[], filters: {[key: string] : Filter}) => { // Query per column - for (const column of selection.columns) { + for (const column of columns) { let updateURL = new URL(url); // Add the filters to the query parameters - for (const filter of Object.values(selection.filters)) { + for (const filter of Object.values(filters)) { // Check that the filter is valid if(!filter.is_valid()){ continue } - console.log("Filter: ", filter) - - const [searchTerm, searchValue] = filter.to_array() - updateURL.searchParams.append(searchTerm, searchValue); + const [columnName, filterValue] = filter.to_array() + updateURL.searchParams.append(columnName, filterValue); } // Create the request body @@ -134,4 +190,9 @@ export const submitChange = async (url: string, {selection, value}: TableUpdate) export function isEmptyArray(arr) { return arr.length == 0 || arr.every((x) => x == null); -} \ No newline at end of file +} + +export const range = (start, stop, step = 1) => + Array(Math.ceil((stop - start) / step)) + .fill(start) + .map((x, y) => x + y * step); \ No newline at end of file diff --git a/src/pages/maps/@id/edit/table.d.ts b/src/pages/maps/@id/edit/table.d.ts index 76f3afa5..e3b98ee0 100644 --- a/src/pages/maps/@id/edit/table.d.ts +++ b/src/pages/maps/@id/edit/table.d.ts @@ -37,6 +37,19 @@ interface TableSelection { // An object that represents a value update made on top of a specific TableSelection interface TableUpdate { - selection: TableSelection; - value: string; + // Function to execute this update + execute: () => Promise<void>; + // Function to apply this update to a cell + applyToCell: (currentValue: string, row: {[key: string]: string}, cellColumnName: string) => string; +} + +export interface DataParameters { + group?: string; + select: { + page?: string + pageSize?: string; + }; + filter: { + [key: string]: Filter; // Used for filters + } } \ No newline at end of file From 22c81eedd66fda3bba8c660f004de6347670ceea Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Tue, 12 Dec 2023 12:48:33 -0600 Subject: [PATCH 34/52] Fix the styles on the interval selectors --- .../components/cell/interval-selection.ts | 56 +++++++++++-------- .../maps/@id/edit/edit-table.module.sass | 2 +- src/pages/maps/@id/edit/override.sass | 6 ++ 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/pages/maps/@id/edit/components/cell/interval-selection.ts b/src/pages/maps/@id/edit/components/cell/interval-selection.ts index ee2237b9..745d5128 100644 --- a/src/pages/maps/@id/edit/components/cell/interval-selection.ts +++ b/src/pages/maps/@id/edit/components/cell/interval-selection.ts @@ -1,5 +1,5 @@ import {Button, MenuItem} from "@blueprintjs/core"; -import {Select2, ItemRenderer} from "@blueprintjs/select"; +import { Select, ItemRenderer, ItemPredicate } from "@blueprintjs/select"; import {EditableCell2Props, EditableCell2, Cell} from "@blueprintjs/table"; import React, {useEffect, useMemo} from "react"; @@ -51,7 +51,7 @@ const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClic active: modifiers.active, disabled: modifiers.disabled, key: interval.int_id, - label: interval.name, + label: interval.int_id.toString(), onClick: handleClick, onFocus: handleFocus, text: interval.name, @@ -62,55 +62,67 @@ const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClic const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2Props) => { - const [menuOpen, setMenuOpen] = React.useState<boolean>(false); const [intervalValues, setIntervalValues] = React.useState<Interval[]>([]); + const [localValue, setLocalValue] = React.useState<string>(value); - async function getIntervals() { - let response = await fetch(`https://macrostrat.org/api/defs/intervals?tilescale_id=11`) + const filterInterval: ItemPredicate<Interval> = (query, interval) => { - if (response.ok) { - let response_data = await response.json(); - setIntervalValues(response_data.success.data); + if(interval?.name == undefined){ + return false } + return interval.name.toLowerCase().indexOf(query.toLowerCase()) >= 0; } const interval = useMemo(() => { if(intervalValues.length == 0){ return null } else { - return intervalValues.filter((interval) => interval.int_id == parseInt(value))[0] + return intervalValues.filter((interval) => interval.int_id == parseInt(localValue))[0] } - }, [value, intervalValues]) + }, [localValue, intervalValues]) useEffect(() => { + + async function getIntervals() { + let response = await fetch(`https://macrostrat.org/api/defs/intervals?tilescale_id=11`) + + if (response.ok) { + let response_data = await response.json(); + setIntervalValues(response_data.success.data); + } + } + getIntervals() }, []) - console.log(interval) - return h(Cell, { - style: {padding: 0}, - ...props + ...props, + style: {...props.style, padding: 0}, }, [ - h(Select2<Interval>, { + h(Select<Interval>, { fill: true, items: intervalValues, className: "update-input-group", - filterable: false, - popoverProps: {isOpen: menuOpen}, + popoverProps: { + position: "bottom", + minimal: true + }, + popoverContentProps:{ + onWheelCapture: (event) => event.stopPropagation() + }, + itemPredicate: filterInterval, itemRenderer: IntervalOption, - onItemSelect: (interval: Interval) => { - setMenuOpen(false); + onItemSelect: (interval: Interval, e) => { onConfirm(interval.int_id.toString()) + setLocalValue(interval.int_id.toString()) }, - noResults: h(MenuItem, {disabled: true, text: "No results.", roleStructure: "listoption"}, []), + noResults: h(MenuItem, {disabled: true, text: "No results.", roleStructure: "listoption"}), }, [ h(Button, { style: {backgroundColor: interval?.color ?? "white", fontSize: "12px", minHeight: "0px", padding: "1.7px 10px", boxShadow: "none"}, fill: true, - onClick: () => setMenuOpen(!menuOpen), alignText: "left", - text: interval?.name ?? "Select an Interval", + text: h("span", {style: {overflow: "hidden", textOverflow: "ellipses"}}, interval?.name ?? "Select an Interval"), rightIcon: "double-caret-vertical", className: "update-input-group", placeholder: "Select A Filter" diff --git a/src/pages/maps/@id/edit/edit-table.module.sass b/src/pages/maps/@id/edit/edit-table.module.sass index 9a298b2f..2665ec0a 100644 --- a/src/pages/maps/@id/edit/edit-table.module.sass +++ b/src/pages/maps/@id/edit/edit-table.module.sass @@ -34,4 +34,4 @@ div.warning color: black border: #ffe26c solid 1px font-size: 1rem - padding-bottom: 0.2rem \ No newline at end of file + padding-bottom: 0.2rem diff --git a/src/pages/maps/@id/edit/override.sass b/src/pages/maps/@id/edit/override.sass index 840bb7ef..6c3c1ad9 100644 --- a/src/pages/maps/@id/edit/override.sass +++ b/src/pages/maps/@id/edit/override.sass @@ -1,4 +1,10 @@ +// For the button text in cells +.bp4-button-text + overflow: hidden + text-overflow: ellipsis + + // Need this to get the popups in front of the sidebar .bp4-portal z-index: 101 From 2a5528a00bae889fe6a65fb70568084431148544 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 12:27:07 -0600 Subject: [PATCH 35/52] UI changes to add submission --- .../components/cell/interval-selection.ts | 20 +- src/pages/maps/@id/edit/edit-page.ts | 70 +++---- src/pages/maps/@id/edit/edit-table.ts | 172 +++++++++++++----- src/pages/maps/@id/edit/table-util.ts | 66 ++++++- src/pages/maps/@id/edit/table.d.ts | 2 + 5 files changed, 235 insertions(+), 95 deletions(-) diff --git a/src/pages/maps/@id/edit/components/cell/interval-selection.ts b/src/pages/maps/@id/edit/components/cell/interval-selection.ts index 745d5128..077b0ad2 100644 --- a/src/pages/maps/@id/edit/components/cell/interval-selection.ts +++ b/src/pages/maps/@id/edit/components/cell/interval-selection.ts @@ -1,5 +1,5 @@ import {Button, MenuItem} from "@blueprintjs/core"; -import { Select, ItemRenderer, ItemPredicate } from "@blueprintjs/select"; +import { Select2, ItemRenderer, ItemPredicate } from "@blueprintjs/select"; import {EditableCell2Props, EditableCell2, Cell} from "@blueprintjs/table"; import React, {useEffect, useMemo} from "react"; @@ -33,7 +33,7 @@ const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClic if (interval == null) { return h(MenuItem, { - shouldDismissPopover: false, + shouldDismissPopover: true, active: modifiers.active, disabled: modifiers.disabled, key: "", @@ -47,7 +47,7 @@ const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClic return h(MenuItem, { style: {backgroundColor: interval.color}, - shouldDismissPopover: false, + shouldDismissPopover: true, active: modifiers.active, disabled: modifiers.disabled, key: interval.int_id, @@ -74,12 +74,14 @@ const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2P } const interval = useMemo(() => { - if(intervalValues.length == 0){ - return null - } else { - return intervalValues.filter((interval) => interval.int_id == parseInt(localValue))[0] + + let interval = null + if(intervalValues.length != 0){ + interval = intervalValues.filter((interval) => interval.int_id == parseInt(value))[0] } - }, [localValue, intervalValues]) + + return interval + }, [value, localValue, intervalValues]) useEffect(() => { @@ -99,7 +101,7 @@ const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2P ...props, style: {...props.style, padding: 0}, }, [ - h(Select<Interval>, { + h(Select2<Interval>, { fill: true, items: intervalValues, className: "update-input-group", diff --git a/src/pages/maps/@id/edit/edit-page.ts b/src/pages/maps/@id/edit/edit-page.ts index a7bfb41f..197777fe 100644 --- a/src/pages/maps/@id/edit/edit-page.ts +++ b/src/pages/maps/@id/edit/edit-page.ts @@ -8,7 +8,7 @@ import { WidthAdjustablePanel } from "./components"; import MapInterface from "./map-interface"; import { useStoredState } from "@macrostrat/ui-components"; import { ParentRouteButton } from "~/components/map-navbar"; -import { Button } from "@blueprintjs/core"; +import { Button, HotkeysProvider } from "@blueprintjs/core"; export const h = hyper.styled(styles); @@ -43,41 +43,43 @@ export default function EditInterface({ const title = mapBounds.properties.name; - return h("div.edit-page", [ - h( - WidthAdjustablePanel, - { - expand: !showMap, - className: "edit-page-content", - storageID: "edit-panel-width", - }, - // TODO: make this basename dynamic - h([ - h("div.edit-page-header", [ - h(ParentRouteButton, { parentRoute: "/maps/" }), - h("h2", title), - h("div.spacer"), - h("div.edit-page-buttons", [ - h(ShowMapButton, { showMap, setShowMap }), + return h(HotkeysProvider, [ + h("div.edit-page", [ + h( + WidthAdjustablePanel, + { + expand: !showMap, + className: "edit-page-content", + storageID: "edit-panel-width", + }, + // TODO: make this basename dynamic + h([ + h("div.edit-page-header", [ + h(ParentRouteButton, { parentRoute: "/maps/" }), + h("h2", title), + h("div.spacer"), + h("div.edit-page-buttons", [ + h(ShowMapButton, { showMap, setShowMap }), + ]), ]), - ]), - h(Router, { basename: `/maps/${source_id}/edit` }, [ - h(Routes, [ - h(Route, { - path: "", - element: h(EditMenu), - }), - h(Route, { - path: "polygons", - element: h(EditTable, { - url: `${import.meta.env.VITE_MACROSTRAT_INGEST_API}/sources/${source_id}/polygons`, + h(Router, { basename: `/maps/${source_id}/edit` }, [ + h(Routes, [ + h(Route, { + path: "", + element: h(EditMenu), }), - }), - ]), - ]), - ]) - ), - h.if(showMap)(MapInterface, { id: source_id, map: mapBounds }), + h(Route, { + path: "polygons", + element: h(EditTable, { + url: `${import.meta.env.VITE_MACROSTRAT_INGEST_API}/sources/${source_id}/polygons`, + }), + }), + ]), + ]) + ]) + ), + h.if(showMap)(MapInterface, { id: source_id, map: mapBounds }), + ]), ]); } diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index cfbf074b..38d56e5e 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -3,7 +3,7 @@ import hyper from "@macrostrat/hyper"; import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo, FunctionComponent } from "react"; -import { HotkeysProvider, InputGroup, Button } from "@blueprintjs/core"; +import { HotkeysProvider, InputGroup, Button, useHotkeys } from "@blueprintjs/core"; import { Spinner, ButtonGroup } from "@blueprintjs/core"; import { Column, @@ -24,7 +24,9 @@ import { submitChange, getTableUpdate, range, - applyTableUpdates + applyTableUpdate, + applyTableUpdates, + submitColumnCopy } from "~/pages/maps/@id/edit/table-util"; import TableMenu from "~/pages/maps/@id/edit/table-menu"; import IntervalSelection from "./components/cell/interval-selection"; @@ -34,7 +36,6 @@ import "./override.sass" import "@blueprintjs/table/lib/css/table.css"; import styles from "./edit-table.module.sass"; import { EditableCell } from "~/pages/maps/@id/edit/components/cell/editable-cell"; -import EditTable from "~/pages/maps/@id/edit/edit-table"; const h = hyper.styled(styles); @@ -54,9 +55,6 @@ const FINAL_COLUMNS = [ interface EditTableProps { url: string; - data: { - [key: string]: any - }; } interface TableState { @@ -68,32 +66,50 @@ interface TableState { export default function TableInterface({ url }: EditTableProps) { + // Selection State + const [selectedColumn, setSelectedColumn] = useState<string | undefined>(undefined) + const [copiedColumn, setCopiedColumn] = useState<string | undefined>(undefined) + // Data State - const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "999999"}, filter: {}}) + const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "9999999"}, filter: {}}) const [data, setData] = useState<any[]>([]) - const [dataToggle, setDataToggle] = useState<boolean>(false); // Error State const [error, setError] = useState<string | undefined>(undefined) // Table Update State - const [tableUpdates, setTableUpdates] = useState<TableUpdate[]>([]) + const [tableUpdates, _setTableUpdates] = useState<TableUpdate[]>([]) const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) - // Memoize non-id columns const nonIdColumnNames = useMemo(() => { return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] }, [data]) - console.log(tableUpdates) + const setTableUpdates = useCallback((newTableUpdates: TableUpdate[]) => { + + // If the table updates are empty, reset the data + if (newTableUpdates.length == 0) { + getData() + } + // If a new update is available apply it to the data + if(newTableUpdates.length > tableUpdates.length){ + setData(applyTableUpdate(data, newTableUpdates.slice(-1)[0])) + } - let getData = async () => { + _setTableUpdates(newTableUpdates) + + }, [tableUpdates, data]) + + const getData = useCallback( async () => { const dataURL = buildURL(url, dataParameters) const response = await fetch(dataURL) - const data = await response.json() + let data = await response.json() + + // Apply tableupdates to the data + data = applyTableUpdates(data, tableUpdates) if(data.length == 0){ setError("Warning: No results matched query") @@ -107,18 +123,67 @@ export default function TableInterface({ url }: EditTableProps) { setUpdateProgress(undefined) return data - } + }, [dataParameters, tableUpdates]) // On mount get data useEffect(() => { getData() }, [dataParameters]) - if(data.length == 0 && error == undefined){ - return h(Spinner) - } + const handlePaste = useCallback(() => { + if(copiedColumn != undefined && selectedColumn != undefined){ + + const tableUpdate = { + description: "Copy column " + copiedColumn + " to column " + selectedColumn + " for all rows", + applyToCell: (value: string, row, cellColumnName) => { + + if(cellColumnName != selectedColumn){ + return value + } + + // If this row doesn't pass all the filters skip it + if(dataParameters?.filter != undefined) { + for (const filter of Object.values(dataParameters.filter)) { + if (!filter.passes(row)) { + return value + } + } + } - const submitTableUpdates = async () => { + if(cellColumnName == selectedColumn){ + return row[copiedColumn] + } + + return value + }, + execute: async () => { + await submitColumnCopy(url, copiedColumn, selectedColumn, dataParameters) + } + } + + setTableUpdates([...tableUpdates, tableUpdate]) + } + }, [selectedColumn, copiedColumn, dataParameters]) + + const handleCopy = useCallback(() => { + setCopiedColumn(selectedColumn) + }, [selectedColumn]) + + const hotkeys = useMemo(() => [ + { + combo: "cmd+c", + label: "Copy data", + onKeyDown: handleCopy, + }, + { + combo: "cmd+v", + label: "Paste Data", + onKeyDown: handlePaste, + } + ], [handlePaste, handleCopy]); + const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys); + + const submitTableUpdates = useCallback(async () => { setUpdateProgress(0) @@ -138,10 +203,10 @@ export default function TableInterface({ url }: EditTableProps) { } setTableUpdates([]) - setDataToggle(!dataToggle) - } + getData() + }, [tableUpdates]) - const columnHeaderCellRenderer = (columnIndex: number) => { + const columnHeaderCellRenderer = useCallback((columnIndex: number) => { const columnName: string = nonIdColumnNames[columnIndex] @@ -169,8 +234,29 @@ export default function TableInterface({ url }: EditTableProps) { backgroundColor: filter.is_valid() || dataParameters?.group == columnName ? "rgba(27,187,255,0.12)" : "#ffffff00" } }, []) - } + }, [dataParameters, data]) + + const rowHeaderCellRenderer = useCallback((rowIndex: number) => { + + if (data.length == 0) { + return h(RowHeaderCell2, { "name": "NULL" }, []); + } + const headerKey = dataParameters?.group ? dataParameters?.group : "_pkid" + let name = data[rowIndex][headerKey] + + if (name == null) { + name = "NULL"; + } else if(name.length > 47){ + name = name.slice(0, 47) + "..." + } + + return h(RowHeaderCell2, { "name": name }, []); + }, [dataParameters, data]) + + if(data.length == 0 && error == undefined){ + return h(Spinner) + } const defaultColumnConfig = nonIdColumnNames.reduce((prev, columnName, index) => { return { @@ -184,15 +270,13 @@ export default function TableInterface({ url }: EditTableProps) { const tableUpdate = getTableUpdate(url, value, columnName, rowIndex, data, dataParameters) setTableUpdates([...tableUpdates, tableUpdate]) }, - value: applyTableUpdates(data[rowIndex], columnName, tableUpdates) + value: data[rowIndex][columnName] }), "key": columnName }) } }, {}) - console.log(defaultColumnConfig) - const columnConfig = { ...defaultColumnConfig, "t_interval": h(Column, { @@ -202,7 +286,7 @@ export default function TableInterface({ url }: EditTableProps) { const tableUpdate = getTableUpdate(url, value, "t_interval", rowIndex, data, dataParameters) setTableUpdates([...tableUpdates, tableUpdate]) }, - value: applyTableUpdates(data[rowIndex], "t_interval", tableUpdates) + value: data[rowIndex]["t_interval"] }) }), "b_interval": h(Column, { @@ -212,27 +296,18 @@ export default function TableInterface({ url }: EditTableProps) { const tableUpdate = getTableUpdate(url, value, "b_interval", rowIndex, data, dataParameters) setTableUpdates([...tableUpdates, tableUpdate]) }, - value: applyTableUpdates(data[rowIndex], "b_interval", tableUpdates) + value: data[rowIndex]["b_interval"] }) }) } + console.log("TableUpdates", tableUpdates, tableUpdates.length, tableUpdates.length == 0) - - const rowHeaderCellRenderer = (rowIndex: number) => { - const headerKey = dataParameters?.group ? dataParameters?.group : "_pkid" - let name = data[rowIndex][headerKey] - - if (name == null) { - name = "NULL"; - } else if(name.length > 47){ - name = name.slice(0, 47) + "..." - } - - return h(RowHeaderCell2, { "name": name }, []); - }; - - return h(HotkeysProvider, {}, [ + return h("div", { + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + tabIndex: 0, + }, [ h("div.table-container", {}, [ h.if(error != undefined)("div.warning", {}, [error]), h("div.input-form", {}, [ @@ -243,7 +318,7 @@ export default function TableInterface({ url }: EditTableProps) { onClick: () => { setTableUpdates([]); }, - disabled: isEmptyArray(tableUpdates), + disabled: tableUpdates.length == 0, }, ["Clear changes"] ), @@ -252,7 +327,7 @@ export default function TableInterface({ url }: EditTableProps) { { type: "submit", onClick: submitTableUpdates, - disabled: isEmptyArray(tableUpdates), + disabled: tableUpdates.length == 0, intent: "success", }, ["Submit"] @@ -264,6 +339,15 @@ export default function TableInterface({ url }: EditTableProps) { { selectionModes: dataParameters?.group ? RegionCardinality.CELLS : SelectionModes.COLUMNS_AND_CELLS, rowHeaderCellRenderer: rowHeaderCellRenderer, + onSelection: (selections: Selection[]) => { + const selectedColumns = selections[0]?.cols + if(selectedColumns[0] == selectedColumns[1] && selections[0]?.rows == undefined){ + setSelectedColumn(nonIdColumnNames[selectedColumns[0]]) + } else { + setSelectedColumn(undefined) + } + console.log(selectedColumn) + }, numRows: data.length, // Dumb hacks to try to get the table to rerender on changes cellRendererDependencies: [data, tableUpdates], diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts index a3d4613a..6a29e553 100644 --- a/src/pages/maps/@id/edit/table-util.ts +++ b/src/pages/maps/@id/edit/table-util.ts @@ -94,15 +94,26 @@ export function buildURL(baseURL: string, dataParameters: DataParameters){ return url } -export const applyTableUpdates = (data: {[key: string]: string}, columnName: string, updates: TableUpdate[]) : string => { +export const applyTableUpdate = (data: any[], tableUpdate: TableUpdate) => { - let value = data[columnName] + let appliedData = structuredClone(data) + for(const [rowIndex, row] of data.entries()){ + for(const columnName of Object.keys(row)){ + appliedData[rowIndex][columnName] = tableUpdate.applyToCell(appliedData[rowIndex][columnName], row, columnName) + } + } + + return appliedData +} + +export const applyTableUpdates = (data: any[], tableUpdates: TableUpdate[]) => { - for(const update of updates) { - value = update.applyToCell(value, data, columnName) + let appliedData = structuredClone(data) + for(const tableUpdate of tableUpdates){ + appliedData = applyTableUpdate(appliedData, tableUpdate) } - return value + return appliedData } /** @@ -141,12 +152,15 @@ export const getTableUpdate = ( } } } - // Return the new value return value } - return {"execute": execute, "applyToCell": apply} as TableUpdate + return { + description: "Update " + columnName + " to " + value + " for " + JSON.stringify(dataParameters.filter), + "execute": execute, + "applyToCell": apply + } as TableUpdate } export const submitChange = async (url: string, value: string, columns: string[], filters: {[key: string] : Filter}) => { @@ -188,6 +202,42 @@ export const submitChange = async (url: string, value: string, columns: string[] } }; +export const submitColumnCopy = async (url: string, sourceColumn: string, targetColumn: string, dataParameters: DataParameters) => { + + + let updateURL = new URL(url + "/" + targetColumn); + + // Add the filters to the query parameters + for (const filter of Object.values(dataParameters.filter)) { + + // Check that the filter is valid + if(!filter.is_valid()){ + continue + } + + const [columnName, filterValue] = filter.to_array() + updateURL.searchParams.append(columnName, filterValue); + } + + // Create the request body + let patch = { "source_column": sourceColumn }; + + // Send the request + let response = await secureFetch(updateURL, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(patch), + }); + + if (response.status != 204) { + + // Stop execution if the request failed + throw Error("Failed to update"); + } +} + export function isEmptyArray(arr) { return arr.length == 0 || arr.every((x) => x == null); } @@ -195,4 +245,4 @@ export function isEmptyArray(arr) { export const range = (start, stop, step = 1) => Array(Math.ceil((stop - start) / step)) .fill(start) - .map((x, y) => x + y * step); \ No newline at end of file + .map((x, y) => x + y * step); diff --git a/src/pages/maps/@id/edit/table.d.ts b/src/pages/maps/@id/edit/table.d.ts index e3b98ee0..3e394c0e 100644 --- a/src/pages/maps/@id/edit/table.d.ts +++ b/src/pages/maps/@id/edit/table.d.ts @@ -37,6 +37,8 @@ interface TableSelection { // An object that represents a value update made on top of a specific TableSelection interface TableUpdate { + // Helpful for debugging + description?: string // Function to execute this update execute: () => Promise<void>; // Function to apply this update to a cell From 2f9b46a329f899d58ff62db49bb5f05af2347d08 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 12:52:44 -0600 Subject: [PATCH 36/52] Update Github action tagging --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 5301dd3f..3091b85e 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,7 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb - type=raw,value={{tag}}-{{date 'YYYYMMDDHHmmss'}} + type=ref,event=tag,value={{tag}}-{{date 'YYYYMMDDHHmmss'}} type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} - From bad319bd54025d13d053da185f888ad0791652ac Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 13:06:24 -0600 Subject: [PATCH 37/52] Update Github action tagging to get a tag of form {{tag}}-{{timestamp}} --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 3091b85e..1ea4de28 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,7 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb - type=ref,event=tag,value={{tag}}-{{date 'YYYYMMDDHHmmss'}} + type=ref,event=tag,suffix=-{{date 'YYYYMMDDHHmmss'}} type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} - From f9e7cf055d44413f22d86751114493e943e64c92 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 13:07:26 -0600 Subject: [PATCH 38/52] Update Github action tagging to get a tag of form {{branch}}-{{timestamp}} --- .github/workflows/build-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 1ea4de28..d5e3c711 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,6 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb + type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}} type=ref,event=tag,suffix=-{{date 'YYYYMMDDHHmmss'}} type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} type=raw,value=sha-{{sha}} From e04b52075fdbffaacdbcad94d805ff5668c83fbd Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 16:10:19 -0600 Subject: [PATCH 39/52] Fixup a merge error and commit updated lock file --- server/index.js | 2 +- src/pages/dev/security/index.page.route.ts | 2 +- src/pages/maps/@id/edit/index.page.ts | 2 +- src/pages/maps/@id/edit/map-interface.ts | 2 +- yarn.lock | 377 ++++++++++++++++++++- 5 files changed, 376 insertions(+), 9 deletions(-) diff --git a/server/index.js b/server/index.js index d71d0466..c5f8e1c5 100644 --- a/server/index.js +++ b/server/index.js @@ -6,7 +6,7 @@ import express from "express" import compression from "compression" -import { renderPage } from "vite-plugin-ssr/server" +import { renderPage } from "vike/server"; import { root } from "./root.js" // Auth imports diff --git a/src/pages/dev/security/index.page.route.ts b/src/pages/dev/security/index.page.route.ts index 7ffb09e9..136e3185 100644 --- a/src/pages/dev/security/index.page.route.ts +++ b/src/pages/dev/security/index.page.route.ts @@ -1,4 +1,4 @@ -import { render, redirect } from 'vite-plugin-ssr/abort' +import { render, redirect } from 'vike/abort' export const guard = (pageContext) => { const { user } = pageContext diff --git a/src/pages/maps/@id/edit/index.page.ts b/src/pages/maps/@id/edit/index.page.ts index 559f8e79..e2f5ab29 100644 --- a/src/pages/maps/@id/edit/index.page.ts +++ b/src/pages/maps/@id/edit/index.page.ts @@ -1,5 +1,5 @@ import { PageContextBuiltInServer } from "vike/types"; -import { SETTINGS } from "~/map-interface/settings"; +import { SETTINGS } from "~/settings"; import h from "@macrostrat/hyper"; import { ClientOnly } from "~/renderer/client-only"; diff --git a/src/pages/maps/@id/edit/map-interface.ts b/src/pages/maps/@id/edit/map-interface.ts index cb376e55..d4ae0fc1 100644 --- a/src/pages/maps/@id/edit/map-interface.ts +++ b/src/pages/maps/@id/edit/map-interface.ts @@ -13,7 +13,7 @@ import boundingBox from "@turf/bbox"; import { LngLatBoundsLike } from "mapbox-gl"; import { useEffect, useMemo, useState } from "react"; import { MapNavbar } from "~/components/map-navbar"; -import { SETTINGS } from "~/map-interface/settings"; +import { SETTINGS } from "~/settings"; import "~/styles/global.styl"; import { s3Address, tempImageIndex } from "../../raster-images"; import styles from "./main.module.sass"; diff --git a/yarn.lock b/yarn.lock index ca9e74c8..b900301f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,7 +241,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: @@ -1622,6 +1622,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.5": + version: 7.23.6 + resolution: "@babel/runtime@npm:7.23.6" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 1a8eaf3d3a103ef5227b60ca7ab5c589118c36ca65ef2d64e65380b32a98a3f3b5b3ef96660fa0471b079a18b619a8317f3e7f03ab2b930c45282a8b69ed9a16 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -2150,6 +2159,25 @@ __metadata: languageName: node linkType: hard +"@emotion/babel-plugin@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/babel-plugin@npm:11.11.0" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/serialize": ^1.1.2 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: 6b363edccc10290f7a23242c06f88e451b5feb2ab94152b18bb8883033db5934fb0e421e2d67d09907c13837c21218a3ac28c51707778a54d6cd3706c0c2f3f9 + languageName: node + linkType: hard + "@emotion/cache@npm:11.7.1": version: 11.7.1 resolution: "@emotion/cache@npm:11.7.1" @@ -2175,7 +2203,7 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.7.1": +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.7.1": version: 11.11.0 resolution: "@emotion/cache@npm:11.11.0" dependencies: @@ -2236,6 +2264,15 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^1.2.1": + version: 1.2.1 + resolution: "@emotion/is-prop-valid@npm:1.2.1" + dependencies: + "@emotion/memoize": ^0.8.1 + checksum: 8f42dc573a3fad79b021479becb639b8fe3b60bdd1081a775d32388bca418ee53074c7602a4c845c5f75fa6831eb1cbdc4d208cc0299f57014ed3a02abcad16a + languageName: node + linkType: hard + "@emotion/memoize@npm:0.7.4": version: 0.7.4 resolution: "@emotion/memoize@npm:0.7.4" @@ -2280,6 +2317,27 @@ __metadata: languageName: node linkType: hard +"@emotion/react@npm:^11.11.1": + version: 11.11.1 + resolution: "@emotion/react@npm:11.11.1" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/cache": ^11.11.0 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: aec3c36650f5f0d3d4445ff44d73dd88712b1609645b6af3e6d08049cfbc51f1785fe13dea1a1d4ab1b0800d68f2339ab11e459687180362b1ef98863155aae5 + languageName: node + linkType: hard + "@emotion/serialize@npm:1.0.2": version: 1.0.2 resolution: "@emotion/serialize@npm:1.0.2" @@ -2306,7 +2364,7 @@ __metadata: languageName: node linkType: hard -"@emotion/serialize@npm:^1.0.2": +"@emotion/serialize@npm:^1.0.2, @emotion/serialize@npm:^1.1.2": version: 1.1.2 resolution: "@emotion/serialize@npm:1.1.2" dependencies: @@ -2333,6 +2391,26 @@ __metadata: languageName: node linkType: hard +"@emotion/styled@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/styled@npm:11.11.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/is-prop-valid": ^1.2.1 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 904f641aad3892c65d7d6c0808b036dae1e6d6dad4861c1c7dc0baa59977047c6cad220691206eba7b4059f1a1c6e6c1ef4ebb8c829089e280fa0f2164a01e6b + languageName: node + linkType: hard + "@emotion/stylis@npm:0.8.5": version: 0.8.5 resolution: "@emotion/stylis@npm:0.8.5" @@ -2354,6 +2432,15 @@ __metadata: languageName: node linkType: hard +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": + version: 1.0.1 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" + peerDependencies: + react: ">=16.8.0" + checksum: 700b6e5bbb37a9231f203bb3af11295eed01d73b2293abece0bc2a2237015e944d7b5114d4887ad9a79776504aa51ed2a8b0ddbc117c54495dd01a6b22f93786 + languageName: node + linkType: hard + "@emotion/utils@npm:0.11.3": version: 0.11.3 resolution: "@emotion/utils@npm:0.11.3" @@ -2715,6 +2802,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.4.2": + version: 1.5.2 + resolution: "@floating-ui/core@npm:1.5.2" + dependencies: + "@floating-ui/utils": ^0.1.3 + checksum: e22de0a5e8a703fe14d9cfb72aeb67c0056c4ae6aa241539934ecb2af56448534b434a7587ecb5de154c21c3c73e44c19249b05c6b67a58eae7861188c8e69ac + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.5.1": + version: 1.5.3 + resolution: "@floating-ui/dom@npm:1.5.3" + dependencies: + "@floating-ui/core": ^1.4.2 + "@floating-ui/utils": ^0.1.3 + checksum: 00053742064aac70957f0bd5c1542caafb3bfe9716588bfe1d409fef72a67ed5e60450d08eb492a77f78c22ed1ce4f7955873cc72bf9f9caf2b0f43ae3561c21 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.4": + version: 2.0.4 + resolution: "@floating-ui/react-dom@npm:2.0.4" + dependencies: + "@floating-ui/dom": ^1.5.1 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 91b2369e25f84888486e48c1656117468248906034ed482d411bb9ed1061b908dd32435b4ca3d0cd0ca6083291510a98ce74d76c671d5cc25b0c41e5fa824bae + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.1.3": + version: 0.1.6 + resolution: "@floating-ui/utils@npm:0.1.6" + checksum: b34d4b5470869727f52e312e08272edef985ba5a450a76de0917ba0a9c6f5df2bdbeb99448e2c60f39b177fb8981c772ff1831424e75123471a27ebd5b52c1eb + languageName: node + linkType: hard + "@hypnosphi/create-react-context@npm:^0.3.1": version: 0.3.1 resolution: "@hypnosphi/create-react-context@npm:0.3.1" @@ -3189,6 +3314,12 @@ __metadata: languageName: unknown linkType: soft +"@macrostrat-web/security@workspace:*, @macrostrat-web/security@workspace:packages/security": + version: 0.0.0-use.local + resolution: "@macrostrat-web/security@workspace:packages/security" + languageName: unknown + linkType: soft + "@macrostrat/api-types@workspace:deps/web-components/packages/api-types": version: 0.0.0-use.local resolution: "@macrostrat/api-types@workspace:deps/web-components/packages/api-types" @@ -3709,10 +3840,14 @@ __metadata: "@babel/preset-typescript": ^7.18.6 "@blueprintjs/core": ^4.14.1 "@blueprintjs/select": 4 + "@blueprintjs/table": ^4 + "@emotion/react": ^11.11.1 + "@emotion/styled": ^11.11.0 "@lagunovsky/redux-react-router": ^3.2.0 "@loadable/component": ^5.14.1 "@macrostrat-web/data-sheet-test": "workspace:*" "@macrostrat-web/globe": "workspace:*" + "@macrostrat-web/security": "workspace:*" "@macrostrat/api-utils": "workspace:*" "@macrostrat/api-views": "workspace:*" "@macrostrat/column-components": "workspace:*" @@ -3730,6 +3865,7 @@ __metadata: "@mapbox/tilebelt": ^1.0.2 "@mapbox/vector-tile": ^1.3.1 "@mdx-js/rollup": ^2.3.0 + "@mui/material": ^5.14.16 "@turf/bbox": ^6.5.0 "@turf/boolean-contains": ^6.5.0 "@turf/buffer": ^6.5.0 @@ -3752,6 +3888,7 @@ __metadata: chroma-js: ^2.4.2 classnames: ^2.2.6 compression: ^1.7.4 + cookie-parser: ^1.4.6 cross-env: ^7.0.3 d3-array: ^3.1.1 d3-axis: ^3.0.0 @@ -3764,6 +3901,7 @@ __metadata: express: ^4.18.2 history: ^5.3.0 immutability-helper: ^3.1.1 + jose: ^5.1.2 mapbox-gl: ^2.15.0 new-github-issue-url: ^1.0.0 pbf: ^3.2.1 @@ -3790,6 +3928,7 @@ __metadata: transition-hook: ^1.5.2 ts-node: ^10.9.1 typescript: ^5.1.6 + use-debounce: ^9.0.4 use-react-router-breadcrumbs: ^3.2.1 use-resize-observer: ^9.1.0 vike: ^0.4.150 @@ -4091,6 +4230,164 @@ __metadata: languageName: node linkType: hard +"@mui/base@npm:5.0.0-beta.27": + version: 5.0.0-beta.27 + resolution: "@mui/base@npm:5.0.0-beta.27" + dependencies: + "@babel/runtime": ^7.23.5 + "@floating-ui/react-dom": ^2.0.4 + "@mui/types": ^7.2.11 + "@mui/utils": ^5.15.0 + "@popperjs/core": ^2.11.8 + clsx: ^2.0.0 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: c51a80c9ffe157f93ec0fbba73a7a0f985929a427a900915db993772ea33dfc69c48591fd59d0c794f6bfc8fcb923257eba5bad485c4e8b24cb008331a52ff37 + languageName: node + linkType: hard + +"@mui/core-downloads-tracker@npm:^5.15.0": + version: 5.15.0 + resolution: "@mui/core-downloads-tracker@npm:5.15.0" + checksum: a7aadd4071ff715e618b8db647137579ca63cab4bf6e3acfe86a7d461f71605fc7ce44eeea5b1e789faae8546617b1f7d14c72a0c00fa3d59951b6eee42a6c5d + languageName: node + linkType: hard + +"@mui/material@npm:^5.14.16": + version: 5.15.0 + resolution: "@mui/material@npm:5.15.0" + dependencies: + "@babel/runtime": ^7.23.5 + "@mui/base": 5.0.0-beta.27 + "@mui/core-downloads-tracker": ^5.15.0 + "@mui/system": ^5.15.0 + "@mui/types": ^7.2.11 + "@mui/utils": ^5.15.0 + "@types/react-transition-group": ^4.4.9 + clsx: ^2.0.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + react-is: ^18.2.0 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 8596780e22623844bf211838cc3998c989718c950c2bfdbb4c3ba5e4852a4e2ffd6b150381f7cd5bf8c1578ab9422d4ed94bdc0fc1dbdd1eda8e4c8a5987b98a + languageName: node + linkType: hard + +"@mui/private-theming@npm:^5.15.0": + version: 5.15.0 + resolution: "@mui/private-theming@npm:5.15.0" + dependencies: + "@babel/runtime": ^7.23.5 + "@mui/utils": ^5.15.0 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 37f107e53b0e6941b06c536d9a9392dd74e96366fbc1d43d0e90f8602468ec0e2d7bba9425cd724bbb0e8fe3fc6216df740a4bdec8d882a19e98f4d647c721a7 + languageName: node + linkType: hard + +"@mui/styled-engine@npm:^5.15.0": + version: 5.15.0 + resolution: "@mui/styled-engine@npm:5.15.0" + dependencies: + "@babel/runtime": ^7.23.5 + "@emotion/cache": ^11.11.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: e46ca6cd914cde70dd545d3ea039f95d8ff568a1f5d0fcaceea8ef9d321b8f7968770220790d1949afdd86ea6486a2462fb76d14637f0de5f9a2c6bbbb402fd4 + languageName: node + linkType: hard + +"@mui/system@npm:^5.15.0": + version: 5.15.0 + resolution: "@mui/system@npm:5.15.0" + dependencies: + "@babel/runtime": ^7.23.5 + "@mui/private-theming": ^5.15.0 + "@mui/styled-engine": ^5.15.0 + "@mui/types": ^7.2.11 + "@mui/utils": ^5.15.0 + clsx: ^2.0.0 + csstype: ^3.1.2 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 57f9d199a724ac29415292ef1ab5b5583fcc4b8284a73dffec1bfa9ffbdfe75d063d7fc51e958750ce209c5f1c4b9bd50e33ecff51ef9f21be4f320fc3571147 + languageName: node + linkType: hard + +"@mui/types@npm:^7.2.11": + version: 7.2.11 + resolution: "@mui/types@npm:7.2.11" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: ce6bbe8ba963af218bf86797f4c8adbf0f294047adeaf6596d3bb4a96f1b01701f1b1ae7dcfe2f7972f6e23c85eee4187e496fb481541713ed8bf12e96f3c34f + languageName: node + linkType: hard + +"@mui/utils@npm:^5.15.0": + version: 5.15.0 + resolution: "@mui/utils@npm:5.15.0" + dependencies: + "@babel/runtime": ^7.23.5 + "@types/prop-types": ^15.7.11 + prop-types: ^15.8.1 + react-is: ^18.2.0 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1d078f60f52ec83182febd3f8bd49988425730fe808697b498df7b4fafe32ff08e721ca9dcf97b11623770ba02141d458ecb5b89c8ed4ec456c9a8d21055285c + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -5125,7 +5422,7 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.9.3": +"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 @@ -6784,6 +7081,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.11": + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 + languageName: node + linkType: hard + "@types/q@npm:^1.5.1": version: 1.5.6 resolution: "@types/q@npm:1.5.6" @@ -6853,6 +7157,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.9": + version: 4.4.10 + resolution: "@types/react-transition-group@npm:4.4.10" + dependencies: + "@types/react": "*" + checksum: fe2ea11f70251e9f79f368e198c18fd469b1d4f1e1d44e4365845b44e15974b0ec925100036f449b023b0ca3480a82725c5f0a73040e282ad32ec7b0def9b57c + languageName: node + linkType: hard + "@types/react@npm:*, @types/react@npm:>=15, @types/react@npm:^18.2.20": version: 18.2.22 resolution: "@types/react@npm:18.2.22" @@ -8766,7 +9079,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-macros@npm:^3.0.1": +"babel-plugin-macros@npm:^3.0.1, babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" dependencies: @@ -10105,6 +10418,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.0.0": + version: 2.0.0 + resolution: "clsx@npm:2.0.0" + checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e + languageName: node + linkType: hard + "cmd-shim@npm:^3.0.0, cmd-shim@npm:^3.0.3": version: 3.0.3 resolution: "cmd-shim@npm:3.0.3" @@ -10600,6 +10920,16 @@ __metadata: languageName: node linkType: hard +"cookie-parser@npm:^1.4.6": + version: 1.4.6 + resolution: "cookie-parser@npm:1.4.6" + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + checksum: 1e5a63aa82e8eb4e02d2977c6902983dee87b02e87ec5ec43ac3cb1e72da354003716570cd5190c0ad9e8a454c9d3237f4ad6e2f16d0902205a96a1c72b77ba5 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -10607,6 +10937,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.4.1": + version: 0.4.1 + resolution: "cookie@npm:0.4.1" + checksum: bd7c47f5d94ab70ccdfe8210cde7d725880d2fcda06d8e375afbdd82de0c8d3b73541996e9ce57d35f67f672c4ee6d60208adec06b3c5fc94cebb85196084cf8 + languageName: node + linkType: hard + "cookie@npm:0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -11228,6 +11565,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.1.2": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + "currently-unhandled@npm:^0.4.1": version: 0.4.1 resolution: "currently-unhandled@npm:0.4.1" @@ -18017,6 +18361,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^5.1.2": + version: 5.1.3 + resolution: "jose@npm:5.1.3" + checksum: c0225c3408b1c3fcfc40c68161e5e14d554c606ffa428250e0db618469df52f4ce32c69918e296c8c299db1081981638096be838426f61d926269d35602fd814 + languageName: node + linkType: hard + "jpeg-js@npm:^0.4.1": version: 0.4.4 resolution: "jpeg-js@npm:0.4.4" @@ -24538,6 +24889,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.2.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e + languageName: node + linkType: hard + "react-json-tree@npm:^0.15.0": version: 0.15.2 resolution: "react-json-tree@npm:0.15.2" @@ -29230,6 +29588,15 @@ __metadata: languageName: node linkType: hard +"use-debounce@npm:^9.0.4": + version: 9.0.4 + resolution: "use-debounce@npm:9.0.4" + peerDependencies: + react: ">=16.8.0" + checksum: 37da4ecbe4e10a6230580cac03a8cae1788ea3e417dfdd92fcf654325458cf1b4567fd57bebf888edab62701a6abe47059a585008fd04228784f223f94d66ce4 + languageName: node + linkType: hard + "use-element-dimensions@npm:^2.1.3": version: 2.1.3 resolution: "use-element-dimensions@npm:2.1.3" From f18c2603ee3083000ac227b0d265cb8de02e8840 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 16:10:31 -0600 Subject: [PATCH 40/52] Allow clearing of commits --- src/pages/maps/@id/edit/edit-page.ts | 2 +- src/pages/maps/@id/edit/edit-table.ts | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/pages/maps/@id/edit/edit-page.ts b/src/pages/maps/@id/edit/edit-page.ts index 197777fe..ee7dac6a 100644 --- a/src/pages/maps/@id/edit/edit-page.ts +++ b/src/pages/maps/@id/edit/edit-page.ts @@ -3,7 +3,7 @@ import styles from "./edit-page.module.sass"; import { useState } from "react"; import EditTable from "./edit-table"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; -import { LinkButton } from "~/map-interface/components/buttons"; +import { LinkButton } from "~/pages/map/map-interface/components/buttons"; import { WidthAdjustablePanel } from "./components"; import MapInterface from "./map-interface"; import { useStoredState } from "@macrostrat/ui-components"; diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 38d56e5e..597ddd33 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -1,7 +1,5 @@ import hyper from "@macrostrat/hyper"; - - import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo, FunctionComponent } from "react"; import { HotkeysProvider, InputGroup, Button, useHotkeys } from "@blueprintjs/core"; import { Spinner, ButtonGroup } from "@blueprintjs/core"; @@ -85,23 +83,25 @@ export default function TableInterface({ url }: EditTableProps) { return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] }, [data]) - const setTableUpdates = useCallback((newTableUpdates: TableUpdate[]) => { + const setTableUpdates = useCallback(async (newTableUpdates: TableUpdate[]) => { // If the table updates are empty, reset the data if (newTableUpdates.length == 0) { - getData() + let newData = await getData(newTableUpdates, dataParameters) + setData(newData) } // If a new update is available apply it to the data if(newTableUpdates.length > tableUpdates.length){ - setData(applyTableUpdate(data, newTableUpdates.slice(-1)[0])) + let newData = applyTableUpdate(data, newTableUpdates.slice(-1)[0]) + setData(newData) } _setTableUpdates(newTableUpdates) - }, [tableUpdates, data]) + }, [data, tableUpdates, dataParameters]) - const getData = useCallback( async () => { + const getData = useCallback( async (tableUpdates: TableUpdate[], dataParameters: DataParameters) => { const dataURL = buildURL(url, dataParameters) @@ -116,18 +116,19 @@ export default function TableInterface({ url }: EditTableProps) { } else { setError(undefined) - setData(data) } // Remove the progress bar on data reload setUpdateProgress(undefined) return data - }, [dataParameters, tableUpdates]) + }, []) // On mount get data useEffect(() => { - getData() + (async () => { + setData(await getData(tableUpdates, dataParameters)) + })() }, [dataParameters]) const handlePaste = useCallback(() => { @@ -203,7 +204,6 @@ export default function TableInterface({ url }: EditTableProps) { } setTableUpdates([]) - getData() }, [tableUpdates]) const columnHeaderCellRenderer = useCallback((columnIndex: number) => { @@ -301,8 +301,6 @@ export default function TableInterface({ url }: EditTableProps) { }) } - console.log("TableUpdates", tableUpdates, tableUpdates.length, tableUpdates.length == 0) - return h("div", { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, From c5a57aab3684b6839a7354df7639d22d86395a60 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Thu, 14 Dec 2023 16:18:51 -0600 Subject: [PATCH 41/52] Add start to auth ui --- packages/security/src/index.ts | 6 +++++- src/pages/dev/security/endpoint/index.page.ts | 9 +++++++++ src/pages/dev/security/index.page.route.ts | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/pages/dev/security/endpoint/index.page.ts diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index ad9978cc..dd23226a 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -10,7 +10,11 @@ export const secureFetch = async (url, options) => { const response = await fetch(url, options); if (response.status === 401 || response.status === 403) { - window.open(`${import.meta.env.VITE_MACROSTRAT_INGEST_API}/security/login`, '_blank').focus(); + + const url = new URL(`${import.meta.env.VITE_MACROSTRAT_INGEST_API}/security/login`); + url.searchParams.append("return_url", `${window.location.origin}/dev/security/endpoint`); + + window.open(url, '_blank').focus(); throw {name: "UnauthorizedError", message: "User is not logged in"} } diff --git a/src/pages/dev/security/endpoint/index.page.ts b/src/pages/dev/security/endpoint/index.page.ts new file mode 100644 index 00000000..d56f3f43 --- /dev/null +++ b/src/pages/dev/security/endpoint/index.page.ts @@ -0,0 +1,9 @@ +import {default as h} from "@macrostrat/hyper"; + +export function Page() { + + return h("div", [ + h("span", "You are logged in, you can now close this tab.") + ]); +} + diff --git a/src/pages/dev/security/index.page.route.ts b/src/pages/dev/security/index.page.route.ts index 136e3185..842275a7 100644 --- a/src/pages/dev/security/index.page.route.ts +++ b/src/pages/dev/security/index.page.route.ts @@ -13,8 +13,8 @@ export const guard = (pageContext) => { throw redirect('/login') */ } - if (!user.groups.includes("admin")) { + if (!user.groups.includes(1)) { // Render the error page and show message to the user - throw render(403, 'Only admins are allowed to access this page.') + return render(403, 'Only admins are allowed to access this page.') } } \ No newline at end of file From 37d8c30c23b0dc534f1f4e139435f24b109ce6ed Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 08:50:53 -0600 Subject: [PATCH 42/52] Pass in intervals from parent --- .../components/cell/interval-selection.ts | 25 ++++--------------- src/pages/maps/@id/edit/edit-table.ts | 24 +++++++++++++++--- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/pages/maps/@id/edit/components/cell/interval-selection.ts b/src/pages/maps/@id/edit/components/cell/interval-selection.ts index 077b0ad2..accb061b 100644 --- a/src/pages/maps/@id/edit/components/cell/interval-selection.ts +++ b/src/pages/maps/@id/edit/components/cell/interval-selection.ts @@ -60,9 +60,8 @@ const IntervalOption: ItemRenderer<Interval> = (interval: Interval, { handleClic } -const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2Props) => { +const IntervalSelection = ({value, onConfirm, intent, intervals, ...props} : EditableCell2Props & {intervals: Interval[]}) => { - const [intervalValues, setIntervalValues] = React.useState<Interval[]>([]); const [localValue, setLocalValue] = React.useState<string>(value); const filterInterval: ItemPredicate<Interval> = (query, interval) => { @@ -76,26 +75,12 @@ const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2P const interval = useMemo(() => { let interval = null - if(intervalValues.length != 0){ - interval = intervalValues.filter((interval) => interval.int_id == parseInt(value))[0] + if(intervals.length != 0){ + interval = intervals.filter((interval) => interval.int_id == parseInt(value))[0] } return interval - }, [value, localValue, intervalValues]) - - useEffect(() => { - - async function getIntervals() { - let response = await fetch(`https://macrostrat.org/api/defs/intervals?tilescale_id=11`) - - if (response.ok) { - let response_data = await response.json(); - setIntervalValues(response_data.success.data); - } - } - - getIntervals() - }, []) + }, [value, localValue, intervals]) return h(Cell, { ...props, @@ -103,7 +88,7 @@ const IntervalSelection = ({value, onConfirm, intent, ...props} : EditableCell2P }, [ h(Select2<Interval>, { fill: true, - items: intervalValues, + items: intervals, className: "update-input-group", popoverProps: { position: "bottom", diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index 597ddd33..be2dc0fd 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -27,7 +27,7 @@ import { submitColumnCopy } from "~/pages/maps/@id/edit/table-util"; import TableMenu from "~/pages/maps/@id/edit/table-menu"; -import IntervalSelection from "./components/cell/interval-selection"; +import IntervalSelection, {Interval} from "./components/cell/interval-selection"; import ProgressPopover from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; import "./override.sass" @@ -69,7 +69,7 @@ export default function TableInterface({ url }: EditTableProps) { const [copiedColumn, setCopiedColumn] = useState<string | undefined>(undefined) // Data State - const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "9999999"}, filter: {}}) + const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "99"}, filter: {}}) const [data, setData] = useState<any[]>([]) // Error State @@ -79,6 +79,23 @@ export default function TableInterface({ url }: EditTableProps) { const [tableUpdates, _setTableUpdates] = useState<TableUpdate[]>([]) const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) + // Cell Values + const [intervals, setIntervals] = useState<Interval[]>([]) + + useEffect(() => { + + async function getIntervals() { + let response = await fetch(`https://macrostrat.org/api/defs/intervals?tilescale_id=11`) + + if (response.ok) { + let response_data = await response.json(); + setIntervals(response_data.success.data); + } + } + + getIntervals() + }, []) + const nonIdColumnNames = useMemo(() => { return data.length ? Object.keys(data[0]).filter(x => x != "_pkid") : [] }, [data]) @@ -282,6 +299,7 @@ export default function TableInterface({ url }: EditTableProps) { "t_interval": h(Column, { ...defaultColumnConfig["t_interval"].props, cellRenderer: (rowIndex) => h(IntervalSelection, { + "intervals": intervals, onConfirm: (value) => { const tableUpdate = getTableUpdate(url, value, "t_interval", rowIndex, data, dataParameters) setTableUpdates([...tableUpdates, tableUpdate]) @@ -292,6 +310,7 @@ export default function TableInterface({ url }: EditTableProps) { "b_interval": h(Column, { ...defaultColumnConfig["b_interval"].props, cellRenderer: (rowIndex) => h(IntervalSelection, { + "intervals": intervals, onConfirm: (value) => { const tableUpdate = getTableUpdate(url, value, "b_interval", rowIndex, data, dataParameters) setTableUpdates([...tableUpdates, tableUpdate]) @@ -344,7 +363,6 @@ export default function TableInterface({ url }: EditTableProps) { } else { setSelectedColumn(undefined) } - console.log(selectedColumn) }, numRows: data.length, // Dumb hacks to try to get the table to rerender on changes From 31a9276a6db1d2f8c9239c4a26617bdc4b2865a5 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 09:12:45 -0600 Subject: [PATCH 43/52] Fix hardcoded paths --- packages/globe-dev/src/index.ts | 2 +- src/pages/map/map-interface/app-state/reducers/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/globe-dev/src/index.ts b/packages/globe-dev/src/index.ts index 98b2ea54..ad7f4e33 100644 --- a/packages/globe-dev/src/index.ts +++ b/packages/globe-dev/src/index.ts @@ -25,7 +25,7 @@ import Map from "./map-comparison"; import { getMapPositionForHash, applyMapPositionToHash, -} from "/Users/Daven/Projects/Macrostrat/Software/web/src/pages/map/map-interface/app-state/reducers/hash-string"; +} from "../../../src/pages/map/map-interface/app-state/reducers/hash-string"; function VisControl({ show, setShown, name }) { const className = show ? "active" : ""; diff --git a/src/pages/map/map-interface/app-state/reducers/index.ts b/src/pages/map/map-interface/app-state/reducers/index.ts index ccd859da..bd6d1d36 100644 --- a/src/pages/map/map-interface/app-state/reducers/index.ts +++ b/src/pages/map/map-interface/app-state/reducers/index.ts @@ -145,6 +145,7 @@ export default appReducer; export * from "./core"; export * from "./map"; export * from "./types"; +export * from "./hash-string"; /* function overallReducer(state: AppState, action: Action): AppState { From 0e0eab09c496a855fe190e4ca24eeab82bf916ca Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 09:13:30 -0600 Subject: [PATCH 44/52] Hardcoding to the newest vike version --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 28cf1454..a6622998 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "use-debounce": "^9.0.4", "use-react-router-breadcrumbs": "^3.2.1", "use-resize-observer": "^9.1.0", - "vike": "^0.4.150", + "vike": "0.4.150-commit-63b1c32", "vite": "^4.4.9", "vite-plugin-cesium": "^1.2.22" }, diff --git a/yarn.lock b/yarn.lock index b900301f..d678f88f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1865,12 +1865,12 @@ __metadata: languageName: node linkType: hard -"@brillout/vite-plugin-import-build@npm:^0.2.20": - version: 0.2.20 - resolution: "@brillout/vite-plugin-import-build@npm:0.2.20" +"@brillout/vite-plugin-import-build@npm:0.2.22-commit-7f1bb0a": + version: 0.2.22-commit-7f1bb0a + resolution: "@brillout/vite-plugin-import-build@npm:0.2.22-commit-7f1bb0a" dependencies: "@brillout/import": ^0.2.3 - checksum: 761d9c8e370f385bc28164091e583a31e9f43437451e186a5ec9c244399988846db4fa8ca7e6582c0f1b5c09461a2840f88d979e82636e030b421e6ca6d1582d + checksum: e3a31e3a9254597076acce298b24bef938d00f34ef2ee5550631f866d13a9899738571fcd4ed0119351d25acc09bb43205519d52fe141f76c90779e1cb39683c languageName: node linkType: hard @@ -3931,7 +3931,7 @@ __metadata: use-debounce: ^9.0.4 use-react-router-breadcrumbs: ^3.2.1 use-resize-observer: ^9.1.0 - vike: ^0.4.150 + vike: 0.4.150-commit-63b1c32 vite: ^4.4.9 vite-plugin-cesium: ^1.2.22 vite-plugin-rewrite-all: ^1.0.1 @@ -29894,15 +29894,15 @@ __metadata: languageName: node linkType: hard -"vike@npm:^0.4.150": - version: 0.4.150 - resolution: "vike@npm:0.4.150" +"vike@npm:0.4.150-commit-63b1c32": + version: 0.4.150-commit-63b1c32 + resolution: "vike@npm:0.4.150-commit-63b1c32" dependencies: "@brillout/import": 0.2.3 "@brillout/json-serializer": ^0.5.8 "@brillout/picocolors": ^1.0.10 "@brillout/require-shim": ^0.1.2 - "@brillout/vite-plugin-import-build": ^0.2.20 + "@brillout/vite-plugin-import-build": 0.2.22-commit-7f1bb0a acorn: ^8.8.2 cac: ^6.7.14 es-module-lexer: ^1.3.0 @@ -29918,7 +29918,7 @@ __metadata: optional: true bin: vike: node/cli/bin-entry.js - checksum: 3c925b3339286ac6bb2e89fa4f066350b407efb7f128f2e6851fc8b2ce7975e369b46dc4821092fe5c1811656aac33656c6cbda511dfe45a72067bf73a4b3ada + checksum: 6e0b8e9aec401083bd05182ca63b212ad72260086e3464e32858d5369a28d2373030f24ae0deed93d71e91f1bfe91a5d02e5235613e14203d3c1bdf37181f04a languageName: node linkType: hard From 33a37588c323e11b787b4109477cf691130cff67 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 11:35:45 -0600 Subject: [PATCH 45/52] Remove unused packages --- package.json | 3 - yarn.lock | 333 +-------------------------------------------------- 2 files changed, 5 insertions(+), 331 deletions(-) diff --git a/package.json b/package.json index a6622998..99331b47 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,6 @@ "@blueprintjs/core": "^4.14.1", "@blueprintjs/select": "4", "@blueprintjs/table": "^4", - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", "@lagunovsky/redux-react-router": "^3.2.0", "@loadable/component": "^5.14.1", "@macrostrat-web/data-sheet-test": "workspace:*", @@ -70,7 +68,6 @@ "@mapbox/point-geometry": "^0.1.0", "@mapbox/tilebelt": "^1.0.2", "@mapbox/vector-tile": "^1.3.1", - "@mui/material": "^5.14.16", "@turf/bbox": "^6.5.0", "@turf/boolean-contains": "^6.5.0", "@turf/buffer": "^6.5.0", diff --git a/yarn.lock b/yarn.lock index d678f88f..778b3255 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,7 +241,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: @@ -1622,15 +1622,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.5": - version: 7.23.6 - resolution: "@babel/runtime@npm:7.23.6" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: 1a8eaf3d3a103ef5227b60ca7ab5c589118c36ca65ef2d64e65380b32a98a3f3b5b3ef96660fa0471b079a18b619a8317f3e7f03ab2b930c45282a8b69ed9a16 - languageName: node - linkType: hard - "@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -2159,25 +2150,6 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.11.0": - version: 11.11.0 - resolution: "@emotion/babel-plugin@npm:11.11.0" - dependencies: - "@babel/helper-module-imports": ^7.16.7 - "@babel/runtime": ^7.18.3 - "@emotion/hash": ^0.9.1 - "@emotion/memoize": ^0.8.1 - "@emotion/serialize": ^1.1.2 - babel-plugin-macros: ^3.1.0 - convert-source-map: ^1.5.0 - escape-string-regexp: ^4.0.0 - find-root: ^1.1.0 - source-map: ^0.5.7 - stylis: 4.2.0 - checksum: 6b363edccc10290f7a23242c06f88e451b5feb2ab94152b18bb8883033db5934fb0e421e2d67d09907c13837c21218a3ac28c51707778a54d6cd3706c0c2f3f9 - languageName: node - linkType: hard - "@emotion/cache@npm:11.7.1": version: 11.7.1 resolution: "@emotion/cache@npm:11.7.1" @@ -2203,7 +2175,7 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.7.1": +"@emotion/cache@npm:^11.7.1": version: 11.11.0 resolution: "@emotion/cache@npm:11.11.0" dependencies: @@ -2264,15 +2236,6 @@ __metadata: languageName: node linkType: hard -"@emotion/is-prop-valid@npm:^1.2.1": - version: 1.2.1 - resolution: "@emotion/is-prop-valid@npm:1.2.1" - dependencies: - "@emotion/memoize": ^0.8.1 - checksum: 8f42dc573a3fad79b021479becb639b8fe3b60bdd1081a775d32388bca418ee53074c7602a4c845c5f75fa6831eb1cbdc4d208cc0299f57014ed3a02abcad16a - languageName: node - linkType: hard - "@emotion/memoize@npm:0.7.4": version: 0.7.4 resolution: "@emotion/memoize@npm:0.7.4" @@ -2317,27 +2280,6 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:^11.11.1": - version: 11.11.1 - resolution: "@emotion/react@npm:11.11.1" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.11.0 - "@emotion/cache": ^11.11.0 - "@emotion/serialize": ^1.1.2 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 - "@emotion/utils": ^1.2.1 - "@emotion/weak-memoize": ^0.3.1 - hoist-non-react-statics: ^3.3.1 - peerDependencies: - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: aec3c36650f5f0d3d4445ff44d73dd88712b1609645b6af3e6d08049cfbc51f1785fe13dea1a1d4ab1b0800d68f2339ab11e459687180362b1ef98863155aae5 - languageName: node - linkType: hard - "@emotion/serialize@npm:1.0.2": version: 1.0.2 resolution: "@emotion/serialize@npm:1.0.2" @@ -2364,7 +2306,7 @@ __metadata: languageName: node linkType: hard -"@emotion/serialize@npm:^1.0.2, @emotion/serialize@npm:^1.1.2": +"@emotion/serialize@npm:^1.0.2": version: 1.1.2 resolution: "@emotion/serialize@npm:1.1.2" dependencies: @@ -2391,26 +2333,6 @@ __metadata: languageName: node linkType: hard -"@emotion/styled@npm:^11.11.0": - version: 11.11.0 - resolution: "@emotion/styled@npm:11.11.0" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.11.0 - "@emotion/is-prop-valid": ^1.2.1 - "@emotion/serialize": ^1.1.2 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 - "@emotion/utils": ^1.2.1 - peerDependencies: - "@emotion/react": ^11.0.0-rc.0 - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 904f641aad3892c65d7d6c0808b036dae1e6d6dad4861c1c7dc0baa59977047c6cad220691206eba7b4059f1a1c6e6c1ef4ebb8c829089e280fa0f2164a01e6b - languageName: node - linkType: hard - "@emotion/stylis@npm:0.8.5": version: 0.8.5 resolution: "@emotion/stylis@npm:0.8.5" @@ -2432,15 +2354,6 @@ __metadata: languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": - version: 1.0.1 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" - peerDependencies: - react: ">=16.8.0" - checksum: 700b6e5bbb37a9231f203bb3af11295eed01d73b2293abece0bc2a2237015e944d7b5114d4887ad9a79776504aa51ed2a8b0ddbc117c54495dd01a6b22f93786 - languageName: node - linkType: hard - "@emotion/utils@npm:0.11.3": version: 0.11.3 resolution: "@emotion/utils@npm:0.11.3" @@ -2802,44 +2715,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.4.2": - version: 1.5.2 - resolution: "@floating-ui/core@npm:1.5.2" - dependencies: - "@floating-ui/utils": ^0.1.3 - checksum: e22de0a5e8a703fe14d9cfb72aeb67c0056c4ae6aa241539934ecb2af56448534b434a7587ecb5de154c21c3c73e44c19249b05c6b67a58eae7861188c8e69ac - languageName: node - linkType: hard - -"@floating-ui/dom@npm:^1.5.1": - version: 1.5.3 - resolution: "@floating-ui/dom@npm:1.5.3" - dependencies: - "@floating-ui/core": ^1.4.2 - "@floating-ui/utils": ^0.1.3 - checksum: 00053742064aac70957f0bd5c1542caafb3bfe9716588bfe1d409fef72a67ed5e60450d08eb492a77f78c22ed1ce4f7955873cc72bf9f9caf2b0f43ae3561c21 - languageName: node - linkType: hard - -"@floating-ui/react-dom@npm:^2.0.4": - version: 2.0.4 - resolution: "@floating-ui/react-dom@npm:2.0.4" - dependencies: - "@floating-ui/dom": ^1.5.1 - peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: 91b2369e25f84888486e48c1656117468248906034ed482d411bb9ed1061b908dd32435b4ca3d0cd0ca6083291510a98ce74d76c671d5cc25b0c41e5fa824bae - languageName: node - linkType: hard - -"@floating-ui/utils@npm:^0.1.3": - version: 0.1.6 - resolution: "@floating-ui/utils@npm:0.1.6" - checksum: b34d4b5470869727f52e312e08272edef985ba5a450a76de0917ba0a9c6f5df2bdbeb99448e2c60f39b177fb8981c772ff1831424e75123471a27ebd5b52c1eb - languageName: node - linkType: hard - "@hypnosphi/create-react-context@npm:^0.3.1": version: 0.3.1 resolution: "@hypnosphi/create-react-context@npm:0.3.1" @@ -3841,8 +3716,6 @@ __metadata: "@blueprintjs/core": ^4.14.1 "@blueprintjs/select": 4 "@blueprintjs/table": ^4 - "@emotion/react": ^11.11.1 - "@emotion/styled": ^11.11.0 "@lagunovsky/redux-react-router": ^3.2.0 "@loadable/component": ^5.14.1 "@macrostrat-web/data-sheet-test": "workspace:*" @@ -3865,7 +3738,6 @@ __metadata: "@mapbox/tilebelt": ^1.0.2 "@mapbox/vector-tile": ^1.3.1 "@mdx-js/rollup": ^2.3.0 - "@mui/material": ^5.14.16 "@turf/bbox": ^6.5.0 "@turf/boolean-contains": ^6.5.0 "@turf/buffer": ^6.5.0 @@ -4230,164 +4102,6 @@ __metadata: languageName: node linkType: hard -"@mui/base@npm:5.0.0-beta.27": - version: 5.0.0-beta.27 - resolution: "@mui/base@npm:5.0.0-beta.27" - dependencies: - "@babel/runtime": ^7.23.5 - "@floating-ui/react-dom": ^2.0.4 - "@mui/types": ^7.2.11 - "@mui/utils": ^5.15.0 - "@popperjs/core": ^2.11.8 - clsx: ^2.0.0 - prop-types: ^15.8.1 - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: c51a80c9ffe157f93ec0fbba73a7a0f985929a427a900915db993772ea33dfc69c48591fd59d0c794f6bfc8fcb923257eba5bad485c4e8b24cb008331a52ff37 - languageName: node - linkType: hard - -"@mui/core-downloads-tracker@npm:^5.15.0": - version: 5.15.0 - resolution: "@mui/core-downloads-tracker@npm:5.15.0" - checksum: a7aadd4071ff715e618b8db647137579ca63cab4bf6e3acfe86a7d461f71605fc7ce44eeea5b1e789faae8546617b1f7d14c72a0c00fa3d59951b6eee42a6c5d - languageName: node - linkType: hard - -"@mui/material@npm:^5.14.16": - version: 5.15.0 - resolution: "@mui/material@npm:5.15.0" - dependencies: - "@babel/runtime": ^7.23.5 - "@mui/base": 5.0.0-beta.27 - "@mui/core-downloads-tracker": ^5.15.0 - "@mui/system": ^5.15.0 - "@mui/types": ^7.2.11 - "@mui/utils": ^5.15.0 - "@types/react-transition-group": ^4.4.9 - clsx: ^2.0.0 - csstype: ^3.1.2 - prop-types: ^15.8.1 - react-is: ^18.2.0 - react-transition-group: ^4.4.5 - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@types/react": - optional: true - checksum: 8596780e22623844bf211838cc3998c989718c950c2bfdbb4c3ba5e4852a4e2ffd6b150381f7cd5bf8c1578ab9422d4ed94bdc0fc1dbdd1eda8e4c8a5987b98a - languageName: node - linkType: hard - -"@mui/private-theming@npm:^5.15.0": - version: 5.15.0 - resolution: "@mui/private-theming@npm:5.15.0" - dependencies: - "@babel/runtime": ^7.23.5 - "@mui/utils": ^5.15.0 - prop-types: ^15.8.1 - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 37f107e53b0e6941b06c536d9a9392dd74e96366fbc1d43d0e90f8602468ec0e2d7bba9425cd724bbb0e8fe3fc6216df740a4bdec8d882a19e98f4d647c721a7 - languageName: node - linkType: hard - -"@mui/styled-engine@npm:^5.15.0": - version: 5.15.0 - resolution: "@mui/styled-engine@npm:5.15.0" - dependencies: - "@babel/runtime": ^7.23.5 - "@emotion/cache": ^11.11.0 - csstype: ^3.1.2 - prop-types: ^15.8.1 - peerDependencies: - "@emotion/react": ^11.4.1 - "@emotion/styled": ^11.3.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - checksum: e46ca6cd914cde70dd545d3ea039f95d8ff568a1f5d0fcaceea8ef9d321b8f7968770220790d1949afdd86ea6486a2462fb76d14637f0de5f9a2c6bbbb402fd4 - languageName: node - linkType: hard - -"@mui/system@npm:^5.15.0": - version: 5.15.0 - resolution: "@mui/system@npm:5.15.0" - dependencies: - "@babel/runtime": ^7.23.5 - "@mui/private-theming": ^5.15.0 - "@mui/styled-engine": ^5.15.0 - "@mui/types": ^7.2.11 - "@mui/utils": ^5.15.0 - clsx: ^2.0.0 - csstype: ^3.1.2 - prop-types: ^15.8.1 - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@types/react": - optional: true - checksum: 57f9d199a724ac29415292ef1ab5b5583fcc4b8284a73dffec1bfa9ffbdfe75d063d7fc51e958750ce209c5f1c4b9bd50e33ecff51ef9f21be4f320fc3571147 - languageName: node - linkType: hard - -"@mui/types@npm:^7.2.11": - version: 7.2.11 - resolution: "@mui/types@npm:7.2.11" - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: ce6bbe8ba963af218bf86797f4c8adbf0f294047adeaf6596d3bb4a96f1b01701f1b1ae7dcfe2f7972f6e23c85eee4187e496fb481541713ed8bf12e96f3c34f - languageName: node - linkType: hard - -"@mui/utils@npm:^5.15.0": - version: 5.15.0 - resolution: "@mui/utils@npm:5.15.0" - dependencies: - "@babel/runtime": ^7.23.5 - "@types/prop-types": ^15.7.11 - prop-types: ^15.8.1 - react-is: ^18.2.0 - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 1d078f60f52ec83182febd3f8bd49988425730fe808697b498df7b4fafe32ff08e721ca9dcf97b11623770ba02141d458ecb5b89c8ed4ec456c9a8d21055285c - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -5422,7 +5136,7 @@ __metadata: languageName: node linkType: hard -"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.3": +"@popperjs/core@npm:^2.11.7, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 @@ -7081,13 +6795,6 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:^15.7.11": - version: 15.7.11 - resolution: "@types/prop-types@npm:15.7.11" - checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 - languageName: node - linkType: hard - "@types/q@npm:^1.5.1": version: 1.5.6 resolution: "@types/q@npm:1.5.6" @@ -7157,15 +6864,6 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:^4.4.9": - version: 4.4.10 - resolution: "@types/react-transition-group@npm:4.4.10" - dependencies: - "@types/react": "*" - checksum: fe2ea11f70251e9f79f368e198c18fd469b1d4f1e1d44e4365845b44e15974b0ec925100036f449b023b0ca3480a82725c5f0a73040e282ad32ec7b0def9b57c - languageName: node - linkType: hard - "@types/react@npm:*, @types/react@npm:>=15, @types/react@npm:^18.2.20": version: 18.2.22 resolution: "@types/react@npm:18.2.22" @@ -9079,7 +8777,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-macros@npm:^3.0.1, babel-plugin-macros@npm:^3.1.0": +"babel-plugin-macros@npm:^3.0.1": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" dependencies: @@ -10418,13 +10116,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e - languageName: node - linkType: hard - "cmd-shim@npm:^3.0.0, cmd-shim@npm:^3.0.3": version: 3.0.3 resolution: "cmd-shim@npm:3.0.3" @@ -11565,13 +11256,6 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.1.2": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 - languageName: node - linkType: hard - "currently-unhandled@npm:^0.4.1": version: 0.4.1 resolution: "currently-unhandled@npm:0.4.1" @@ -24889,13 +24573,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.2.0": - version: 18.2.0 - resolution: "react-is@npm:18.2.0" - checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e - languageName: node - linkType: hard - "react-json-tree@npm:^0.15.0": version: 0.15.2 resolution: "react-json-tree@npm:0.15.2" From 69dc55235eea0444807c8826d15cfcde4988d904 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 12:03:59 -0600 Subject: [PATCH 46/52] Add new env variable for the signing key --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index c2d4a216..01d50380 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ VITE_MACROSTRAT_TILESERVER_DOMAIN='https://dev.macrostrat.org/tiles' VITE_MACROSTRAT_API_DOMAIN='https://macrostrat.org' VITE_MACROSTRAT_INGEST_API=https://dev.macrostrat.org/api/ingest VITE_CORELLE_API_DOMAIN='https://rotate.macrostrat.org' -PUBLIC_URL='/' \ No newline at end of file +PUBLIC_URL='/' +SECRET_KEY='Replace with api signing key' \ No newline at end of file From 3bfec74790f6b5600dd060574de82d38351debf7 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 13:05:42 -0600 Subject: [PATCH 47/52] Remove unwanted vscode settings --- .vscode/settings.json | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 211abe2d..97569907 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,33 +5,5 @@ }, "typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, - "prettier.prettierPath": ".yarn/sdks/prettier/index.js", - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/__pycache__": true, - "[!s]*/**": true, - "s[!r]*/**": true, - "sr[!c]*/**": true, - "src/[!p]*/**": true, - "src/p[!a]*/**": true, - "src/pa[!g]*/**": true, - "src/pag[!e]*/**": true, - "src/page[!s]*/**": true, - "src/pages/[!m]*/**": true, - "src/pages/m[!a]*/**": true, - "src/pages/ma[!p]*/**": true, - "src/pages/map[!s]*/**": true, - "src/pages/maps/[!@]*/**": true, - "src/pages/maps/@[!i]*/**": true, - "src/pages/maps/@i[!d]*/**": true, - "src/pages/maps/@id/[!e]*/**": true, - "src/pages/maps/@id/e[!d]*/**": true, - "src/pages/maps/@id/ed[!i]*/**": true, - "src/pages/maps/@id/edi[!t]*/**": true - } + "prettier.prettierPath": ".yarn/sdks/prettier/index.js" } From d32b1f81cdf18bf6a66ebb6a8a0a48ab9792fb6c Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 13:45:54 -0600 Subject: [PATCH 48/52] Update build actions to include more informative tags --- .github/workflows/build-dev.yaml | 1 + .github/workflows/build-prod.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index d5e3c711..bd14acfd 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -25,6 +25,7 @@ jobs: tags: | type=semver,pattern={{version}} type=raw,value=latest-itb + type=ref,event=pr,suffix=-{{date 'YYYYMMDDHHmmss'}} type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}} type=ref,event=tag,suffix=-{{date 'YYYYMMDDHHmmss'}} type=raw,value=latest-itb-{{date 'YYYYMMDDHHmmss'}} diff --git a/.github/workflows/build-prod.yaml b/.github/workflows/build-prod.yaml index faf02f7d..72880a37 100644 --- a/.github/workflows/build-prod.yaml +++ b/.github/workflows/build-prod.yaml @@ -21,6 +21,8 @@ jobs: with: images: hub.opensciencegrid.org/macrostrat/web tags: | + type=ref,event=pr,suffix=-{{date 'YYYYMMDDHHmmss'}} + type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}} type=semver,pattern={{version}} type=raw,value=latest,enable={{is_default_branch}} type=raw,value=sha-{{sha}} From 6ced91a5402ce566f8282f4693d884e4c885df28 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 13:55:38 -0600 Subject: [PATCH 49/52] Add a failure state for submissions --- .../progress-popover/progress-popover.ts | 2 +- src/pages/maps/@id/edit/edit-table.ts | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts b/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts index 062ab025..30f74f76 100644 --- a/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts +++ b/src/pages/maps/@id/edit/components/progress-popover/progress-popover.ts @@ -5,7 +5,7 @@ import hyper from "@macrostrat/hyper"; import styles from "./main.module.sass"; const h = hyper.styled(styles); -interface ProgressPopoverProps extends React.HTMLProps<HTMLDivElement> { +export interface ProgressPopoverProps extends React.HTMLProps<HTMLDivElement> { text: string; value: number; progressBarProps?: ProgressBarProps; diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index be2dc0fd..f354e064 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -28,7 +28,9 @@ import { } from "~/pages/maps/@id/edit/table-util"; import TableMenu from "~/pages/maps/@id/edit/table-menu"; import IntervalSelection, {Interval} from "./components/cell/interval-selection"; -import ProgressPopover from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; +import ProgressPopover, { + ProgressPopoverProps +} from "~/pages/maps/@id/edit/components/progress-popover/progress-popover"; import "./override.sass" import "@blueprintjs/table/lib/css/table.css"; @@ -77,7 +79,7 @@ export default function TableInterface({ url }: EditTableProps) { // Table Update State const [tableUpdates, _setTableUpdates] = useState<TableUpdate[]>([]) - const [updateProgress, setUpdateProgress] = useState<number | undefined>(undefined) + const [updateProgress, setUpdateProgress] = useState<ProgressPopoverProps>(undefined) // Cell Values const [intervals, setIntervals] = useState<Interval[]>([]) @@ -203,21 +205,33 @@ export default function TableInterface({ url }: EditTableProps) { const submitTableUpdates = useCallback(async () => { - setUpdateProgress(0) + setUpdateProgress({value: 0, text: "Submitting changes"}) let index = 0 for(const tableUpdate of tableUpdates){ + setUpdateProgress({...updateProgress, text: tableUpdate?.description ?? "Submitting changes"}) + try { await tableUpdate.execute() } catch (e) { - setUpdateProgress(undefined) + setUpdateProgress({ + progressBarProps: { intent: "danger" }, + value: 1, + text: "Error submitting changes" + }) + console.error(e) + + setTimeout(() => { + setUpdateProgress(undefined) + }, 5000) + return // If there is an error, stop submitting } index += 1 - setUpdateProgress(index / tableUpdates.length) + setUpdateProgress({...updateProgress, value: index / tableUpdates.length}) } setTableUpdates([]) @@ -373,9 +387,8 @@ export default function TableInterface({ url }: EditTableProps) { h.if(updateProgress != undefined)( ProgressPopover, { - text: "Submitting Changes", - value: updateProgress, progressBarProps: { intent: "success" }, + ...updateProgress } ) ]), From 529f8bcdc504d079ce74e6cbfcdb67599b68df20 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 14:02:47 -0600 Subject: [PATCH 50/52] Build new image whenever a branch is pushed to --- .github/workflows/build-dev.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index bd14acfd..7d6521f2 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -2,6 +2,7 @@ name: Build Development on: push: + branches: [ '**' ] tags: - v[0-9]+.[0-9]+.[0-9]+-** # Semver Pre-Release pull_request: From d5d4b969e79ff1d8437d2a7588c6db70a3a349e5 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 16:09:23 -0600 Subject: [PATCH 51/52] Add infinite scrolling --- .../maps/@id/edit/components/main.module.sass | 2 ++ src/pages/maps/@id/edit/edit-table.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/maps/@id/edit/components/main.module.sass b/src/pages/maps/@id/edit/components/main.module.sass index a90171cc..812ded8d 100644 --- a/src/pages/maps/@id/edit/components/main.module.sass +++ b/src/pages/maps/@id/edit/components/main.module.sass @@ -6,6 +6,8 @@ position: relative .width-adjustable-panel-content + display: flex + flex-direction: column overflow: scroll height: 100% flex-grow: 1 diff --git a/src/pages/maps/@id/edit/edit-table.ts b/src/pages/maps/@id/edit/edit-table.ts index f354e064..c2a5a273 100644 --- a/src/pages/maps/@id/edit/edit-table.ts +++ b/src/pages/maps/@id/edit/edit-table.ts @@ -71,7 +71,7 @@ export default function TableInterface({ url }: EditTableProps) { const [copiedColumn, setCopiedColumn] = useState<string | undefined>(undefined) // Data State - const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "99"}, filter: {}}) + const [dataParameters, setDataParameters] = useState<DataParameters>({select: {page: "0", pageSize: "50"}, filter: {}}) const [data, setData] = useState<any[]>([]) // Error State @@ -338,6 +338,9 @@ export default function TableInterface({ url }: EditTableProps) { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, tabIndex: 0, + style: { + minHeight: "0" + } }, [ h("div.table-container", {}, [ h.if(error != undefined)("div.warning", {}, [error]), @@ -378,6 +381,15 @@ export default function TableInterface({ url }: EditTableProps) { setSelectedColumn(undefined) } }, + onVisibleCellsChange: (visibleCells) => { + + console.log(visibleCells) + if(visibleCells["rowIndexEnd"] > parseInt(dataParameters.select.pageSize) - 10){ + const newPageSize = (parseInt(dataParameters.select.pageSize) + 50).toString() + + setDataParameters({...dataParameters, select: {...dataParameters.select, pageSize: newPageSize}}) + } + }, numRows: data.length, // Dumb hacks to try to get the table to rerender on changes cellRendererDependencies: [data, tableUpdates], From cde4aa92c6a2ac626c417b78f5980d95fa0fa183 Mon Sep 17 00:00:00 2001 From: Cannon Lock <clock@wisc.edu> Date: Mon, 18 Dec 2023 16:13:30 -0600 Subject: [PATCH 52/52] Don't add invalid filters to the query url --- src/pages/maps/@id/edit/table-util.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/maps/@id/edit/table-util.ts b/src/pages/maps/@id/edit/table-util.ts index 6a29e553..f8967a75 100644 --- a/src/pages/maps/@id/edit/table-util.ts +++ b/src/pages/maps/@id/edit/table-util.ts @@ -86,8 +86,10 @@ export function buildURL(baseURL: string, dataParameters: DataParameters){ // Add the rest of the filters if(dataParameters?.filter != undefined){ for(const filter of Object.values(dataParameters?.filter)){ - const [columnName, filterValue] = filter.to_array() - url.searchParams.append(columnName, filterValue); + if(filter.is_valid()){ + const [columnName, filterValue] = filter.to_array() + url.searchParams.append(columnName, filterValue); + } } }