diff --git a/example/basket/src/main/scala/org/finos/vuu/core/module/basket/BasketModule.scala b/example/basket/src/main/scala/org/finos/vuu/core/module/basket/BasketModule.scala index 8630bcb8e..6431c50e8 100644 --- a/example/basket/src/main/scala/org/finos/vuu/core/module/basket/BasketModule.scala +++ b/example/basket/src/main/scala/org/finos/vuu/core/module/basket/BasketModule.scala @@ -126,7 +126,7 @@ object BasketModule extends DefaultModule { joinColumns = Columns.allFrom(tableDefs.get(NAME, BasketTradingConstituentTable)) ++ Columns.allFromExcept(tableDefs.get(PriceModule.NAME, "prices"), "ric"), joins = JoinTo( - table = tableDefs.get(PriceModule.NAME, "prices"), + table = tableDefs.get(PriceModule.NAME, PriceModule.PriceTable), joinSpec = JoinSpec(left = "ric", right = "ric", LeftOuterJoin) ), joinFields = Seq(), diff --git a/example/basket/src/main/scala/org/finos/vuu/core/module/basket/service/BasketService.scala b/example/basket/src/main/scala/org/finos/vuu/core/module/basket/service/BasketService.scala index 7be804a60..d8061eadc 100644 --- a/example/basket/src/main/scala/org/finos/vuu/core/module/basket/service/BasketService.scala +++ b/example/basket/src/main/scala/org/finos/vuu/core/module/basket/service/BasketService.scala @@ -5,23 +5,26 @@ import org.finos.toolbox.time.Clock import org.finos.vuu.core.module.basket.BasketConstants.Side import org.finos.vuu.core.module.basket.BasketModule import org.finos.vuu.core.module.basket.BasketModule.BasketConstituentTable -import org.finos.vuu.core.table.{DataTable, RowData, RowWithData, TableContainer} +import org.finos.vuu.core.module.price.PriceModule +import org.finos.vuu.core.table.{DataTable, EmptyRowData, RowData, RowWithData, TableContainer} import org.finos.vuu.net.rpc.RpcHandler import org.finos.vuu.net.RequestContext import org.finos.vuu.order.oms.OmsApi import org.finos.vuu.viewport._ + import java.util.concurrent.atomic.AtomicInteger object BasketTradeId { private val counter: AtomicInteger = new AtomicInteger(0) - def oneNew(user:String): String = { + + def oneNew(user: String): String = { val counterValue = counter.incrementAndGet() user + "-" + "".padTo(5 - counterValue.toString.length, "0").mkString + counterValue } } -trait BasketServiceIF{ +trait BasketServiceIF { def createBasket(basketId: String, name: String)(ctx: RequestContext): ViewPortAction } @@ -32,11 +35,11 @@ class BasketService(val table: DataTable, val tableContainer: TableContainer, va private def getConstituentsForSourceBasket(basketId: String): List[RowData] = { val table = tableContainer.getTable(BasketConstituentTable) val keys = table.primaryKeys.toList - keys.map( key => table.pullRow(key) ).filter(_.get(BC.BasketId).toString == basketId) + keys.map(key => table.pullRow(key)).filter(_.get(BC.BasketId).toString == basketId) } private def mkTradingConstituentRow(side: String, sourceBasketId: String, basketTradeInstanceId: String, constituentKey: String, - quantity: Long, weighting: Double, basketConsRow: RowData): RowWithData = { + quantity: Long, weighting: Double, limitPrice: Option[Double], basketConsRow: RowData): RowWithData = { RowWithData( constituentKey, Map( @@ -49,6 +52,7 @@ class BasketService(val table: DataTable, val tableContainer: TableContainer, va BTC.Side -> side, BTC.Weighting -> weighting, BTC.PriceStrategyId -> 2, + BTC.LimitPrice -> limitPrice.orNull, BTC.Algo -> -1, BTC.OrderStatus -> OrderStates.PENDING, BTC.FilledQty -> 0 @@ -70,19 +74,33 @@ class BasketService(val table: DataTable, val tableContainer: TableContainer, va logger.error("Cannot find the Basket Trading table.") } + val priceTable = tableContainer.getTable(PriceModule.PriceTable) tableContainer.getTable(BasketModule.BasketTradingConstituentTable) match { case table: DataTable => - constituents.foreach( rowData => { - val constituentKey = basketTradeId + "." + rowData.get(BTC.Ric) + constituents.foreach(rowData => { + val ric = rowData.get(BTC.Ric).toString + val constituentKey = s"$basketTradeId.$ric" val weighting = rowData.get(BTC.Weighting).asInstanceOf[Double] val quantity = (weighting * 100).asInstanceOf[Long] val side = rowData.get(BTC.Side).toString - table.processUpdate(constituentKey, mkTradingConstituentRow(side, sourceBasketId, basketTradeId, constituentKey, quantity, weighting, rowData), clock.now()) + val limitPrice = getLastPrice(priceTable, ric) + table.processUpdate(constituentKey, mkTradingConstituentRow(side, sourceBasketId, basketTradeId, constituentKey, quantity, weighting, limitPrice, rowData), clock.now()) }) case null => logger.error("Cannot find the Basket Trading Constituent.") } ViewPortCreateSuccess(basketTradeId) + } + + private def getLastPrice(priceTable: DataTable, ric: String): Option[Double] = { + priceTable.pullRow(ric) match { + case row: RowWithData => + row.get("last") match { + case null => None + case price: Double => Some(price) + } + case EmptyRowData => None } -} + } +} \ No newline at end of file diff --git a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketCreateTest.scala b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketCreateTest.scala index 635db6e65..a190bb1a4 100644 --- a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketCreateTest.scala +++ b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketCreateTest.scala @@ -5,6 +5,7 @@ import org.finos.toolbox.lifecycle.LifecycleContainer import org.finos.toolbox.time.{Clock, TestFriendlyClock} import org.finos.vuu.api.ViewPortDef import org.finos.vuu.core.module.TableDefContainer +import org.finos.vuu.core.module.basket.BasketModule.{BasketColumnNames => B, BasketConstituentColumnNames => BC} import org.finos.vuu.core.module.basket.service.{BasketServiceIF, BasketTradeId, BasketTradingServiceIF} import org.finos.vuu.core.module.price.PriceModule import org.finos.vuu.order.oms.OmsApi @@ -32,21 +33,36 @@ class BasketCreateTest extends VuuServerTestCase { vuuServer => vuuServer.login("testUser", "testToken") + val basketId = ".FTSE" vuuServer.overrideViewPortDef("prices", (table, _, _, _) => ViewPortDef(table.getTableDef.columns, null)) - val pricesProvider = vuuServer.getProvider(PriceModule.NAME, "prices") + val pricesProvider = vuuServer.getProvider(PriceModule.NAME, PriceModule.PriceTable) + val basketProvider = vuuServer.getProvider(BasketModule.NAME, BasketModule.BasketTable) + val constituentProvider = vuuServer.getProvider(BasketModule.NAME, BasketModule.BasketConstituentTable) - pricesProvider.tick("VOD.L", Map("ric" -> "VOD.L", "phase" -> "C")) + basketProvider.tick(basketId, Map(B.Id -> basketId, B.Name -> ".FTSE 100", B.NotionalValue -> 1000001, B.NotionalValueUsd -> 1500001)) - val viewport = vuuServer.createViewPort(PriceModule.NAME, "prices") + //given constituent with price + constituentProvider.tick(s"VOD.L.$basketId", Map(BC.RicBasketId -> s"VOD.L.$basketId", BC.Ric -> "VOD.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Vodafone")) + pricesProvider.tick("VOD.L", Map("ric" -> "VOD.L", "bid" -> 1.3, "ask" -> 1.6, "last" -> 1.5, "phase" -> "C")) + + //given constituent with no price + constituentProvider.tick(s"BT.L.$basketId", Map(BC.RicBasketId -> s"BT.L.$basketId", BC.Ric -> "BT.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "SELL", BC.Description -> "British Telecom")) + + //given constituent with price but missing last price + constituentProvider.tick(s"BP.L.$basketId", Map(BC.RicBasketId -> s"BP.L.$basketId", BC.Ric -> "BP.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Beyond Petroleum")) + pricesProvider.tick("BP.L", Map("ric" -> "BP.L", "bid" -> 5.3, "phase" -> "C")) + + val viewportPrices = vuuServer.createViewPort(PriceModule.NAME, "prices") vuuServer.runOnce() - assertVpEq(combineQsForVp(viewport)) { + assertVpEq(combineQsForVp(viewportPrices)) { Table( ("ric", "bid", "ask", "bidSize", "askSize", "last", "open", "close", "phase", "scenario"), - ("VOD.L", null, null, null, null, null, null, null, "C", null) + ("BP.L", 5.3, null, null, null, null, null, null, "C", null), + ("VOD.L", 1.3, 1.6, null, null, 1.5, null, null, "C", null), ) } @@ -54,12 +70,13 @@ class BasketCreateTest extends VuuServerTestCase { val basketService = vuuServer.getViewPortRpcServiceProxy[BasketServiceIF](viewportBasket) - val vpAction = basketService.createBasket(".FTSE", "TestBasket")(vuuServer.requestContext) - + val vpAction = basketService.createBasket(basketId, "TestBasket")(vuuServer.requestContext) + vuuServer.runOnce() assert(vpAction.isInstanceOf[ViewPortCreateSuccess]) val basketTradeInstanceId = vpAction.asInstanceOf[ViewPortCreateSuccess].key val viewportBasketTrading = vuuServer.createViewPort(BasketModule.NAME, BasketTradingTable) + val viewportBasketTradingCons = vuuServer.createViewPort(BasketModule.NAME, BasketTradingConstituentTable) val basketTradingService = vuuServer.getViewPortRpcServiceProxy[BasketTradingServiceIF](viewportBasketTrading) @@ -67,11 +84,22 @@ class BasketCreateTest extends VuuServerTestCase { basketTradingService.editCellAction().func(basketTradeInstanceId, BT.Units, 100.asInstanceOf[Object], viewportBasketTrading, vuuServer.session) vuuServer.runOnce() + Then("get all the updates that have occurred for all view ports from the outbound queue") + val updates = combineQs(viewportBasketTrading) + + assertVpEq(filterByVp(viewportBasketTrading, updates)) { + Table( + ("instanceId", "basketId", "basketName", "units", "status", "filledPct", "totalNotionalUsd", "totalNotional", "fxRateToUsd", "side"), + (basketTradeInstanceId, ".FTSE", "TestBasket", 100, "OFF-MARKET", null, null, null, null, "BUY") + ) + } - assertVpEq(combineQsForVp(viewportBasketTrading)) { + assertVpEq(filterByVp(viewportBasketTradingCons, updates)) { Table( - ("basketId", "instanceId", "basketName", "units", "status", "filledPct", "totalNotionalUsd", "totalNotional", "fxRateToUsd", "side"), - (".FTSE", basketTradeInstanceId, "TestBasket", 100, "OFF-MARKET", null, null, null, null, "BUY") + ("quantity", "side", "instanceId", "instanceIdRic", "basketId", "ric", "description", "notionalUsd", "notionalLocal", "venue", "algo", "algoParams", "pctFilled", "weighting", "priceSpread", "limitPrice", "priceStrategyId", "filledQty", "orderStatus"), + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), + (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, 1.5, 2, 0, "PENDING"), ) } } diff --git a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketMutateOffMarketTest.scala b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketMutateOffMarketTest.scala index c54e31162..77fe31da4 100644 --- a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketMutateOffMarketTest.scala +++ b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketMutateOffMarketTest.scala @@ -93,9 +93,9 @@ class BasketMutateOffMarketTest extends VuuServerTestCase { assertVpEq(combineQsForVp(vpBasketTradingCons)) { Table( ("quantity", "side", "instanceId", "instanceIdRic", "basketId", "ric", "description", "notionalUsd", "notionalLocal", "venue", "algo", "algoParams", "pctFilled", "weighting", "priceSpread", "limitPrice", "priceStrategyId", "filledQty", "orderStatus"), - (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING") + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, 301.5, 2, 0, "PENDING"), + (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, 201.5, 2, 0, "PENDING"), + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, 101.5, 2, 0, "PENDING") ) } @@ -120,9 +120,9 @@ class BasketMutateOffMarketTest extends VuuServerTestCase { assertVpEq(filterByVp(vpBasketTradingCons, updates)) { Table( ("quantity", "side", "instanceId", "instanceIdRic", "basketId", "ric", "description", "notionalUsd", "notionalLocal", "venue", "algo", "algoParams", "pctFilled", "weighting", "priceSpread", "limitPrice", "priceStrategyId", "filledQty", "orderStatus"), - (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING") + (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, 301.5, 2, 0, "PENDING"), + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, 201.5, 2, 0, "PENDING"), + (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, 101.5, 2, 0, "PENDING") ) } @@ -133,9 +133,9 @@ class BasketMutateOffMarketTest extends VuuServerTestCase { assertVpEq(filterByVp(vpBasketTradingCons, combineQs(vpBasketTrading))) { Table( ("quantity", "side", "instanceId", "instanceIdRic", "basketId", "ric", "description", "notionalUsd", "notionalLocal", "venue", "algo", "algoParams", "pctFilled", "weighting", "priceSpread", "limitPrice", "priceStrategyId", "filledQty", "orderStatus"), - (100L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (100L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (100L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING") + (100L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, 301.5, 2, 0, "PENDING"), + (100L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, 201.5, 2, 0, "PENDING"), + (100L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, 101.5, 2, 0, "PENDING") ) } } @@ -235,12 +235,12 @@ class BasketMutateOffMarketTest extends VuuServerTestCase { def GivenBasketTradeExist(vuuServer: TestVuuServer, basketId: String, basketTradeName: String): String = { val basketProvider = vuuServer.getProvider(BasketModule.NAME, BasketModule.BasketTable) - basketProvider.tick(".FTSE", Map(B.Id -> ".FTSE", B.Name -> ".FTSE 100", B.NotionalValue -> 1000001, B.NotionalValueUsd -> 1500001)) + basketProvider.tick(basketId, Map(B.Id -> basketId, B.Name -> ".FTSE 100", B.NotionalValue -> 1000001, B.NotionalValueUsd -> 1500001)) val constituentProvider = vuuServer.getProvider(BasketModule.NAME, BasketModule.BasketConstituentTable) - constituentProvider.tick("VOD.L.FTSE", Map(BC.RicBasketId -> "VOD.L.FTSE", BC.Ric -> "VOD.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Vodafone")) - constituentProvider.tick("BT.L.FTSE", Map(BC.RicBasketId -> "BT.L.FTSE", BC.Ric -> "BT.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "SELL", BC.Description -> "British Telecom")) - constituentProvider.tick("BP.L.FTSE", Map(BC.RicBasketId -> "BP.L.FTSE", BC.Ric -> "BP.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Beyond Petroleum")) + constituentProvider.tick(s"VOD.L.$basketId", Map(BC.RicBasketId -> s"VOD.L.$basketId", BC.Ric -> "VOD.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Vodafone")) + constituentProvider.tick(s"BT.L.$basketId", Map(BC.RicBasketId -> s"BT.L.$basketId", BC.Ric -> "BT.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "SELL", BC.Description -> "British Telecom")) + constituentProvider.tick(s"BP.L.$basketId", Map(BC.RicBasketId -> s"BP.L.$basketId", BC.Ric -> "BP.L", BC.BasketId -> basketId, BC.Weighting -> 0.1, BC.Side -> "BUY", BC.Description -> "Beyond Petroleum")) val vpBasket = vuuServer.createViewPort(BasketModule.NAME, BasketModule.BasketTable) val basketService = vuuServer.getViewPortRpcServiceProxy[BasketServiceIF](vpBasket) diff --git a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketSendToMarketTest.scala b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketSendToMarketTest.scala index 15b40173c..282a49481 100644 --- a/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketSendToMarketTest.scala +++ b/example/basket/src/test/scala/org/finos/vuu/core/module/basket/BasketSendToMarketTest.scala @@ -64,9 +64,9 @@ class BasketSendToMarketTest extends VuuServerTestCase { assertVpEq(combineQsForVp(vpBasketTradingCons)) { Table( ("quantity", "side", "instanceId", "instanceIdRic", "basketId", "ric", "description", "notionalUsd", "notionalLocal", "venue", "algo", "algoParams", "pctFilled", "weighting", "priceSpread", "limitPrice", "priceStrategyId", "filledQty", "orderStatus"), - (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING"), - (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, null, 2, 0, "PENDING") + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.BP.L", ".FTSE", "BP.L", "Beyond Petroleum", null, null, null, -1, null, null, 0.1, null, 301.5, 2, 0, "PENDING"), + (10L, "SELL", basketTradeInstanceId, s"$basketTradeInstanceId.BT.L", ".FTSE", "BT.L", "British Telecom", null, null, null, -1, null, null, 0.1, null, 201.5, 2, 0, "PENDING"), + (10L, "BUY", basketTradeInstanceId, s"$basketTradeInstanceId.VOD.L", ".FTSE", "VOD.L", "Vodafone", null, null, null, -1, null, null, 0.1, null, 101.5, 2, 0, "PENDING") ) } diff --git a/example/main/src/main/resources/static/ftse100.csv b/example/main/src/main/resources/static/ftse100.csv index aa0315f95..f56009d01 100644 --- a/example/main/src/main/resources/static/ftse100.csv +++ b/example/main/src/main/resources/static/ftse100.csv @@ -1,100 +1,99 @@ Symbol,Name,Last Trade,Change,Volume, Weighting -AAL.L,Anglo American PLC,436.35�13:13,�5.35�(1.24%),5799089,0.0278736825813547 -ABF.L,Associated British Foods PLC,"3,435.60�13:12",�7.40�(0.21%),86808,0.000417248060431947 -ADM.L,Admiral Group PLC,"1,627.00�13:13",,86808,0.000417248060431947 -ADN.L,Aberdeen Asset Management PLC,334.00�13:13,�2.50�(0.75%),806880,0.00387831899135251 -AHT.L,Ashtead Group PLC,"1,027.00�13:13",�6.00�(0.59%),331255,0.00159219779580666 -ANTO.L,Antofagasta PLC,484.10�13:13,�11.70�(2.48%),1753976,0.00843059492263598 -ARM.L,ARM Holdings PLC,"1,058.00�13:13",�3.00�(0.28%),475927,0.00228757277736148 -AV.L,Aviva PLC,493.97�13:13,�2.23�(0.45%),2226835,0.0107034211668507 -AZN.L,AstraZeneca PLC,"4,399.50�13:13",�2.50�(0.06%),815133,0.00391798755004232 -BA.L,BAE Systems PLC,478.10�13:13,�4.30�(0.91%),2039934,0.00980506986578636 -BAB.L,Babcock International Group PLC,988.00�13:13,�9.50�(0.97%),209614,0.00100752275066102 -BARC.L,Barclays PLC,226.30�13:13,�1.15�(0.51%),6575664,0.0316063387021032 -BATS.L,British American Tobacco PLC,"3,803.50�13:13",�8.50�(0.22%),465110,0.0022355801929258 -BDEV.L,Barratt Developments PLC,576.00�13:13,�0.50�(0.09%),1044365,0.00501980543997108 -BG.L,BG Group PLC,"1,013.50�13:13",�5.50�(0.55%),1507332,0.00724508516988073 -BKG.L,Berkeley Group Holdings (The) PLC,"3,126.00�13:13",�15.00�(0.48%),95071,0.000456964684744788 -BLND.L,British Land Co PLC,828.06�13:12,�10.44�(1.25%),1802548,0.00866405926683583 -BLT.L,BHP Billiton PLC,881.40�13:13,�4.30�(0.49%),4947287,0.0237794431982097 -BNZL.L,Bunzl PLC,"1,875.40�13:05",�4.60�(0.24%),104541,0.000502482829757812 -BP.L,BP PLC,381.50�13:13,�2.95�(0.78%),10493561,0.0504379547308349 -BRBY.L,Burberry Group PLC,"1,269.00�13:13",�7.00�(0.55%),295647,0.00142104572530785 -BT-A.L,BT Group PLC,489.20�13:13,�3.70�(0.75%),3914982,0.0188176048996174 -CCL.L,Carnival PLC,"3,426.00�13:12",�22.00�(0.64%),86257,0.000414599644602783 -CNA.L,Centrica PLC,212.80�13:13,�0.60�(0.28%),2144540,0.0103078651220939 -CPG.L,Compass Group PLC,"1,054.00�13:08",�5.00�(0.48%),1001167,0.00481217156158961 -CPI.L,Capita PLC,"1,235.00�13:11",�1.00�(0.08%),244591,0.0011756418803464 -CRH.L,CRH PLC,"1,783.20�13:12",�17.80�(0.99%),897325,0.00431304851888186 -DC.L,DIXONS CARPHONE,462.10�13:11,,756906,0.00363811584680332 -DGE.L,Diageo PLC,"1,881.50�13:13",�6.50�(0.34%),756906,0.00363811584680332 -DLG.L,Direct Line Insurance Group PLC,403.80�13:13,�0.40�(0.10%),1095340,0.00526481995338596 -EXPN.L,Experian PLC,"1,191.00�13:12",�2.00�(0.17%),467283,0.00224602485281105 -EZJ.L,easyJet PLC,"1,682.00�13:12",�28.00�(1.64%),1191230,0.00572572121265722 -FRES.L,Fresnillo PLC,678.50�13:12,�6.50�(0.97%),381871,0.00183548675335462 -GFS.L,G4S PLC,232.30�13:03,�2.00�(0.85%),1096551,0.00527064070033535 -GKN.L,GKN PLC,294.80�13:12,�2.50�(0.86%),792247,0.00380798456516713 -GLEN.L,Glencore PLC,90.48�13:13,�1.65�(1.86%),41631528,0.200104533116974 -GSK.L,GlaxoSmithKline PLC,"1,345.00�13:13",�0.50�(0.04%),1767356,0.00849490672625522 -HIK.L,Hikma Pharmaceuticals PLC,"2,010.00�13:04",�57.00�(2.92%),261511,0.00125696891451962 -HL.L,Hargreaves Lansdown PLC,"1,488.03�13:12",�9.97�(0.67%),372261,0.00178929568961912 -HMSO.L,Hammerson PLC,597.50�13:11,�3.50�(0.58%),478301,0.0022989835562697 -HSBA.L,HSBC Holdings PLC,519.70�13:13,�0.50�(0.10%),7415629,0.0356436828072631 -IAG.L,International Consolidated Airlines Group SA,575.40�13:12,�16.10�(2.72%),4311514,0.0207235606629018 -IHG.L,InterContinental Hotels Group PLC,"2,481.00�13:12",�19.00�(0.76%),219918,0.00105704956863507 -III.L,3i Group PLC,487.30�13:11,�4.50�(0.92%),189987,0.000913184352332553 -IMT.L,Imperial Tobacco Group PLC,"3,571.00�13:13",�29.00�(0.81%),926816,0.00445479884777089 -INTU.L,intu properties plc,319.90�13:09,�4.60�(1.42%),514821,0.0024745192115892 -ISAT.L,Inmarsat PLC,"1,054.44�13:13",�3.44�(0.33%),988089,0.00474931133978598 -ITRK.L,Intertek Group PLC,"2,643.00�13:14",�3.00�(0.11%),45868,0.000220467399731505 -ITV.L,ITV PLC,267.30�13:14,�2.60�(0.96%),3453208,0.0165980593985356 -JMAT.L,Johnson Matthey PLC,"2,445.00�13:14",�29.00�(1.20%),276397,0.00132851940096775 -KGF.L,Kingfisher PLC,346.20�13:14,�4.30�(1.23%),1021408,0.00490946118917235 -LAND.L,Land Securities Group PLC,"1,239.00�13:13",�7.00�(0.56%),384973,0.00185039670961971 -LGEN.L,Legal & General Group PLC,266.00�13:14,�1.60�(0.60%),1998399,0.00960542930051541 -LLOY.L,Lloyds Banking Group PLC,73.86�13:14,�0.02�(0.03%),18907878,0.0908818936317375 -LSE.L,London Stock Exchange Group PLC,"2,544.00�13:11",�6.00�(0.24%),129657,0.000623204448569543 -MGGT.L,Meggitt PLC,386.00�13:15,�3.20�(0.84%),611044,0.00293702105610748 -MKS.L,Marks & Spencer Group PLC,514.75�13:12,�3.25�(0.63%),920128,0.00442265255908587 -MNDI.L,Mondi PLC,"1,463.00�13:14",�7.00�(0.48%),383546,0.00184353774521278 -MRW.L,Morrison (Wm) Supermarkets PLC,155.20�13:14,,920128,0.00442265255908587 -NG.L,National Grid PLC,926.40�13:14,�1.10�(0.12%),1659592,0.00797693234619361 -NXT.L,Next PLC,"7,765.00�13:11",�95.00�(1.21%),114062,0.000548246109448308 -OML.L,Old Mutual PLC,198.50�13:14,�0.40�(0.20%),2040849,0.00980946787029396 -PRU.L,Prudential PLC,"1,499.50�13:15",�14.00�(0.93%),580870,0.00279198784516525 -PSON.L,Pearson PLC,794.00�13:09,�5.00�(0.63%),1177953,0.00566190448495522 -RB.L,Reckitt Benckiser Group PLC,"6,293.00�13:14",�34.00�(0.54%),281172,0.0013514707359664 -RBS.L,Royal Bank of Scotland Group PLC,313.40�13:14,�2.40�(0.77%),2100058,0.0100940596177149 -RDSA.L,Royal Dutch Shell PLC,"1,636.00�13:14",�18.00�(1.11%),2467461,0.0118600050276642 -RDSB.L,Royal Dutch Shell PLC,"1,652.00�13:15",�14.50�(0.89%),1457434,0.0070052473240666 -REL.L,Reed Elsevier PLC,"1,170.00�13:14",0.00�(0.00%),908802,0.00436821343443777 -RIO.L,Rio Tinto PLC,"2,235.00�13:15",�21.00�(0.95%),2190722,0.0105298417823887 -RMG.L,Royal Mail PLC,453.50�13:14,�1.20�(0.26%),995316,0.00478404836555252 -RR.L,Rolls-Royce Group PLC,546.63�13:14,�8.38�(1.51%),2792915,0.0134243199555489 -RRS.L,Randgold Resources Ltd,"3,929.00�13:14",0.00�(0.00%),135524,0.000651404549603483 -RSA.L,RSA Insurance Group PLC,437.10�13:14,�0.10�(0.02%),395477,0.00190088484005443 -SAB.L,SABMiller PLC,"4,011.00�13:15",�1.00�(0.02%),892451,0.00428962133421518 -SBRY.L,Sainsbury (J) PLC,255.80�13:14,�7.40�(2.98%),2395670,0.0115149371133421 -SDR.L,Schroders PLC,"2,930.00�13:09",�12.00�(0.41%),44674,0.000214728364341268 -SGE.L,Sage Group (The) PLC,545.50�13:13,�0.50�(0.09%),539717,0.00259418338669419 -SHP.L,Shire PLC,"4,685.00�13:14",�22.00�(0.47%),221318,0.0010637787558598 -SKY.L,SKY,"1,095.00�13:12",�4.00�(0.37%),925016,0.0044461470356248 -SL.L,Standard Life PLC,399.90�13:14,�3.20�(0.79%),861636,0.00414150711683647 -SMIN.L,Smiths Group PLC,992.50�13:14,�27.50�(2.70%),640309,0.00307768510191594 -SN.L,Smith & Nephew PLC,"1,110.00�13:14",�9.00�(0.82%),480018,0.00230723642374461 -SPD.L,Sports Direct International PLC,694.50�13:11,�1.50�(0.22%),157981,0.000759345519250522 -SSE.L,SSE PLC,"1,463.00�13:13",�2.00�(0.14%),562454,0.00270347019378617 -STAN.L,Standard Chartered PLC,583.00�13:14,�0.60�(0.10%),2018697,0.00970299290214945 -STJ.L,St James's Place PLC,964.00�13:14,�11.00�(1.13%),418480,0.00201145019271912 -SVT.L,Severn Trent PLC,"2,199.00�13:12",�1.00�(0.05%),95342,0.000458267263129005 -TPK.L,Travis Perkins PLC,"1,945.00�13:13",�4.00�(0.21%),92916,0.000446606542981001 -TSCO.L,Tesco PLC,171.54�13:14,�2.54�(1.50%),9831136,0.0472539676970174 -TUI.L,TUI AG,"1,115.00�13:10",�5.00�(0.45%),458970,0.00220606790038304 -TW.L,Taylor Wimpey PLC,183.90�13:15,�1.10�(0.59%),3180729,0.0152883721086725 -ULVR.L,Unilever PLC,"2,791.00�13:14",�29.00�(1.03%),824827,0.0039645823650113 -UU.L,United Utilities Group PLC,959.00�13:10,�2.50�(0.26%),436911,0.00210003994253274 -VOD.L,Vodafone Group PLC,224.25�13:15,�1.30�(0.58%),17572036,0.0844610858312637 -WOS.L,Wolseley PLC,"3,657.00�13:14",�4.00�(0.11%),179536,0.000862950969699912 -WPP.L,WPP PLC,"1,502.00�13:15",�12.00�(0.79%),857887,0.0041234873147611 -WTB.L,Whitbread PLC,"4,484.00�13:16",�60.00�(1.32%),141036,0.000677898321019722 -,,,,208048900, +AAL.L,Anglo American PLC,436.35 13:13, 5.35 (1.24%),5799089,0.0278736825813547 +ABF.L,Associated British Foods PLC,3435.60 13:12, 7.40 (0.21%),86808,0.000417248060431947 +ADM.L,Admiral Group PLC,1627.00 13:13,,86808,0.000417248060431947 +ADN.L,Aberdeen Asset Management PLC,334.00 13:13, 2.50 (0.75%),806880,0.00387831899135251 +AHT.L,Ashtead Group PLC,1027.00 13:13, 6.00 (0.59%),331255,0.00159219779580666 +ANTO.L,Antofagasta PLC,484.10 13:13, 11.70 (2.48%),1753976,0.00843059492263598 +ARM.L,ARM Holdings PLC,1058.00 13:13, 3.00 (0.28%),475927,0.00228757277736148 +AV.L,Aviva PLC,493.97 13:13, 2.23 (0.45%),2226835,0.0107034211668507 +AZN.L,AstraZeneca PLC, 4399.50 13:13, 2.50 (0.06%),815133,0.00391798755004232 +BA.L,BAE Systems PLC,478.10 13:13, 4.30 (0.91%),2039934,0.00980506986578636 +BAB.L,Babcock International Group PLC,988.00 13:13, 9.50 (0.97%),209614,0.00100752275066102 +BARC.L,Barclays PLC,226.30 13:13, 1.15 (0.51%),6575664,0.0316063387021032 +BATS.L,British American Tobacco PLC,3803.50 13:13, 8.50 (0.22%),465110,0.0022355801929258 +BDEV.L,Barratt Developments PLC,576.00 13:13, 0.50 (0.09%),1044365,0.00501980543997108 +BG.L,BG Group PLC,1013.50 13:13, 5.50 (0.55%),1507332,0.00724508516988073 +BKG.L,Berkeley Group Holdings (The) PLC,3126.00 13:13, 15.00 (0.48%),95071,0.000456964684744788 +BLND.L,British Land Co PLC,828.06 13:12, 10.44 (1.25%),1802548,0.00866405926683583 +BLT.L,BHP Billiton PLC,881.40 13:13, 4.30 (0.49%),4947287,0.0237794431982097 +BNZL.L,Bunzl PLC,1875.40 13:05, 4.60 (0.24%),104541,0.000502482829757812 +BP.L,BP PLC,381.50 13:13, 2.95 (0.78%),10493561,0.0504379547308349 +BRBY.L,Burberry Group PLC,1269.00 13:13, 7.00 (0.55%),295647,0.00142104572530785 +BT-A.L,BT Group PLC,489.20 13:13, 3.70 (0.75%),3914982,0.0188176048996174 +CCL.L,Carnival PLC,3426.00 13:12, 22.00 (0.64%),86257,0.000414599644602783 +CNA.L,Centrica PLC,212.80 13:13, 0.60 (0.28%),2144540,0.0103078651220939 +CPG.L,Compass Group PLC,1054.00 13:08, 5.00 (0.48%),1001167,0.00481217156158961 +CPI.L,Capita PLC,1235.00 13:11, 1.00 (0.08%),244591,0.0011756418803464 +CRH.L,CRH PLC,1783.20 13:12, 17.80 (0.99%),897325,0.00431304851888186 +DC.L,DIXONS CARPHONE,462.10 13:11,,756906,0.00363811584680332 +DGE.L,Diageo PLC,1881.50 13:13, 6.50 (0.34%),756906,0.00363811584680332 +DLG.L,Direct Line Insurance Group PLC,403.80 13:13, 0.40 (0.10%),1095340,0.00526481995338596 +EXPN.L,Experian PLC,1191.00 13:12, 2.00 (0.17%),467283,0.00224602485281105 +EZJ.L,easyJet PLC,1682.00 13:12, 28.00 (1.64%),1191230,0.00572572121265722 +FRES.L,Fresnillo PLC,678.50 13:12, 6.50 (0.97%),381871,0.00183548675335462 +GFS.L,G4S PLC,232.30 13:03, 2.00 (0.85%),1096551,0.00527064070033535 +GKN.L,GKN PLC,294.80 13:12, 2.50 (0.86%),792247,0.00380798456516713 +GLEN.L,Glencore PLC,90.48 13:13, 1.65 (1.86%),41631528,0.200104533116974 +GSK.L,GlaxoSmithKline PLC,1345.00 13:13, 0.50 (0.04%),1767356,0.00849490672625522 +HIK.L,Hikma Pharmaceuticals PLC,2010.00 13:04, 57.00 (2.92%),261511,0.00125696891451962 +HL.L,Hargreaves Lansdown PLC,1488.03 13:12, 9.97 (0.67%),372261,0.00178929568961912 +HMSO.L,Hammerson PLC,597.50 13:11, 3.50 (0.58%),478301,0.0022989835562697 +HSBA.L,HSBC Holdings PLC,519.70 13:13, 0.50 (0.10%),7415629,0.0356436828072631 +IAG.L,International Consolidated Airlines Group SA,575.40 13:12, 16.10 (2.72%),4311514,0.0207235606629018 +IHG.L,InterContinental Hotels Group PLC,2481.00 13:12, 19.00 (0.76%),219918,0.00105704956863507 +III.L,3i Group PLC,487.30 13:11, 4.50 (0.92%),189987,0.000913184352332553 +IMT.L,Imperial Tobacco Group PLC,3571.00 13:13, 29.00 (0.81%),926816,0.00445479884777089 +INTU.L,intu properties plc,319.90 13:09, 4.60 (1.42%),514821,0.0024745192115892 +ISAT.L,Inmarsat PLC,1054.44 13:13, 3.44 (0.33%),988089,0.00474931133978598 +ITRK.L,Intertek Group PLC,2643.00 13:14, 3.00 (0.11%),45868,0.000220467399731505 +ITV.L,ITV PLC,267.30 13:14, 2.60 (0.96%),3453208,0.0165980593985356 +JMAT.L,Johnson Matthey PLC,2445.00 13:14, 29.00 (1.20%),276397,0.00132851940096775 +KGF.L,Kingfisher PLC,346.20 13:14, 4.30 (1.23%),1021408,0.00490946118917235 +LAND.L,Land Securities Group PLC,1239.00 13:13, 7.00 (0.56%),384973,0.00185039670961971 +LGEN.L,Legal & General Group PLC,266.00 13:14, 1.60 (0.60%),1998399,0.00960542930051541 +LLOY.L,Lloyds Banking Group PLC,73.86 13:14, 0.02 (0.03%),18907878,0.0908818936317375 +LSE.L,London Stock Exchange Group PLC,2544.00 13:11, 6.00 (0.24%),129657,0.000623204448569543 +MGGT.L,Meggitt PLC,386.00 13:15, 3.20 (0.84%),611044,0.00293702105610748 +MKS.L,Marks & Spencer Group PLC,514.75 13:12, 3.25 (0.63%),920128,0.00442265255908587 +MNDI.L,Mondi PLC,1463.00 13:14, 7.00 (0.48%),383546,0.00184353774521278 +MRW.L,Morrison (Wm) Supermarkets PLC,155.20 13:14,,920128,0.00442265255908587 +NG.L,National Grid PLC,926.40 13:14, 1.10 (0.12%),1659592,0.00797693234619361 +NXT.L,Next PLC,7765.00 13:11, 95.00 (1.21%),114062,0.000548246109448308 +OML.L,Old Mutual PLC,198.50 13:14, 0.40 (0.20%),2040849,0.00980946787029396 +PRU.L,Prudential PLC,1499.50 13:15, 14.00 (0.93%),580870,0.00279198784516525 +PSON.L,Pearson PLC,794.00 13:09, 5.00 (0.63%),1177953,0.00566190448495522 +RB.L,Reckitt Benckiser Group PLC,6293.00 13:14, 34.00 (0.54%),281172,0.0013514707359664 +RBS.L,Royal Bank of Scotland Group PLC,313.40 13:14, 2.40 (0.77%),2100058,0.0100940596177149 +RDSA.L,Royal Dutch Shell PLC,1636.00 13:14, 18.00 (1.11%),2467461,0.0118600050276642 +RDSB.L,Royal Dutch Shell PLC,1652.00 13:15, 14.50 (0.89%),1457434,0.0070052473240666 +REL.L,Reed Elsevier PLC,1170.00 13:14,0.00 (0.00%),908802,0.00436821343443777 +RIO.L,Rio Tinto PLC,2235.00 13:15, 21.00 (0.95%),2190722,0.0105298417823887 +RMG.L,Royal Mail PLC,453.50 13:14, 1.20 (0.26%),995316,0.00478404836555252 +RR.L,Rolls-Royce Group PLC,546.63 13:14, 8.38 (1.51%),2792915,0.0134243199555489 +RRS.L,Randgold Resources Ltd,3929.00 13:14,0.00 (0.00%),135524,0.000651404549603483 +RSA.L,RSA Insurance Group PLC,437.10 13:14, 0.10 (0.02%),395477,0.00190088484005443 +SAB.L,SABMiller PLC,4011.00 13:15, 1.00 (0.02%),892451,0.00428962133421518 +SBRY.L,Sainsbury (J) PLC,255.80 13:14, 7.40 (2.98%),2395670,0.0115149371133421 +SDR.L,Schroders PLC,2930.00 13:09, 12.00 (0.41%),44674,0.000214728364341268 +SGE.L,Sage Group (The) PLC,545.50 13:13, 0.50 (0.09%),539717,0.00259418338669419 +SHP.L,Shire PLC,4685.00 13:14, 22.00 (0.47%),221318,0.0010637787558598 +SKY.L,SKY,1095.00 13:12, 4.00 (0.37%),925016,0.0044461470356248 +SL.L,Standard Life PLC,399.90 13:14, 3.20 (0.79%),861636,0.00414150711683647 +SMIN.L,Smiths Group PLC,992.50 13:14, 27.50 (2.70%),640309,0.00307768510191594 +SN.L,Smith & Nephew PLC,1110.00 13:14, 9.00 (0.82%),480018,0.00230723642374461 +SPD.L,Sports Direct International PLC,694.50 13:11, 1.50 (0.22%),157981,0.000759345519250522 +SSE.L,SSE PLC,1463.00 13:13, 2.00 (0.14%),562454,0.00270347019378617 +STAN.L,Standard Chartered PLC,583.00 13:14, 0.60 (0.10%),2018697,0.00970299290214945 +STJ.L,St James's Place PLC,964.00 13:14, 11.00 (1.13%),418480,0.00201145019271912 +SVT.L,Severn Trent PLC,2199.00 13:12, 1.00 (0.05%),95342,0.000458267263129005 +TPK.L,Travis Perkins PLC,1945.00 13:13, 4.00 (0.21%),92916,0.000446606542981001 +TSCO.L,Tesco PLC,171.54 13:14, 2.54 (1.50%),9831136,0.0472539676970174 +TUI.L,TUI AG,1115.00 13:10, 5.00 (0.45%),458970,0.00220606790038304 +TW.L,Taylor Wimpey PLC,183.90 13:15, 1.10 (0.59%),3180729,0.0152883721086725 +ULVR.L,Unilever PLC,2791.00 13:14, 29.00 (1.03%),824827,0.0039645823650113 +UU.L,United Utilities Group PLC,959.00 13:10, 2.50 (0.26%),436911,0.00210003994253274 +VOD.L,Vodafone Group PLC,224.25 13:15, 1.30 (0.58%),17572036,0.0844610858312637 +WOS.L,Wolseley PLC,3657.00 13:14, 4.00 (0.11%),179536,0.000862950969699912 +WPP.L,WPP PLC,1502.00 13:15, 12.00 (0.79%),857887,0.0041234873147611 +WTB.L,Whitbread PLC,4484.00 13:16, 60.00 (1.32%),141036,0.000677898321019722 diff --git a/example/price/src/main/scala/org/finos/vuu/core/module/price/PriceModule.scala b/example/price/src/main/scala/org/finos/vuu/core/module/price/PriceModule.scala index 6254f5b59..b3de9b53f 100644 --- a/example/price/src/main/scala/org/finos/vuu/core/module/price/PriceModule.scala +++ b/example/price/src/main/scala/org/finos/vuu/core/module/price/PriceModule.scala @@ -43,13 +43,13 @@ class PricesService(val table: DataTable, val provider: Provider) extends RpcHan object PriceModule { final val NAME = "PRICE" - + final val PriceTable = "prices" def apply()(implicit clock: Clock, lifecycle: LifecycleContainer, tableDefContainer: TableDefContainer): ViewServerModule = { ModuleFactory.withNamespace(NAME) .addTable( AutoSubscribeTableDef( - name = "prices", + name = PriceTable, keyField = "ric", Columns.fromNames("ric".string(), "bid".double(), "bidSize".int(), "ask".double(), "askSize".int(), "last".double(), "open".double(), "close".double(), "scenario".string(), "phase".string()), diff --git a/vuu-ui/package-lock.json b/vuu-ui/package-lock.json index 3f9c458db..610a5347e 100644 --- a/vuu-ui/package-lock.json +++ b/vuu-ui/package-lock.json @@ -1228,10 +1228,6 @@ "resolved": "packages/vuu-codemirror", "link": true }, - "node_modules/@finos/vuu-data": { - "resolved": "packages/vuu-data", - "link": true - }, "node_modules/@finos/vuu-data-ag-grid": { "resolved": "packages/vuu-data-ag-grid", "link": true @@ -2007,12 +2003,27 @@ } } }, - "node_modules/@salt-ds/core/node_modules/@salt-ds/icons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.9.0.tgz", - "integrity": "sha512-82PnI/9KStt/owSTV/+vxH5AkJaYDEKuJrBbzeC2axxipv8ydbToTaCn6usLOSTm9CQ04rzMFPNcWQSpGUHtjA==", + "node_modules/@salt-ds/core/node_modules/@salt-ds/styles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.0.tgz", + "integrity": "sha512-tPd/XK1PFkPkBsYwh4fhDbo997eaev6row9aaz/sPM0FpEpU8nbvQRhxtr8iA/S4Q5hCj0xrk/jGTuK40V13oQ==", + "peerDependencies": { + "@types/react": ">=16.14.0", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@salt-ds/icons": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.9.1.tgz", + "integrity": "sha512-ck1tL+vmBFUziFKB973pVLcLLvZIh5N1gDwS1dwm9DRKefXSS0ZtaB4KjrYvw/7LhVG8rqrcwtCSmvqo/3M4rg==", "dependencies": { - "@salt-ds/styles": "^0.2.0", + "@salt-ds/styles": "^0.2.1", "@salt-ds/window": "^0.1.1", "clsx": "^2.0.0" }, @@ -2027,10 +2038,10 @@ } } }, - "node_modules/@salt-ds/core/node_modules/@salt-ds/styles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.0.tgz", - "integrity": "sha512-tPd/XK1PFkPkBsYwh4fhDbo997eaev6row9aaz/sPM0FpEpU8nbvQRhxtr8iA/S4Q5hCj0xrk/jGTuK40V13oQ==", + "node_modules/@salt-ds/icons/node_modules/@salt-ds/styles": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.1.tgz", + "integrity": "sha512-/GYQLY+ILzGyd2/KndCmoEfLw/t3pcYwihJn3ofe4yd6nhLYHPkvl4TXXzq6NnfD3NHmQWnWh3jQicLsYcvdXg==", "peerDependencies": { "@types/react": ">=16.14.0", "react": ">=16.14.0", @@ -2077,26 +2088,6 @@ } } }, - "node_modules/@salt-ds/lab/node_modules/@salt-ds/icons": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.6.0.tgz", - "integrity": "sha512-/9c1L5LyU5cPxaYDyywDu8VnpvOvnaDmuLr/BWLEdyzxSzXTDfcIPtSgpaSaPadubvnnE9mhXdNknLyTqjNXZg==", - "dependencies": { - "@salt-ds/styles": "^0.1.1", - "@salt-ds/window": "^0.1.1", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "@types/react": ">=16.14.0", - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@salt-ds/styles": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.1.1.tgz", @@ -12888,28 +12879,13 @@ "@lezer/highlight": "^1.1.3" } }, - "packages/vuu-data": { - "name": "@finos/vuu-data", - "version": "0.0.26", - "license": "Apache-2.0", - "dependencies": { - "@finos/vuu-filter-parser": "0.0.26", - "@finos/vuu-utils": "0.0.26" - }, - "devDependencies": { - "@finos/vuu-data-types": "0.0.26", - "@finos/vuu-filter-types": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-table-types": "0.0.26" - } - }, "packages/vuu-data-ag-grid": { "name": "@finos/vuu-data-ag-grid", "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-protocol-types": "0.0.26", @@ -12939,7 +12915,7 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-parser": "0.0.26", "@finos/vuu-utils": "0.0.26" @@ -12974,10 +12950,10 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { + "@finos/vuu-data-local": "0.0.26", "@thomaschaplin/isin-generator": "1.0.3" }, "devDependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-table-types": "0.0.26" } }, @@ -13027,7 +13003,7 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", @@ -13066,7 +13042,6 @@ "license": "Apache-2.0", "dependencies": { "@finos/vuu-codemirror": "0.0.26", - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-ui-controls": "0.0.26", @@ -13144,7 +13119,7 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-icons": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -13240,7 +13215,8 @@ "@finos/vuu-table": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.13.2" + "@salt-ds/core": "1.13.2", + "@salt-ds/icons": "1.9.1" }, "peerDependencies": { "clsx": "^2.0.0", @@ -13263,36 +13239,13 @@ "react-dom": ">=17.0.2" } }, - "sample-apps/app-vuu-basket-trader": { - "version": "0.0.26", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid-types": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@fontsource/open-sans": "^4.5.13", - "@salt-ds/core": "1.8.0", - "@salt-ds/lab": "1.0.0-alpha.15", - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "devDependencies": {}, - "engines": { - "node": ">=16.0.0" - } - }, "sample-apps/app-vuu-example": { "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-shell": "0.0.26", @@ -13314,8 +13267,8 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", @@ -13347,8 +13300,8 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -13378,8 +13331,8 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -13409,8 +13362,8 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -13435,67 +13388,6 @@ "react-dom": ">=17.0.2" } }, - "sample-apps/feature-vuu-blotter": { - "version": "0.0.26", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid": "0.0.26", - "@finos/vuu-filters": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-theme": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", - "@salt-ds/lab": "1.0.0-alpha.15" - }, - "devDependencies": {}, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, - "sample-apps/feature-vuu-table": { - "version": "0.0.26", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid-types": "0.0.26", - "@finos/vuu-filter-types": "0.0.26", - "@finos/vuu-filters": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-table": "0.0.26", - "@finos/vuu-table-extras": "0.0.26", - "@finos/vuu-theme": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", - "@salt-ds/lab": "1.0.0-alpha.15" - }, - "devDependencies": {}, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, "sample-apps/standalone-table": { "version": "0.0.26", "license": "Apache-2.0", @@ -13523,6 +13415,7 @@ "@finos/vuu-layout": "0.0.26", "@finos/vuu-theme": "0.0.26", "@finos/vuu-utils": "0.0.26", + "@internationalized/date": "^3.0.0", "@salt-ds/core": "1.13.2", "@salt-ds/theme": "1.7.1", "clsx": "^2.0.0", @@ -13534,6 +13427,43 @@ "@mdx-js/esbuild": "^2.3.0", "@thomaschaplin/isin-generator": "^1.0.3" } + }, + "showcase/node_modules/@salt-ds/core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@salt-ds/core/-/core-1.13.3.tgz", + "integrity": "sha512-C/i3b74fNC87+dLqZlmvotLQIIlacV3tmZyBXW0njUtrhnHKnQ5X8lP2d5p3Y8P7xSSt9iRNs0dbpS91WDKmBw==", + "dependencies": { + "@floating-ui/react": "^0.23.0", + "@salt-ds/icons": "^1.9.1", + "@salt-ds/styles": "^0.2.1", + "@salt-ds/window": "^0.1.1", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "@types/react": ">=16.14.0", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "showcase/node_modules/@salt-ds/styles": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.1.tgz", + "integrity": "sha512-/GYQLY+ILzGyd2/KndCmoEfLw/t3pcYwihJn3ofe4yd6nhLYHPkvl4TXXzq6NnfD3NHmQWnWh3jQicLsYcvdXg==", + "peerDependencies": { + "@types/react": ">=16.14.0", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } } }, "dependencies": { @@ -14259,6 +14189,7 @@ "@finos/vuu-layout": "0.0.26", "@finos/vuu-theme": "0.0.26", "@finos/vuu-utils": "0.0.26", + "@internationalized/date": "^3.0.0", "@mdx-js/esbuild": "^2.3.0", "@salt-ds/core": "1.13.2", "@salt-ds/theme": "1.7.1", @@ -14267,6 +14198,25 @@ "react": ">=17.0.2", "react-dom": ">=17.0.2", "react-router-dom": "^6.2.1" + }, + "dependencies": { + "@salt-ds/core": { + "version": "https://registry.npmjs.org/@salt-ds/core/-/core-1.13.3.tgz", + "integrity": "sha512-C/i3b74fNC87+dLqZlmvotLQIIlacV3tmZyBXW0njUtrhnHKnQ5X8lP2d5p3Y8P7xSSt9iRNs0dbpS91WDKmBw==", + "requires": { + "@floating-ui/react": "^0.23.0", + "@salt-ds/icons": "^1.9.1", + "@salt-ds/styles": "^0.2.1", + "@salt-ds/window": "^0.1.1", + "clsx": "^2.0.0" + } + }, + "@salt-ds/styles": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.1.tgz", + "integrity": "sha512-/GYQLY+ILzGyd2/KndCmoEfLw/t3pcYwihJn3ofe4yd6nhLYHPkvl4TXXzq6NnfD3NHmQWnWh3jQicLsYcvdXg==", + "requires": {} + } } }, "@finos/vuu-codemirror": { @@ -14281,22 +14231,11 @@ "@lezer/highlight": "^1.1.3" } }, - "@finos/vuu-data": { - "version": "file:packages/vuu-data", - "requires": { - "@finos/vuu-data-types": "0.0.26", - "@finos/vuu-filter-parser": "0.0.26", - "@finos/vuu-filter-types": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-table-types": "0.0.26", - "@finos/vuu-utils": "0.0.26" - } - }, "@finos/vuu-data-ag-grid": { "version": "file:packages/vuu-data-ag-grid", "requires": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-protocol-types": "0.0.26", @@ -14317,7 +14256,7 @@ "@finos/vuu-data-react": { "version": "file:packages/vuu-data-react", "requires": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-parser": "0.0.26", "@finos/vuu-filter-types": "0.0.26", @@ -14341,7 +14280,7 @@ "@finos/vuu-data-test": { "version": "file:packages/vuu-data-test", "requires": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@thomaschaplin/isin-generator": "1.0.3" } @@ -14356,7 +14295,7 @@ "@finos/vuu-datatable": { "version": "file:packages/vuu-datatable", "requires": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", @@ -14384,7 +14323,6 @@ "version": "file:packages/vuu-filters", "requires": { "@finos/vuu-codemirror": "0.0.26", - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-types": "0.0.26", @@ -14434,7 +14372,7 @@ "@finos/vuu-shell": { "version": "file:packages/vuu-shell", "requires": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-icons": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -14502,7 +14440,8 @@ "@finos/vuu-table": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.13.2" + "@salt-ds/core": "1.13.2", + "@salt-ds/icons": "1.9.1" } }, "@finos/vuu-utils": { @@ -15015,16 +14954,6 @@ "clsx": "^2.0.0" }, "dependencies": { - "@salt-ds/icons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.9.0.tgz", - "integrity": "sha512-82PnI/9KStt/owSTV/+vxH5AkJaYDEKuJrBbzeC2axxipv8ydbToTaCn6usLOSTm9CQ04rzMFPNcWQSpGUHtjA==", - "requires": { - "@salt-ds/styles": "^0.2.0", - "@salt-ds/window": "^0.1.1", - "clsx": "^2.0.0" - } - }, "@salt-ds/styles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.0.tgz", @@ -15033,6 +14962,24 @@ } } }, + "@salt-ds/icons": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.9.1.tgz", + "integrity": "sha512-ck1tL+vmBFUziFKB973pVLcLLvZIh5N1gDwS1dwm9DRKefXSS0ZtaB4KjrYvw/7LhVG8rqrcwtCSmvqo/3M4rg==", + "requires": { + "@salt-ds/styles": "^0.2.1", + "@salt-ds/window": "^0.1.1", + "clsx": "^2.0.0" + }, + "dependencies": { + "@salt-ds/styles": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@salt-ds/styles/-/styles-0.2.1.tgz", + "integrity": "sha512-/GYQLY+ILzGyd2/KndCmoEfLw/t3pcYwihJn3ofe4yd6nhLYHPkvl4TXXzq6NnfD3NHmQWnWh3jQicLsYcvdXg==", + "requires": {} + } + } + }, "@salt-ds/lab": { "version": "1.0.0-alpha.15", "resolved": "https://registry.npmjs.org/@salt-ds/lab/-/lab-1.0.0-alpha.15.tgz", @@ -15056,18 +15003,6 @@ "react-window": "^1.8.6", "rifm": "^0.12.0", "tinycolor2": "^1.4.2" - }, - "dependencies": { - "@salt-ds/icons": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@salt-ds/icons/-/icons-1.6.0.tgz", - "integrity": "sha512-/9c1L5LyU5cPxaYDyywDu8VnpvOvnaDmuLr/BWLEdyzxSzXTDfcIPtSgpaSaPadubvnnE9mhXdNknLyTqjNXZg==", - "requires": { - "@salt-ds/styles": "^0.1.1", - "@salt-ds/window": "^0.1.1", - "clsx": "^2.0.0" - } - } } }, "@salt-ds/styles": { @@ -15922,8 +15857,9 @@ "app-vuu-example": { "version": "file:sample-apps/app-vuu-example", "requires": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-shell": "0.0.26", @@ -17862,8 +17798,8 @@ "feature-basket-trading": { "version": "file:sample-apps/feature-basket-trading", "requires": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", @@ -17883,8 +17819,8 @@ "feature-vuu-filter-table": { "version": "file:sample-apps/feature-filter-table", "requires": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -17903,8 +17839,8 @@ "feature-vuu-instrument-tiles": { "version": "file:sample-apps/feature-instrument-tiles", "requires": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -17923,8 +17859,8 @@ "feature-vuu-template": { "version": "file:sample-apps/feature-template", "requires": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-filter-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", diff --git a/vuu-ui/packages/vuu-data-ag-grid/package.json b/vuu-ui/packages/vuu-data-ag-grid/package.json index aa1d35dfa..6f6d17cda 100644 --- a/vuu-ui/packages/vuu-data-ag-grid/package.json +++ b/vuu-ui/packages/vuu-data-ag-grid/package.json @@ -9,8 +9,8 @@ "type-defs": "node ../../scripts/build-type-defs.mjs" }, "dependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-filter-types": "0.0.26", diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts index cc46c6747..c39de166d 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts @@ -54,6 +54,8 @@ import { buildDataToClientMap, toClientRow } from "./array-data-utils"; import { collapseGroup, expandGroup, GroupMap, groupRows } from "./group-utils"; import { sortRows } from "./sort-utils"; +const { debug, info } = logger("ArrayDataSource"); + const { KEY } = metadataKeys; export interface ArrayDataSourceConstructorProps @@ -64,7 +66,6 @@ export interface ArrayDataSourceConstructorProps keyColumn?: string; rangeChangeRowset?: "delta" | "full"; } -const { debug } = logger("ArrayDataSource"); const toDataSourceRow = (key: number) => @@ -232,6 +233,7 @@ export class ArrayDataSource if (range) { // set range and trigger dispatch of initial rows this.range = range; + this.sendRowsToClient(); } else if (this.#range !== NULL_RANGE) { this.sendRowsToClient(); } @@ -239,26 +241,36 @@ export class ArrayDataSource } unsubscribe() { - console.log("unsubscribe noop"); + console.log(`unsubscribe noop`); } suspend() { - // console.log("noop"); + console.log(`suspend #${this.viewport}, current status ${this.#status}`); + info?.(`suspend #${this.viewport}, current status ${this.#status}`); + this.#status = "suspended"; + return this; } resume() { - // console.log("resume noop"); + console.log(`resume #${this.viewport}, current status ${this.#status}`); + // const isDisabled = this.#status.startsWith("disabl"); + const isSuspended = this.#status === "suspended"; + info?.(`resume #${this.viewport}, current status ${this.#status}`); + console.log(`resume noop`); + if (isSuspended) { + this.#status = "subscribed"; + } return this; } disable() { - // console.log("disable noop"); + console.log(`disable noop`); return this; } enable() { - // console.log("enable noop"); + console.log(`enable noop`); return this; } @@ -439,9 +451,7 @@ export class ArrayDataSource } set range(range: VuuRange) { - if (range.from !== this.#range.from || range.to !== this.#range.to) { - this.setRange(range); - } + this.setRange(range); } protected delete(row: VuuRowDataItemType[]) { @@ -492,9 +502,13 @@ export class ArrayDataSource }; private setRange(range: VuuRange, forceFullRefresh = false) { - this.#range = range; - this.keys.reset(range); - this.sendRowsToClient(forceFullRefresh); + if (range.from !== this.#range.from || range.to !== this.#range.to) { + this.#range = range; + this.keys.reset(range); + this.sendRowsToClient(forceFullRefresh); + } else if (forceFullRefresh) { + this.sendRowsToClient(forceFullRefresh); + } } sendRowsToClient(forceFullRefresh = false, row?: DataSourceRow) { diff --git a/vuu-ui/packages/vuu-data-react/package.json b/vuu-ui/packages/vuu-data-react/package.json index 9056d8b20..91fb8388b 100644 --- a/vuu-ui/packages/vuu-data-react/package.json +++ b/vuu-ui/packages/vuu-data-react/package.json @@ -16,7 +16,7 @@ "@finos/vuu-protocol-types": "0.0.26" }, "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-filter-parser": "0.0.26", "@finos/vuu-utils": "0.0.26" diff --git a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js index 7b138e587..19e233673 100644 --- a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js @@ -193,7 +193,7 @@ var KeySet = class { } next() { if (this.free.length > 0) { - return this.free.pop(); + return this.free.shift(); } else { return this.nextKeyValue++; } @@ -231,7 +231,7 @@ var KeySet = class { return key; } toDebugString() { - return Array.from(this.keys.entries()).map((k, v) => \`\${k}=>\${v}\`).join(","); + return Array.from(this.keys.entries()).map(([k, v]) => \`\${k}=>\${v}\`).join(","); } }; diff --git a/vuu-ui/packages/vuu-data-remote/src/message-utils.ts b/vuu-ui/packages/vuu-data-remote/src/message-utils.ts index 07647817e..72fe93113 100644 --- a/vuu-ui/packages/vuu-data-remote/src/message-utils.ts +++ b/vuu-ui/packages/vuu-data-remote/src/message-utils.ts @@ -74,24 +74,6 @@ export interface VuuTableMetaWithTable extends VuuTableMeta { table: VuuTable; } -export const getColumnByName = ( - schema: TableSchema, - name?: string -): SchemaColumn | undefined => { - if (name === undefined) { - return undefined; - } else { - const column = schema.columns.find((col) => col.name === name); - if (column) { - return column; - } else { - throw Error( - `getColumnByName no column '${name}' in schema for ${schema.table.table}` - ); - } - } -}; - export const createSchemaFromTableMetadata = ({ columns, dataTypes, diff --git a/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts b/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts index 5f5558ea8..50c40c082 100644 --- a/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts +++ b/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts @@ -263,7 +263,6 @@ export class VuuDataSource resume() { console.log(`resume #${this.viewport}, current status ${this.#status}`); - const isDisabled = this.#status.startsWith("disabl"); const isSuspended = this.#status === "suspended"; info?.(`resume #${this.viewport}, current status ${this.#status}`); diff --git a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts index 692dbbe45..cc12240b8 100644 --- a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts @@ -18,8 +18,8 @@ import { updateTableRow, createSubscription, } from "./test-utils"; -import { DataSourceDataMessage, DataSourceEnabledMessage } from "../src"; import { VuuRow } from "@finos/vuu-protocol-types"; +import { DataSourceDataMessage } from "@finos/vuu-data-types"; const SERVER_MESSAGE_CONSTANTS = { module: "CORE", @@ -270,8 +270,8 @@ describe("ServerProxy", () => { [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true], - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); }); @@ -323,16 +323,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [20,9,true,false,0,0,"key-20",0,"key-20","name 20",1020,true,], - [21,8,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], - [22,7,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], - [23,6,true,false,0,0,"key-23",0,"key-23","name 23",1023,true], - [24,5,true,false,0,0,"key-24",0,"key-24","name 24",1024,true], - [25,4,true,false,0,0,"key-25",0,"key-25","name 25",1025,true], - [26,3,true,false,0,0,"key-26",0,"key-26","name 26",1026,true], - [27,2,true,false,0,0,"key-27",0,"key-27","name 27",1027,true], - [28,1,true,false,0,0,"key-28",0,"key-28","name 28",1028,true], - [29,0,true,false,0,0,"key-29",0,"key-29","name 29",1029,true,], + [20,0,true,false,0,0,"key-20",0,"key-20","name 20",1020,true,], + [21,1,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [22,2,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], + [23,3,true,false,0,0,"key-23",0,"key-23","name 23",1023,true], + [24,4,true,false,0,0,"key-24",0,"key-24","name 24",1024,true], + [25,5,true,false,0,0,"key-25",0,"key-25","name 25",1025,true], + [26,6,true,false,0,0,"key-26",0,"key-26","name 26",1026,true], + [27,7,true,false,0,0,"key-27",0,"key-27","name 27",1027,true], + [28,8,true,false,0,0,"key-28",0,"key-28","name 28",1028,true], + [29,9,true,false,0,0,"key-29",0,"key-29","name 29",1029,true,], ], }); }); @@ -499,15 +499,15 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [11,8,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], - [12,7,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,6,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,5,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [12,1,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,2,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,3,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], [15,4,true,false,0,0,"key-15",0,"key-15","name 15",1015,true,], - [16,3,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,2,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,1,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,0,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [16,5,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,6,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,7,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,8,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], ], }); }); @@ -707,26 +707,26 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [4975,19,true,false,0,0,"key-75",0,"key-75","name 75",5975,true], - [4976,18,true,false,0,0,"key-76",0,"key-76","name 76",5976,true], - [4977,17,true,false,0,0,"key-77",0,"key-77","name 77",5977,true], - [4978,16,true,false,0,0,"key-78",0,"key-78","name 78",5978,true], - [4979,15,true,false,0,0,"key-79",0,"key-79","name 79",5979,true], - [4980,14,true,false,0,0,"key-80",0,"key-80","name 80",5980,true], - [4981,13,true,false,0,0,"key-81",0,"key-81","name 81",5981,true], - [4982,12,true,false,0,0,"key-82",0,"key-82","name 82",5982,true], - [4983,11,true,false,0,0,"key-83",0,"key-83","name 83",5983,true], - [4984,10,true,false,0,0,"key-84",0,"key-84","name 84",5984,true], - [4985,9,true,false,0,0,"key-85",0,"key-85","name 85",5985,true], - [4986,8,true,false,0,0,"key-86",0,"key-86","name 86",5986,true], - [4987,7,true,false,0,0,"key-87",0,"key-87","name 87",5987,true], - [4988,6,true,false,0,0,"key-88",0,"key-88","name 88",5988,true], - [4989,5,true,false,0,0,"key-89",0,"key-89","name 89",5989,true], - [4990,4,true,false,0,0,"key-90",0,"key-90","name 90",5990,true], - [4991,3,true,false,0,0,"key-91",0,"key-91","name 91",5991,true], - [4992,2,true,false,0,0,"key-92",0,"key-92","name 92",5992,true], - [4993,1,true,false,0,0,"key-93",0,"key-93","name 93",5993,true], - [4994,0,true,false,0,0,"key-94",0,"key-94","name 94",5994,true], + [4975,0,true,false,0,0,"key-75",0,"key-75","name 75",5975,true], + [4976,1,true,false,0,0,"key-76",0,"key-76","name 76",5976,true], + [4977,2,true,false,0,0,"key-77",0,"key-77","name 77",5977,true], + [4978,3,true,false,0,0,"key-78",0,"key-78","name 78",5978,true], + [4979,4,true,false,0,0,"key-79",0,"key-79","name 79",5979,true], + [4980,5,true,false,0,0,"key-80",0,"key-80","name 80",5980,true], + [4981,6,true,false,0,0,"key-81",0,"key-81","name 81",5981,true], + [4982,7,true,false,0,0,"key-82",0,"key-82","name 82",5982,true], + [4983,8,true,false,0,0,"key-83",0,"key-83","name 83",5983,true], + [4984,9,true,false,0,0,"key-84",0,"key-84","name 84",5984,true], + [4985,10,true,false,0,0,"key-85",0,"key-85","name 85",5985,true], + [4986,11,true,false,0,0,"key-86",0,"key-86","name 86",5986,true], + [4987,12,true,false,0,0,"key-87",0,"key-87","name 87",5987,true], + [4988,13,true,false,0,0,"key-88",0,"key-88","name 88",5988,true], + [4989,14,true,false,0,0,"key-89",0,"key-89","name 89",5989,true], + [4990,15,true,false,0,0,"key-90",0,"key-90","name 90",5990,true], + [4991,16,true,false,0,0,"key-91",0,"key-91","name 91",5991,true], + [4992,17,true,false,0,0,"key-92",0,"key-92","name 92",5992,true], + [4993,18,true,false,0,0,"key-93",0,"key-93","name 93",5993,true], + [4994,19,true,false,0,0,"key-94",0,"key-94","name 94",5994,true], [4995,20,true,false,0,0,"key-95",0,"key-95","name 95",5995,true], [4996,21,true,false,0,0,"key-96",0,"key-96","name 96",5996,true], [4997,22,true,false,0,0,"key-97",0,"key-97","name 97",5997,true], @@ -770,8 +770,8 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); @@ -793,9 +793,9 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,4,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [12,2,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], [13,3,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [14,4,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], ], }); @@ -818,9 +818,9 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [15,7,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [15,5,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], [16,6,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [17,7,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], ], }); @@ -911,8 +911,8 @@ describe("ServerProxy", () => { [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true], - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); }); @@ -1023,16 +1023,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,0,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,1,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], - [15,3,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], - [16,4,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,6,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,7,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], - [20,9,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], - [21,8,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [12,2,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,3,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,4,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [15,5,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [16,6,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,7,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,8,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,9,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [20,0,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], + [21,1,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], ], }); }); @@ -1086,16 +1086,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,9,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,8,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,7,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], - [15,6,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], - [16,5,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,4,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,3,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,2,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], - [20,1,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], - [21,0,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [12,0,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,1,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [15,3,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [16,4,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,6,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,7,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [20,8,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], + [21,9,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], [22,10,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], ], }); diff --git a/vuu-ui/packages/vuu-data-test/package.json b/vuu-ui/packages/vuu-data-test/package.json index 7237d0ef3..ee23ca6a5 100644 --- a/vuu-ui/packages/vuu-data-test/package.json +++ b/vuu-ui/packages/vuu-data-test/package.json @@ -9,10 +9,10 @@ "type-defs": "node ../../scripts/build-type-defs.mjs" }, "dependencies": { + "@finos/vuu-data-local": "0.0.26", "@thomaschaplin/isin-generator": "1.0.3" }, "devDependencies": { - "@finos/vuu-data": "0.0.26", "@finos/vuu-table-types": "0.0.26" } } diff --git a/vuu-ui/showcase/src/examples/utils/ArrayProxy.ts b/vuu-ui/packages/vuu-data-test/src/ArrayProxy.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/ArrayProxy.ts rename to vuu-ui/packages/vuu-data-test/src/ArrayProxy.ts diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index 435bc6cd1..2e321a5cc 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -1,13 +1,16 @@ import { ArrayDataSource, - ArrayDataSourceConstructorProps + ArrayDataSourceConstructorProps, } from "@finos/vuu-data-local"; import { - DataSourceRow, MenuRpcResponse, RpcResponse, + DataSourceRow, + MenuRpcResponse, + RpcResponse, SelectionItem, SubscribeCallback, - SubscribeProps, VuuUIMessageInRPCEditReject, - VuuUIMessageInRPCEditResponse + SubscribeProps, + VuuUIMessageInRPCEditReject, + VuuUIMessageInRPCEditResponse, } from "@finos/vuu-data-types"; import { ClientToServerEditRpc, @@ -15,7 +18,7 @@ import { ClientToServerViewportRpcCall, VuuMenu, VuuRange, - VuuRowDataItemType + VuuRowDataItemType, } from "@finos/vuu-protocol-types"; import { metadataKeys } from "@finos/vuu-utils"; import { makeSuggestions } from "./makeSuggestions"; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts index 37fe1c24d..2977e074e 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts @@ -91,6 +91,8 @@ function createTradingBasket(basketId: string, basketName: string) { (c) => c[key] === basketId ); + const { instanceId } = tableMaps.basketTrading; + constituents.forEach(([, , description, , ric, , , quantity, weighting]) => { const algo = ""; const algoParams = ""; @@ -103,7 +105,6 @@ function createTradingBasket(basketId: string, basketName: string) { const side = "BUY"; const venue = "venue"; - const { instanceId } = tableMaps.basketTrading; const basketInstanceId = basketTradingRow[instanceId]; const basketTradingConstituentRow: VuuRowDataItemType[] = [ algo, @@ -126,6 +127,9 @@ function createTradingBasket(basketId: string, basketName: string) { ]; basketTradingConstituent.insert(basketTradingConstituentRow); }); + + // return the key + return basketTradingRow[instanceId] as string; } async function addConstituent(rpcRequest: ClientToServerViewportRpcCall) { @@ -144,7 +148,13 @@ async function createNewBasket(rpcRequest: ClientToServerViewportRpcCall) { const { params: [basketId, basketName], } = rpcRequest; - createTradingBasket(basketId, basketName); + const key = createTradingBasket(basketId, basketName); + return { + action: { + type: "VP_CREATE_SUCCESS", + key, + }, + }; } //------------------- diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts index 6efd6cf1b..02554a94f 100644 --- a/vuu-ui/packages/vuu-data-test/src/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -1,3 +1,4 @@ +export * from "./ArrayProxy"; export * from "./schemas"; export * from "./TickingArrayDataSource"; export * from "./vuu-row-generator"; diff --git a/vuu-ui/packages/vuu-datatable/package.json b/vuu-ui/packages/vuu-datatable/package.json index 56914915a..9cd5b5f1f 100644 --- a/vuu-ui/packages/vuu-datatable/package.json +++ b/vuu-ui/packages/vuu-datatable/package.json @@ -9,7 +9,7 @@ "type-defs": "node ../../scripts/build-type-defs.mjs" }, "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", diff --git a/vuu-ui/packages/vuu-filters/package.json b/vuu-ui/packages/vuu-filters/package.json index e312c2cbb..5ef28661b 100644 --- a/vuu-ui/packages/vuu-filters/package.json +++ b/vuu-ui/packages/vuu-filters/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@finos/vuu-codemirror": "0.0.26", - "@finos/vuu-data": "0.0.26", "@finos/vuu-data-react": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-ui-controls": "0.0.26", diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts index 551193bd9..63ad4364b 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts @@ -252,43 +252,40 @@ export const useFilterBar = ({ [deleteFilter, editPillLabel, filters, focusFilterClause] ); + const addIfNewElseUpdate = useCallback( + (edited: Filter, existing: Filter | undefined) => { + if (existing === undefined) { + const idx = onAddFilter(edited); + editPillLabel(idx); + return idx; + } else { + return onChangeFilter(existing, edited); + } + }, + [editPillLabel, onAddFilter, onChangeFilter] + ); + const handleMenuAction = useCallback( ({ menuId }) => { switch (menuId) { case "apply-save": { - // TODO save these into state together - const isNewFilter = editingFilter.current === undefined; - const newFilter = editFilter as Filter; - const changeHandler = isNewFilter ? onAddFilter : onChangeFilter; - const indexOfNewFilter = changeHandler(newFilter); - - if (isNewFilter) { - editPillLabel(indexOfNewFilter); - } - + const editedFilter = editFilter as Filter; + const idx = addIfNewElseUpdate(editedFilter, editingFilter.current); + setActiveFilterIndex(appendIfNotPresent(idx)); setEditFilter(undefined); - - setActiveFilterIndex((indices) => - indices.includes(indexOfNewFilter) - ? indices - : indices.concat(indexOfNewFilter) - ); - + editingFilter.current = undefined; setShowMenu(false); return true; } - case "and-clause": { const newFilter = addClause( editFilter as Filter, EMPTY_FILTER_CLAUSE ); - console.log({ newFilter }); setEditFilter(newFilter); setShowMenu(false); return true; } - case "or-clause": setEditFilter((filter) => addClause(filter as Filter, {}, { combineWith: "or" }) @@ -299,7 +296,7 @@ export const useFilterBar = ({ return false; } }, - [editFilter, editPillLabel, onAddFilter, onChangeFilter] + [editFilter, addIfNewElseUpdate] ); useEffect(() => { @@ -471,3 +468,6 @@ export const useFilterBar = ({ showMenu, }; }; + +const appendIfNotPresent = (n: number) => (ns: number[]) => + ns.includes(n) ? ns : ns.concat(n); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts index 4a85266d7..877b40d60 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts @@ -167,12 +167,12 @@ export const useFilters = ({ ); const handleChangeFilter = useCallback( - (filter: Filter) => { + (oldFilter: Filter, newFilter: Filter) => { let index = -1; const newFilters = filters.map((f, i) => { - if (f === filter) { + if (f === oldFilter) { index = i; - return filter; + return newFilter; } else { return f; } diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/TextInput.tsx b/vuu-ui/packages/vuu-filters/src/filter-clause/TextInput.tsx index 44cd3fd7b..8bcec734b 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/TextInput.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-clause/TextInput.tsx @@ -58,25 +58,15 @@ export const TextInput = forwardRef(function TextInput( const getSuggestions = suggestionProvider(); const handleSingleValueSelectionChange = useCallback( - (evt, value) => onInputComplete(value), + (_, value) => onInputComplete(value), [onInputComplete] ); const handleMultiValueSelectionChange = useCallback( - (evt, value) => { - if (value.length === 1) { - onInputComplete(value[0]); - } else if (value.length > 1) { - onInputComplete(value); - } - }, + (_, values) => onInputComplete(values), [onInputComplete] ); - useEffect(() => { - // setValueInputValue(""); - }, [column]); - useEffect(() => { if (table) { const params: TypeaheadParams = valueInputValue @@ -130,7 +120,6 @@ export const TextInput = forwardRef(function TextInput( } switch (operator) { case "in": - //TODO multiselect return ( { const [screenshotErrorMessage, setScreenshotErrorMessage] = useState< string | undefined >(); + const [username] = getAuthDetailsFromCookies(); useEffect(() => { if (componentId) { @@ -53,7 +55,7 @@ export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => { name: layoutName, group, screenshot: screenshot ?? "", - user: "User", + user: username, }); }; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index 2d99a8161..fa2927b47 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -245,7 +245,7 @@ export const LayoutManagementProvider = ( getPersistenceManager() .loadLayout(id) .then((layoutJson) => { - const { layout: currentLayout } = applicationJSONRef.current; + const { layout: currentLayout } = applicationJSONRef.current; setApplicationLayout({ ...currentLayout, children: (currentLayout.children || []).concat(layoutJson), diff --git a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css index cf8fb562c..12710db0d 100644 --- a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css +++ b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css @@ -16,7 +16,7 @@ --menu-level-2-width: 0px; - box-shadow: 3px 4px 4px 0px rgba(0, 0, 0, 0.15); + box-shadow: 3px 4px 4px 0 rgba(0, 0, 0, 0.15); display: flex; height: calc(100% - 4px); margin-bottom: 4px; @@ -165,12 +165,12 @@ .vuuLeftNav-drawer { display: flex; - padding: 40px 32px 0px 24px; + padding: 0 0 0 24px; flex-direction: column; align-items: flex-start; flex-shrink: 0; align-self: stretch; background: #FFF; - box-shadow: 3px 4px 4px 0px rgba(0, 0, 0, 0.15); + box-shadow: 3px 4px 4px 0 rgba(0, 0, 0, 0.15); height: 100% } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts b/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts index 9f7e2f387..56648dffe 100644 --- a/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts +++ b/vuu-ui/packages/vuu-shell/src/persistence-management/LocalPersistenceManager.ts @@ -1,5 +1,6 @@ import { ApplicationJSON, LayoutJSON } from "@finos/vuu-layout"; import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; +import { getAuthDetailsFromCookies } from "@finos/vuu-shell"; import { formatDate, getUniqueId } from "@finos/vuu-utils"; import { defaultApplicationJson } from "./defaultApplicationJson"; @@ -11,16 +12,21 @@ import { WithId, } from "../layout-management"; -const metadataSaveLocation = "layouts/metadata"; -const layoutsSaveLocation = "layouts/layouts"; +const baseMetadataSaveLocation = "layouts/metadata"; +const baseLayoutsSaveLocation = "layouts/layouts"; export class LocalPersistenceManager implements PersistenceManager { - #urlKey = "api/vui"; + username: string = getAuthDetailsFromCookies()[0]; + metadataSaveLocation = `${baseMetadataSaveLocation}/${this.username}`; + layoutsSaveLocation = `${baseLayoutsSaveLocation}/${this.username}`; + + #urlKey = `api/vui/${this.username}`; constructor(urlKey?: string) { if (urlKey) { this.#urlKey = urlKey; } } + createLayout( metadata: LayoutMetadataDto, layout: LayoutJSON @@ -103,7 +109,7 @@ export class LocalPersistenceManager implements PersistenceManager { loadMetadata(): Promise { return new Promise((resolve) => { - const metadata = getLocalEntity(metadataSaveLocation); + const metadata = getLocalEntity(this.metadataSaveLocation); resolve(metadata || []); }); } @@ -133,24 +139,24 @@ export class LocalPersistenceManager implements PersistenceManager { }); } - private loadLayouts(): Promise { + loadLayouts = (): Promise => { return new Promise((resolve) => { - const layouts = getLocalEntity(layoutsSaveLocation); + const layouts = getLocalEntity(this.layoutsSaveLocation); resolve(layouts || []); }); } - private saveLayoutsWithMetadata( + saveLayoutsWithMetadata = ( layouts: Layout[], metadata: LayoutMetadata[] - ): void { - saveLocalEntity(layoutsSaveLocation, layouts); - saveLocalEntity(metadataSaveLocation, metadata); + ): void => { + saveLocalEntity(this.layoutsSaveLocation, layouts); + saveLocalEntity(this.metadataSaveLocation, metadata); } // Ensures that there is exactly one Layout entry and exactly one Metadata // entry in local storage corresponding to the provided ID. - private async validateIds(id: string): Promise { + validateIds = async (id: string): Promise => { return Promise.all([ this.validateId(id, "metadata").catch((error) => error.message), this.validateId(id, "layout").catch((error) => error.message), @@ -168,13 +174,13 @@ export class LocalPersistenceManager implements PersistenceManager { // Ensures that there is exactly one element (Layout or Metadata) in local // storage corresponding to the provided ID. - private validateId( + validateId = ( id: string, dataType: "metadata" | "layout" - ): Promise { + ): Promise => { return new Promise((resolve, reject) => { const loadFunc = - dataType === "metadata" ? this.loadMetadata : this.loadLayouts; + dataType === "metadata" ? () => this.loadMetadata() : () => this.loadLayouts(); loadFunc().then((array: WithId[]) => { const count = array.filter((element) => element.id === id).length; diff --git a/vuu-ui/packages/vuu-shell/src/persistence-management/RemotePersistenceManager.ts b/vuu-ui/packages/vuu-shell/src/persistence-management/RemotePersistenceManager.ts index a80e6baf8..e7c908dd3 100644 --- a/vuu-ui/packages/vuu-shell/src/persistence-management/RemotePersistenceManager.ts +++ b/vuu-ui/packages/vuu-shell/src/persistence-management/RemotePersistenceManager.ts @@ -1,3 +1,4 @@ +import { getAuthDetailsFromCookies } from "@finos/vuu-shell"; import { PersistenceManager } from "./PersistenceManager"; import { ApplicationJSON, @@ -15,6 +16,8 @@ export type GetLayoutResponseDto = { definition: LayoutJSON }; export type GetApplicationResponseDto = { definition: ApplicationJSON }; export class RemotePersistenceManager implements PersistenceManager { + username: string = getAuthDetailsFromCookies()[0]; + createLayout( metadata: LayoutMetadataDto, layout: LayoutJSON @@ -139,7 +142,7 @@ export class RemotePersistenceManager implements PersistenceManager { method: "PUT", headers: { "Content-Type": "application/json", - username: "vuu-user", + username: this.username, }, body: JSON.stringify(applicationJSON), }) @@ -160,7 +163,7 @@ export class RemotePersistenceManager implements PersistenceManager { fetch(`${baseURL}/${applicationLayoutsSaveLocation}`, { method: "GET", headers: { - username: "vuu-user", + username: this.username, }, }) .then((response) => { diff --git a/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts index 021df57bd..bdd447543 100644 --- a/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-shell/test/layout-persistence/LocalLayoutPersistenceManager.test.ts @@ -27,6 +27,16 @@ vi.mock("@finos/vuu-filters", async () => { }; }); +const username = "vuu user"; + +vi.mock("@finos/vuu-shell", async () => { + return { + getAuthDetailsFromCookies: (): [string, string] => { + return [username, "token"]; + }, + }; +}); + const persistenceManager = new LocalPersistenceManager(); const existingId = "existing_id"; @@ -38,7 +48,7 @@ const existingMetadata: LayoutMetadata = { name: "Existing Layout", group: "Group 1", screenshot: "screenshot", - user: "vuu user", + user: username, created: newDate, }; @@ -51,14 +61,14 @@ const metadataToAdd: LayoutMetadataDto = { name: "New Layout", group: "Group 1", screenshot: "screenshot", - user: "vuu user", + user: username, }; const metadataToUpdate: Omit = { name: "New Layout", group: "Group 1", screenshot: "screenshot", - user: "vuu user", + user: username, created: newDate, }; @@ -66,8 +76,8 @@ const layoutToAdd: LayoutJSON = { type: "t", }; -const metadataSaveLocation = "layouts/metadata"; -const layoutsSaveLocation = "layouts/layouts"; +const metadataSaveLocation = `layouts/metadata/${username}`; +const layoutsSaveLocation = `layouts/layouts/${username}`; afterEach(() => { localStorage.clear(); diff --git a/vuu-ui/packages/vuu-shell/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts b/vuu-ui/packages/vuu-shell/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts index 2068f25ef..68ab36fb6 100644 --- a/vuu-ui/packages/vuu-shell/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts +++ b/vuu-ui/packages/vuu-shell/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts @@ -9,17 +9,28 @@ import { LayoutJSON } from "@finos/vuu-layout"; import { v4 as uuidv4 } from "uuid"; import { expectPromiseRejectsWithError } from "@finos/vuu-utils/test/utils"; -const persistence = new RemotePersistenceManager(); const mockFetch = vi.fn(); global.fetch = mockFetch; +const username = "vuu user"; + +vi.mock("@finos/vuu-shell", async () => { + return { + getAuthDetailsFromCookies: (): [string, string] => { + return [username, "token"]; + }, + }; +}); + +const persistence = new RemotePersistenceManager(); + const metadata: LayoutMetadata = { id: "0001", name: "layout 1", group: "group 1", screenshot: "screenshot", - user: "username", + user: username, created: "01.01.2000", }; @@ -27,7 +38,7 @@ const metadataToAdd: LayoutMetadataDto = { name: "layout 1", group: "group 1", screenshot: "screenshot", - user: "username", + user: username, }; const layout: LayoutJSON = { @@ -44,7 +55,7 @@ type FetchResponse = { statusText?: string; }; -describe("RemoteLayoutPersistenceManager", () => { +describe("RemotePersistenceManager", () => { beforeEach(() => { vi.clearAllMocks(); }); diff --git a/vuu-ui/packages/vuu-table/src/Row.tsx b/vuu-ui/packages/vuu-table/src/Row.tsx index 1309e484d..122b9f09f 100644 --- a/vuu-ui/packages/vuu-table/src/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/Row.tsx @@ -92,7 +92,6 @@ export const Row = memo( return (
; + /** * Selection Bookends style the left and right edge of a selection block. * They are optional, value defaults to zero. @@ -132,8 +138,9 @@ const TableCore = ({ onRowClick: onRowClickProp, onSelect, onSelectionChange, - renderBufferSize = 0, + renderBufferSize = 5, rowHeight = 20, + scrollingApiRef, selectionModel = "extended", showColumnHeaders = true, headerHeight = showColumnHeaders ? 25 : 0, @@ -148,6 +155,7 @@ const TableCore = ({ columns, data, draggableRow, + getRowOffset, handleContextMenuAction, headings, highlightedIndex, @@ -187,11 +195,12 @@ const TableCore = ({ onSelectionChange, renderBufferSize, rowHeight, + scrollingApiRef, selectionModel, size, }); - const className = cx(`${classBase}-contentContainer`, { + const contentContainerClassName = cx(`${classBase}-contentContainer`, { [`${classBase}-colLines`]: tableAttributes.columnSeparators, [`${classBase}-rowLines`]: tableAttributes.rowSeparators, // [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, @@ -225,7 +234,7 @@ const TableCore = ({
@@ -251,6 +260,7 @@ const TableCore = ({
{data.map((data) => ( @@ -296,6 +304,7 @@ export const Table = forwardRef(function TableNext( onSelectionChange, renderBufferSize, rowHeight, + scrollingApiRef, selectionModel, showColumnHeaders, headerHeight, @@ -349,6 +358,7 @@ export const Table = forwardRef(function TableNext( onSelectionChange={onSelectionChange} renderBufferSize={renderBufferSize} rowHeight={rowHeight} + scrollingApiRef={scrollingApiRef} selectionModel={selectionModel} showColumnHeaders={showColumnHeaders} size={size} diff --git a/vuu-ui/packages/vuu-table/src/index.ts b/vuu-ui/packages/vuu-table/src/index.ts index 558444450..db5414018 100644 --- a/vuu-ui/packages/vuu-table/src/index.ts +++ b/vuu-ui/packages/vuu-table/src/index.ts @@ -7,4 +7,5 @@ export * from "./cell-renderers"; export type { RowProps } from "./Row"; export * from "./useControlledTableNavigation"; export * from "./useTableModel"; +export * from "./useTableScroll"; export * from "./useTableViewport"; diff --git a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts index da10cb990..94b547439 100644 --- a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts @@ -1,12 +1,15 @@ import { RefObject } from "react"; +/** + * [rowIndex, colIndex + */ export type CellPos = [number, number]; export const headerCellQuery = (colIdx: number) => `.vuuTable-col-headers .vuuTableHeaderCell:nth-child(${colIdx})`; export const dataCellQuery = (rowIdx: number, colIdx: number) => - `.vuuTable-body > [aria-rowindex='${rowIdx}'] > [role='cell']:nth-child(${ + `.vuuTable-body > [aria-rowindex='${rowIdx + 1}'] > [role='cell']:nth-child(${ colIdx + 1 })`; diff --git a/vuu-ui/packages/vuu-table/src/table-header/useTableHeader.ts b/vuu-ui/packages/vuu-table/src/table-header/useTableHeader.ts index 3792f8d68..cc98be81e 100644 --- a/vuu-ui/packages/vuu-table/src/table-header/useTableHeader.ts +++ b/vuu-ui/packages/vuu-table/src/table-header/useTableHeader.ts @@ -1,5 +1,5 @@ import { ColumnDescriptor } from "@finos/vuu-table-types"; -import { useDragDropNext as useDragDrop } from "@finos/vuu-ui-controls"; +import { useDragDrop as useDragDrop } from "@finos/vuu-ui-controls"; import { moveColumnTo, visibleColumnAtIndex } from "@finos/vuu-utils"; import { MouseEventHandler, useCallback, useRef } from "react"; import { TableHeaderProps } from "./TableHeader"; diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index d220b00a9..078ebb9e4 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -13,7 +13,6 @@ import { MovingWindow } from "./moving-window"; export interface DataSourceHookProps { dataSource: DataSource; - // onConfigChange?: (message: DataSourceConfigMessage) => void; onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; onSizeChange: (size: number) => void; onSubscribed: (subscription: DataSourceSubscribedMessage) => void; @@ -38,7 +37,6 @@ export const useDataSource = ({ const data = useRef([]); const isMounted = useRef(true); const hasUpdated = useRef(false); - // const rafHandle = useRef(null); const rangeRef = useRef(NULL_RANGE); const dataWindow = useMemo( @@ -100,21 +98,6 @@ export const useDataSource = ({ }; }, [dataSource]); - // Keep until we'tre sure we don't need it for updates - // const refreshIfUpdated = useCallback(() => { - // if (isMounted.current) { - // console.log(`RAF updated data ? ${hasUpdated.current}`); - // if (hasUpdated.current) { - // forceUpdate({}); - // hasUpdated.current = false; - // } - // rafHandle.current = requestAnimationFrame(refreshIfUpdated); - // } - // }, [forceUpdate]); - // useEffect(() => { - // rafHandle.current = requestAnimationFrame(refreshIfUpdated); - // }, [refreshIfUpdated]); - useEffect(() => { if (dataSource.status === "disabled") { dataSource.enable?.(datasourceMessageHandler); @@ -129,6 +112,8 @@ export const useDataSource = ({ const setRange = useCallback( (range: VuuRange) => { + // TODO can we directly call setData here when we do an + // in-situ row scroll ? const fullRange = getFullRange(range, renderBufferSize); dataWindow.setRange(fullRange); dataSource.range = rangeRef.current = fullRange; diff --git a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts index 48861ec3d..b7d4acb03 100644 --- a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts @@ -1,5 +1,6 @@ -import { useControlled } from "@salt-ds/core"; import { VuuRange } from "@finos/vuu-protocol-types"; +import { getIndexFromRowElement } from "@finos/vuu-utils"; +import { useControlled } from "@salt-ds/core"; import { KeyboardEvent, MouseEvent, @@ -8,7 +9,7 @@ import { useEffect, useRef, } from "react"; -import { ScrollDirection, ScrollRequestHandler } from "./useTableScroll"; +import { TableNavigationStyle } from "./Table"; import { CellPos, closestRowIndex, @@ -16,7 +17,7 @@ import { getTableCell, headerCellQuery, } from "./table-dom-utils"; -import { TableNavigationStyle } from "./Table"; +import { ScrollDirection, ScrollRequestHandler } from "./useTableScroll"; const rowNavigationKeys = new Set([ "Home", @@ -215,9 +216,9 @@ NavigationHookProps) => { const colIdx = parseInt(tableCell.dataset.idx ?? "-1", 10); return [-1, colIdx]; } else { - const focusedRow = tableCell.closest("[role='row']"); + const focusedRow = tableCell.closest("[role='row']") as HTMLElement; if (focusedRow) { - const rowIdx = parseInt(focusedRow.ariaRowIndex ?? "-1", 10); + const rowIdx = getIndexFromRowElement(focusedRow); // TODO will get trickier when we introduce horizontal virtualisation const colIdx = Array.from(focusedRow.childNodes).indexOf(tableCell); return [rowIdx, colIdx]; @@ -236,11 +237,9 @@ NavigationHookProps) => { focusableCell.current = activeCell; activeCell.setAttribute("tabindex", "0"); } - const [direction, distance] = howFarIsCellOutsideViewport(activeCell); - if (direction && distance) { - requestScroll?.({ type: "scroll-distance", distance, direction }); - } - console.log(`activeCell focus`); + // TODO needs to be scroll cell + console.log(`scroll row ${cellPos[0]}`); + requestScroll?.({ type: "scroll-row", rowIndex: cellPos[0] }); activeCell.focus({ preventScroll: true }); } } @@ -336,18 +335,9 @@ NavigationHookProps) => { const scrollRowIntoViewIfNecessary = useCallback( (rowIndex: number) => { - const { current: container } = containerRef; - const activeRow = container?.querySelector( - `[aria-rowindex="${rowIndex}"]` - ) as HTMLElement; - if (activeRow) { - const [direction, distance] = howFarIsRowOutsideViewport(activeRow); - if (direction && distance) { - requestScroll?.({ type: "scroll-distance", distance, direction }); - } - } + requestScroll?.({ type: "scroll-row", rowIndex }); }, - [containerRef, requestScroll] + [requestScroll] ); const moveHighlightedRow = useCallback( @@ -358,6 +348,7 @@ NavigationHookProps) => { : nextCellPos(key, [highlighted ?? -1, 0], columnCount, rowCount); if (nextRowIdx !== highlighted) { setHighlightedIndex(nextRowIdx); + // TO(DO make this a scroll request) scrollRowIntoViewIfNecessary(nextRowIdx); } }, diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index 061cce244..35a91b5c6 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -17,7 +17,7 @@ import { MeasuredProps, MeasuredSize } from "@finos/vuu-layout"; import { VuuRange, VuuSortType } from "@finos/vuu-protocol-types"; import { DragStartHandler, - useDragDropNext as useDragDrop, + useDragDrop as useDragDrop, } from "@finos/vuu-ui-controls"; import { applySort, @@ -61,7 +61,6 @@ import { } from "./useTableModel"; import { useTableScroll } from "./useTableScroll"; import { useTableViewport } from "./useTableViewport"; -import { useVirtualViewport } from "./useVirtualViewport"; import { useTableAndColumnSettings } from "./useTableAndColumnSettings"; const stripInternalProperties = (tableConfig: TableConfig): TableConfig => { @@ -90,6 +89,7 @@ export interface TableHookProps | "onSelectionChange" | "onRowClick" | "renderBufferSize" + | "scrollingApiRef" > { containerRef: RefObject; headerHeight: number; @@ -136,6 +136,7 @@ export const useTable = ({ onSelectionChange, renderBufferSize = 0, rowHeight = 20, + scrollingApiRef, selectionModel, size, }: TableHookProps) => { @@ -199,13 +200,6 @@ export const useTable = ({ return [stateColumns ?? runtimeColumns, setSize]; }, [runtimeColumns, stateColumns]); - // console.log({ - // config, - // tableConfig, - // runtimeColumns, - // columns, - // }); - const columnMap = useMemo( () => buildColumnMap(dataSource.columns), [dataSource.columns] @@ -227,7 +221,10 @@ export const useTable = ({ const initialRange = useInitialValue({ from: 0, - to: viewportMeasurements.rowCount, + to: + viewportMeasurements.rowCount === 0 + ? 0 + : viewportMeasurements.rowCount + 1, }); const onSubscribed = useCallback( @@ -256,7 +253,6 @@ export const useTable = ({ const handleConfigEditedInSettingsPanel = useCallback( (tableConfig: TableConfig) => { - console.log(`settings changed`); dispatchColumnAction({ type: "init", tableConfig, @@ -466,26 +462,20 @@ export const useTable = ({ [columns, dataSource, dispatchColumnAction] ); - const { onVerticalScroll } = useVirtualViewport({ - columns, - getRowAtPosition, - setRange, - viewportMeasurements, - }); - const handleVerticalScroll = useCallback( - (scrollTop: number) => { - onVerticalScroll(scrollTop); + (_: number, pctScrollTop: number) => { + setPctScrollTop(pctScrollTop); }, - [onVerticalScroll] + [setPctScrollTop] ); const { requestScroll, ...scrollProps } = useTableScroll({ - maxScrollLeft: viewportMeasurements.maxScrollContainerScrollHorizontal, - maxScrollTop: viewportMeasurements.maxScrollContainerScrollVertical, + getRowAtPosition, rowHeight, + scrollingApiRef, + setRange, onVerticalScroll: handleVerticalScroll, - viewportRowCount: viewportMeasurements.rowCount, + viewportMeasurements, }); const { @@ -663,11 +653,6 @@ export const useTable = ({ itemQuery: ".vuuTableRow", }); - // console.log({ - // tableAttributes, - // config: tableConfig, - // }); - return { ...containerProps, "aria-rowcount": dataSource.size, @@ -680,6 +665,7 @@ export const useTable = ({ columnMap, columns, data, + getRowOffset, handleContextMenuAction, headings, highlightedIndex: highlightedIndexRef.current, diff --git a/vuu-ui/packages/vuu-table/src/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/useTableScroll.ts index ef47a7469..180a6fc10 100644 --- a/vuu-ui/packages/vuu-table/src/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/useTableScroll.ts @@ -1,4 +1,14 @@ -import { useCallback, useRef } from "react"; +import { getRowElementAtIndex, RowAtPositionFunc } from "@finos/vuu-utils"; +import { VuuRange } from "@finos/vuu-protocol-types"; +import { + ForwardedRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react"; +import { ViewportMeasurements } from "./useTableViewport"; export type ScrollDirectionVertical = "up" | "down"; export type ScrollDirectionHorizontal = "left" | "right"; @@ -6,6 +16,13 @@ export type ScrollDirection = | ScrollDirectionVertical | ScrollDirectionHorizontal; +/** + * scroll into view the row at given index posiiton. + */ +export interface ScrollRequestRow { + rowIndex: number; + type: "scroll-row"; +} export interface ScrollRequestEnd { type: "scroll-end"; direction: "home" | "end"; @@ -16,19 +33,18 @@ export interface ScrollRequestPage { direction: ScrollDirectionVertical; } -export interface ScrollRequestDistance { - direction: ScrollDirection; - type: "scroll-distance"; - distance: number; -} - export type ScrollRequest = | ScrollRequestPage - | ScrollRequestDistance - | ScrollRequestEnd; + | ScrollRequestEnd + | ScrollRequestRow; export type ScrollRequestHandler = (request: ScrollRequest) => void; +export interface ScrollingAPI { + scrollToIndex: (itemIndex: number) => void; + scrollToKey: (rowKey: string) => void; +} + const getPctScroll = (container: HTMLElement) => { const { scrollLeft, scrollTop } = container; const { clientHeight, clientWidth, scrollHeight, scrollWidth } = container; @@ -37,12 +53,46 @@ const getPctScroll = (container: HTMLElement) => { return [pctScrollLeft, pctScrollTop]; }; +export const noScrolling: ScrollingAPI = { + scrollToIndex: () => undefined, + scrollToKey: () => undefined, +}; + interface CallbackRefHookProps { onAttach?: (el: T) => void; onDetach: (el: T) => void; label?: string; } +const NO_SCROLL_NECESSARY = [undefined, undefined] as const; + +export const howFarIsRowOutsideViewport = ( + rowEl: HTMLElement, + totalHeaderHeight: number, + contentContainer = rowEl.closest(".vuuTable-contentContainer") +): readonly [ScrollDirection | undefined, number | undefined] => { + //TODO lots of scope for optimisation here + if (contentContainer) { + // TODO take totalHeaderHeight into consideration + const viewport = contentContainer?.getBoundingClientRect(); + const upperBoundary = viewport.top + totalHeaderHeight; + const row = rowEl.getBoundingClientRect(); + if (row) { + if (row.bottom > viewport.bottom) { + return ["down", row.bottom - viewport.bottom]; + } else if (row.top < upperBoundary) { + return ["up", row.top - upperBoundary]; + } else { + return NO_SCROLL_NECESSARY; + } + } else { + throw Error("Whats going on, row not found"); + } + } else { + throw Error("Whats going on, scrollbar container not found"); + } +}; + const useCallbackRef = ({ onAttach, onDetach, @@ -65,27 +115,50 @@ const useCallbackRef = ({ }; export interface TableScrollHookProps { - maxScrollLeft: number; - maxScrollTop: number; + getRowAtPosition: RowAtPositionFunc; onHorizontalScroll?: (scrollLeft: number) => void; onVerticalScroll?: (scrollTop: number, pctScrollTop: number) => void; rowHeight: number; - viewportRowCount: number; + scrollingApiRef?: ForwardedRef; + setRange: (range: VuuRange) => void; + viewportMeasurements: ViewportMeasurements; } export const useTableScroll = ({ - maxScrollLeft, - maxScrollTop, + getRowAtPosition, onHorizontalScroll, onVerticalScroll, - rowHeight, - viewportRowCount, + scrollingApiRef, + setRange, + viewportMeasurements, }: TableScrollHookProps) => { + const firstRowRef = useRef(0); const contentContainerScrolledRef = useRef(false); const scrollPosRef = useRef({ scrollTop: 0, scrollLeft: 0 }); const scrollbarContainerRef = useRef(null); const contentContainerRef = useRef(null); + const { + appliedPageSize, + isVirtualScroll, + maxScrollContainerScrollHorizontal: maxScrollLeft, + maxScrollContainerScrollVertical: maxScrollTop, + rowCount: viewportRowCount, + totalHeaderHeight, + } = viewportMeasurements; + + const handleVerticalScroll = useCallback( + (scrollTop: number, pctScrollTop: number) => { + onVerticalScroll?.(scrollTop, pctScrollTop); + const firstRow = getRowAtPosition(scrollTop); + if (firstRow !== firstRowRef.current) { + firstRowRef.current = firstRow; + setRange({ from: firstRow, to: firstRow + viewportRowCount + 1 }); + } + }, + [getRowAtPosition, onVerticalScroll, setRange, viewportRowCount] + ); + const handleScrollbarContainerScroll = useCallback(() => { const { current: contentContainer } = contentContainerRef; const { current: scrollbarContainer } = scrollbarContainerRef; @@ -95,7 +168,7 @@ export const useTableScroll = ({ } else if (contentContainer && scrollbarContainer) { const [pctScrollLeft, pctScrollTop] = getPctScroll(scrollbarContainer); const rootScrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - const rootScrollTop = Math.round(pctScrollTop * maxScrollTop); + const rootScrollTop = pctScrollTop * maxScrollTop; contentContainer.scrollTo({ left: rootScrollLeft, top: rootScrollTop, @@ -113,20 +186,19 @@ export const useTableScroll = ({ const { scrollLeft, scrollTop } = contentContainer; const [pctScrollLeft, pctScrollTop] = getPctScroll(contentContainer); contentContainerScrolledRef.current = true; - scrollbarContainer.scrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - scrollbarContainer.scrollTop = Math.round(pctScrollTop * maxScrollTop); + scrollbarContainer.scrollTop = pctScrollTop * maxScrollTop; if (scrollPos.scrollTop !== scrollTop) { scrollPos.scrollTop = scrollTop; - onVerticalScroll?.(scrollTop, pctScrollTop); + handleVerticalScroll(scrollTop, pctScrollTop); } if (scrollPos.scrollLeft !== scrollLeft) { scrollPos.scrollLeft = scrollLeft; onHorizontalScroll?.(scrollLeft); } } - }, [maxScrollLeft, maxScrollTop, onHorizontalScroll, onVerticalScroll]); + }, [handleVerticalScroll, maxScrollLeft, maxScrollTop, onHorizontalScroll]); const handleAttachScrollbarContainer = useCallback( (el: HTMLDivElement) => { @@ -174,48 +246,70 @@ export const useTableScroll = ({ onDetach: handleDetachScrollbarContainer, }); - //TODO should this be async ? const requestScroll: ScrollRequestHandler = useCallback( (scrollRequest) => { const { current: scrollbarContainer } = contentContainerRef; if (scrollbarContainer) { const { scrollLeft, scrollTop } = scrollbarContainer; contentContainerScrolledRef.current = false; - if (scrollRequest.type === "scroll-distance") { - let newScrollLeft = scrollLeft; - let newScrollTop = scrollTop; - if ( - scrollRequest.direction === "up" || - scrollRequest.direction === "down" - ) { - newScrollTop = Math.min( - Math.max(0, scrollTop + scrollRequest.distance), - maxScrollTop - ); - } else { - newScrollLeft = Math.min( - Math.max(0, scrollLeft + scrollRequest.distance), - maxScrollLeft + if (scrollRequest.type === "scroll-row") { + const activeRow = getRowElementAtIndex( + scrollbarContainer, + scrollRequest.rowIndex + ); + if (activeRow !== null) { + const [direction, distance] = howFarIsRowOutsideViewport( + activeRow, + totalHeaderHeight ); + if (direction && distance) { + if (isVirtualScroll) { + console.log( + `virtual scroll row required ${direction} ${distance} + first Row ${firstRowRef.current}` + ); + // const from = firstRowRef.current + 1; + // console.log(`setRange from ${from}`); + // setRange({ from, to: from + viewportRowCount + 1 }); + } else { + let newScrollLeft = scrollLeft; + let newScrollTop = scrollTop; + if (direction === "up" || direction === "down") { + newScrollTop = Math.min( + Math.max(0, scrollTop + distance), + maxScrollTop + ); + } else { + newScrollLeft = Math.min( + Math.max(0, scrollLeft + distance), + maxScrollLeft + ); + } + scrollbarContainer.scrollTo({ + top: newScrollTop, + left: newScrollLeft, + behavior: "smooth", + }); + } + } } - scrollbarContainer.scrollTo({ - top: newScrollTop, - left: newScrollLeft, - behavior: "smooth", - }); } else if (scrollRequest.type === "scroll-page") { const { direction } = scrollRequest; - const scrollBy = - viewportRowCount * (direction === "down" ? rowHeight : -rowHeight); - const newScrollTop = Math.min( - Math.max(0, scrollTop + scrollBy), - maxScrollTop - ); - scrollbarContainer.scrollTo({ - top: newScrollTop, - left: scrollLeft, - behavior: "auto", - }); + if (isVirtualScroll) { + console.log(`need a virtual page scroll`); + } else { + const scrollBy = + direction === "down" ? appliedPageSize : -appliedPageSize; + const newScrollTop = Math.min( + Math.max(0, scrollTop + scrollBy), + maxScrollTop + ); + scrollbarContainer.scrollTo({ + top: newScrollTop, + left: scrollLeft, + behavior: "auto", + }); + } } else if (scrollRequest.type === "scroll-end") { const { direction } = scrollRequest; const scrollTo = direction === "end" ? maxScrollTop : 0; @@ -227,9 +321,50 @@ export const useTableScroll = ({ } } }, - [maxScrollLeft, maxScrollTop, rowHeight, viewportRowCount] + [ + appliedPageSize, + isVirtualScroll, + maxScrollLeft, + maxScrollTop, + setRange, + totalHeaderHeight, + viewportRowCount, + ] + ); + + const scrollHandles: ScrollingAPI = useMemo( + () => ({ + scrollToIndex: (rowIndex: number) => { + if (scrollbarContainerRef.current) { + const scrollPos = (rowIndex - 30) * 20; + scrollbarContainerRef.current.scrollTop = scrollPos; + } + }, + scrollToKey: (rowKey: string) => { + console.log(`scrollToKey ${rowKey}`); + }, + }), + [] + ); + + useImperativeHandle( + scrollingApiRef, + () => { + if (scrollbarContainerRef.current) { + return scrollHandles; + } else { + return noScrolling; + } + }, + [scrollHandles] ); + useEffect(() => { + const { current: from } = firstRowRef; + const rowRange = { from, to: from + viewportRowCount + 1 }; + setRange(rowRange); + }, [setRange, viewportRowCount]); + return { /** Ref to be assigned to ScrollbarContainer */ scrollbarContainerRef: scrollbarContainerCallbackRef, diff --git a/vuu-ui/packages/vuu-table/src/useTableViewport.ts b/vuu-ui/packages/vuu-table/src/useTableViewport.ts index 18f7e6f2d..2c8745b8e 100644 --- a/vuu-ui/packages/vuu-table/src/useTableViewport.ts +++ b/vuu-ui/packages/vuu-table/src/useTableViewport.ts @@ -24,8 +24,10 @@ export interface TableViewportHookProps { } export interface ViewportMeasurements { + appliedPageSize: number; contentHeight: number; horizontalScrollbarHeight: number; + isVirtualScroll: boolean; maxScrollContainerScrollHorizontal: number; maxScrollContainerScrollVertical: number; pinnedWidthLeft: number; @@ -44,14 +46,17 @@ export interface TableViewportHookResult extends ViewportMeasurements { } // Too simplistic, it depends on rowHeight -const MAX_RAW_ROWS = 1_500_000; +// const MAX_RAW_ROWS = 1_000_000; +const MAX_RAW_ROWS = 100_000; const UNMEASURED_VIEWPORT: TableViewportHookResult = { + appliedPageSize: 0, contentHeight: 0, contentWidth: 0, getRowAtPosition: () => -1, getRowOffset: () => -1, horizontalScrollbarHeight: 0, + isVirtualScroll: false, maxScrollContainerScrollHorizontal: 0, maxScrollContainerScrollVertical: 0, pinnedWidthLeft: 0, @@ -94,32 +99,33 @@ export const useTableViewport = ({ size, }: TableViewportHookProps): TableViewportHookResult => { const pctScrollTopRef = useRef(0); - const appliedRowCount = Math.min(rowCount, MAX_RAW_ROWS); - const appliedContentHeight = appliedRowCount * rowHeight; + // TODO we are limited by pixels not an arbitraty number of rows + const pixelContentHeight = rowHeight * Math.min(rowCount, MAX_RAW_ROWS); const virtualContentHeight = rowCount * rowHeight; - const virtualisedExtent = virtualContentHeight - appliedContentHeight; + const virtualisedExtent = virtualContentHeight - pixelContentHeight; const { pinnedWidthLeft, pinnedWidthRight, unpinnedWidth } = useMemo( () => measurePinnedColumns(columns), [columns] ); - const [actualRowOffset, actualRowAtPosition] = useMemo( - () => actualRowPositioning(rowHeight), - [rowHeight] - ); + const totalHeaderHeightRef = useRef(headerHeight); + useMemo(() => { + totalHeaderHeightRef.current = headerHeight * (1 + headings.length); + }, [headerHeight, headings.length]); - const [getRowOffset, getRowAtPosition] = useMemo(() => { - if (virtualisedExtent) { - return virtualRowPositioning( - rowHeight, - virtualisedExtent, - pctScrollTopRef - ); - } else { - return [actualRowOffset, actualRowAtPosition]; - } - }, [actualRowAtPosition, actualRowOffset, virtualisedExtent, rowHeight]); + const [getRowOffset, getRowAtPosition, isVirtualScroll] = + useMemo(() => { + if (virtualisedExtent) { + return virtualRowPositioning( + rowHeight, + virtualisedExtent, + pctScrollTopRef + ); + } else { + return actualRowPositioning(rowHeight); + } + }, [virtualisedExtent, rowHeight]); const setPctScrollTop = useCallback((scrollPct: number) => { pctScrollTopRef.current = scrollPct; @@ -127,37 +133,42 @@ export const useTableViewport = ({ return useMemo(() => { if (size) { - const headingsDepth = headings.length; + const { current: totalHeaderHeight } = totalHeaderHeightRef; + // TODO determine this at runtime const scrollbarSize = 15; const contentWidth = pinnedWidthLeft + unpinnedWidth + pinnedWidthRight; const horizontalScrollbarHeight = contentWidth > size.width ? scrollbarSize : 0; - const totalHeaderHeight = headerHeight * (1 + headingsDepth); const maxScrollContainerScrollVertical = - appliedContentHeight - + pixelContentHeight - ((size?.height ?? 0) - horizontalScrollbarHeight) + totalHeaderHeight; const maxScrollContainerScrollHorizontal = contentWidth - size.width + pinnedWidthLeft; const visibleRows = (size.height - headerHeight) / rowHeight; const count = Number.isInteger(visibleRows) - ? visibleRows + 1 + ? visibleRows : Math.ceil(visibleRows); const viewportBodyHeight = size.height - totalHeaderHeight; const verticalScrollbarWidth = - appliedContentHeight > viewportBodyHeight ? scrollbarSize : 0; + pixelContentHeight > viewportBodyHeight ? scrollbarSize : 0; + + const appliedPageSize = + count * rowHeight * (pixelContentHeight / virtualContentHeight); return { - contentHeight: appliedContentHeight, + appliedPageSize, + contentHeight: pixelContentHeight, + contentWidth, getRowAtPosition, getRowOffset, + isVirtualScroll, horizontalScrollbarHeight, maxScrollContainerScrollHorizontal, maxScrollContainerScrollVertical, pinnedWidthLeft, pinnedWidthRight, rowCount: count, - contentWidth, setPctScrollTop, totalHeaderHeight, verticalScrollbarWidth, @@ -167,16 +178,17 @@ export const useTableViewport = ({ return UNMEASURED_VIEWPORT; } }, [ - size, - headings.length, + getRowAtPosition, + getRowOffset, + headerHeight, + isVirtualScroll, pinnedWidthLeft, unpinnedWidth, pinnedWidthRight, - appliedContentHeight, - headerHeight, + pixelContentHeight, rowHeight, - getRowAtPosition, - getRowOffset, setPctScrollTop, + size, + virtualContentHeight, ]); }; diff --git a/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts b/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts deleted file mode 100644 index b95486ccf..000000000 --- a/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { RuntimeColumnDescriptor } from "@finos/vuu-table-types"; -import { VuuRange } from "@finos/vuu-protocol-types"; -import { RowAtPositionFunc } from "@finos/vuu-utils"; -import { useCallback, useEffect, useRef } from "react"; -import { ViewportMeasurements } from "./useTableViewport"; - -export interface VirtualViewportHookProps { - columns: RuntimeColumnDescriptor[]; - getRowAtPosition: RowAtPositionFunc; - setRange: (range: VuuRange) => void; - viewportMeasurements: ViewportMeasurements; -} - -export const useVirtualViewport = ({ - getRowAtPosition, - setRange, - viewportMeasurements, -}: VirtualViewportHookProps) => { - const firstRowRef = useRef(0); - const { rowCount: viewportRowCount } = viewportMeasurements; - - const handleVerticalScroll = useCallback( - (scrollTop: number) => { - const firstRow = getRowAtPosition(scrollTop); - if (firstRow !== firstRowRef.current) { - firstRowRef.current = firstRow; - setRange({ from: firstRow, to: firstRow + viewportRowCount }); - } - }, - [getRowAtPosition, setRange, viewportRowCount] - ); - - useEffect(() => { - const { current: from } = firstRowRef; - const rowRange = { from, to: from + viewportRowCount }; - setRange(rowRange); - }, [setRange, viewportRowCount]); - - return { - onVerticalScroll: handleVerticalScroll, - }; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/package.json b/vuu-ui/packages/vuu-ui-controls/package.json index 02ccb0c13..51c8ac421 100644 --- a/vuu-ui/packages/vuu-ui-controls/package.json +++ b/vuu-ui/packages/vuu-ui-controls/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@salt-ds/core": "1.13.2", + "@salt-ds/icons": "1.9.1", "@finos/vuu-data-types": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-layout": "0.0.26", diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.css new file mode 100644 index 000000000..153c3e2a6 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.css @@ -0,0 +1,7 @@ +.saltCalendar { + width: min-content; +} + +.saltCalendar .saltIcon { + display: inline-block; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.tsx new file mode 100644 index 000000000..81b1f49cd --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/Calendar.tsx @@ -0,0 +1,74 @@ +import { forwardRef, useCallback } from "react"; +import { clsx } from "clsx"; +import { makePrefixer } from "@salt-ds/core"; +import { + CalendarNavigation, + CalendarNavigationProps, +} from "./internal/CalendarNavigation"; +import { CalendarWeekHeader } from "./internal/CalendarWeekHeader"; +import { + CalendarCarousel, + CalendarCarouselProps, +} from "./internal/CalendarCarousel"; +import { CalendarContext } from "./internal/CalendarContext"; +import { useCalendar, useCalendarProps } from "./useCalendar"; + +import "./Calendar.css"; + +export type CalendarProps = useCalendarProps & { + className?: string; + renderDayContents?: CalendarCarouselProps["renderDayContents"]; + hideYearDropdown?: CalendarNavigationProps["hideYearDropdown"]; + TooltipProps?: CalendarCarouselProps["TooltipProps"]; + hideOutOfRangeDates?: CalendarCarouselProps["hideOutOfRangeDates"]; +}; + +const withBaseName = makePrefixer("saltCalendar"); + +export const Calendar = forwardRef( + function Calendar(props, ref) { + const { + className, + renderDayContents, + hideYearDropdown, + TooltipProps, + ...rest + } = props; + + const { state, helpers } = useCalendar({ hideYearDropdown, ...rest }); + + const { setCalendarFocused } = helpers; + + const handleFocus = useCallback(() => { + setCalendarFocused(true); + }, [setCalendarFocused]); + + const handleBlur = useCallback(() => { + setCalendarFocused(false); + }, [setCalendarFocused]); + + return ( + +
+ + + +
+
+ ); + } +); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/index.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/index.ts new file mode 100644 index 000000000..43197d908 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/index.ts @@ -0,0 +1,4 @@ +export * from "./Calendar"; +export * from "./useCalendarDay"; +export * from "./useCalendar"; +export * from "./useSelection"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.css new file mode 100644 index 000000000..a610fbf78 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.css @@ -0,0 +1,18 @@ +.saltCalendarCarousel-track { + display: grid; + grid-auto-flow: column; +} + +.saltCalendarCarousel-track > * { + position: absolute; + left: 0; + width: 100%; +} + +.saltCalendarCarousel-track > :nth-child(2) { + position: relative; +} + +.saltCalendarCarousel-shouldAnimate { + transition: transform 200ms ease-in-out; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.tsx new file mode 100644 index 000000000..9fafde7ca --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarCarousel.tsx @@ -0,0 +1,121 @@ +import { forwardRef, useEffect, useRef, useState } from "react"; +import { + DateValue, + getLocalTimeZone, + isSameMonth, + today, +} from "@internationalized/date"; +import { CalendarMonth, CalendarMonthProps } from "./CalendarMonth"; +import { + makePrefixer, + useIsomorphicLayoutEffect, + usePrevious, +} from "@salt-ds/core"; +import { useCalendarContext } from "./CalendarContext"; + +import "./CalendarCarousel.css"; +import { formatDate, monthDiff } from "./utils"; + +export type CalendarCarouselProps = Omit; + +function getMonths(month: DateValue) { + return [month.subtract({ months: 1 }), month, month.add({ months: 1 })]; +} + +function usePreviousMonth(visibleMonth: DateValue) { + const previous = usePrevious(visibleMonth, [formatDate(visibleMonth)]); + + return previous ?? today(getLocalTimeZone()); +} + +const withBaseName = makePrefixer("saltCalendarCarousel"); + +export const CalendarCarousel = forwardRef< + HTMLDivElement, + CalendarCarouselProps +>(function CalendarCarousel(props, ref) { + const { ...rest } = props; + + const { + state: { visibleMonth }, + } = useCalendarContext(); + const containerRef = useRef(null); + const diffIndex = (a: DateValue, b: DateValue) => monthDiff(a, b); + + const { current: baseIndex } = useRef(visibleMonth); + const previousVisibleMonth = usePreviousMonth(visibleMonth); + + useIsomorphicLayoutEffect(() => { + if (Math.abs(diffIndex(visibleMonth, previousVisibleMonth)) > 1) { + containerRef.current?.classList.remove(withBaseName("shouldAnimate")); + } else { + containerRef.current?.classList.add(withBaseName("shouldAnimate")); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formatDate(visibleMonth), formatDate(previousVisibleMonth)]); + + useIsomorphicLayoutEffect(() => { + if (containerRef.current) { + containerRef.current.style.transform = `translate3d(${ + diffIndex(baseIndex, visibleMonth) * 100 + }%, 0, 0)`; + } + }); + + const [months, setMonths] = useState(() => getMonths(visibleMonth)); + + useEffect(() => { + setMonths((oldMonths) => { + const newMonths = getMonths(visibleMonth).filter((month) => { + return !oldMonths.find((oldMonth) => isSameMonth(oldMonth, month)); + }); + + return oldMonths.concat(newMonths); + }); + const finishTransition = () => { + setMonths(getMonths(visibleMonth)); + }; + const container = containerRef.current; + + if ( + container && + parseFloat(window.getComputedStyle(container).transitionDuration) > 0 + ) { + container?.addEventListener("transitionend", finishTransition); + + return () => { + container?.removeEventListener("transitionend", finishTransition); + }; + } else { + finishTransition(); + } + + return undefined; + }, [formatDate(visibleMonth)]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+
+ {months.map((date, index) => ( +
+ +
+ ))} +
+
+ ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarContext.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarContext.tsx new file mode 100644 index 000000000..33c6bae2f --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarContext.tsx @@ -0,0 +1,25 @@ +import { createContext, useContext } from "react"; +import { useCalendar } from "../useCalendar"; + +type CalendarState = { + state: ReturnType["state"]; + helpers: ReturnType["helpers"]; +}; + +const CalendarContext = createContext(null); + +if (process.env.NODE_ENV !== "production") { + CalendarContext.displayName = "CalendarContext"; +} + +function useCalendarContext(): CalendarState { + const context = useContext(CalendarContext); + + if (!context) { + throw new Error("Unexpected usage"); + } + + return context; +} + +export { CalendarContext, useCalendarContext }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.css new file mode 100644 index 000000000..9740b780f --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.css @@ -0,0 +1,165 @@ +/* TODO: Design need to align characteristics for CalendarDay */ +.saltCalendarDay { + --calendar-day-text-color: var(--salt-content-primary-foreground); + --calendar-day-background: var(--salt-selectable-background); + --calendar-day-background-hover: var(--salt-selectable-background-hover); + --calendar-day-text-color-hover: var(--salt-content-primary-foreground); + --calendar-day-outOfRange-text-color: var(--salt-content-secondary-foreground-disabled); + --calendar-day-blocked-text-color: var(--salt-content-primary-foreground); + --calendar-day-blocked-cursor: var(--salt-selectable-cursor-disabled); + --calendar-day-blocked-icon-color: var(--salt-status-error-foreground); + --calendar-day-blocked-background: var(--calendar-day-background); + --calendar-day-unselectable-text-color: var(--salt-content-secondary-foreground-disabled); + --calendar-day-unselectable-background: var(--calendar-day-background); + --calendar-day-unselectable-cursor: var(--salt-selectable-cursor-disabled); + + --calendar-day-selected-background: var(--salt-selectable-background-selected); + --calendar-day-selected-text-color: var(--salt-content-primary-foreground); + /* --calendar-day-selected-focused-outlineColor: var(--salt-color-white); TODO: Check with design */ + + --calendar-day-selectedStart-background: var(--salt-selectable-background-selected); + --calendar-day-selectedStart-text-color: var(--salt-content-primary-foreground); + /* --calendar-day-selectedStart-focused-outlineColor: var(--salt-color-white); TODO: Check with design */ + + --calendar-day-selectedEnd-background: var(--salt-selectable-background-selected); + --calendar-day-selectedEnd-text-color: var(--salt-content-primary-foreground); + /* --calendar-day-selectedEnd-focused-outlineColor: var(--salt-color-white); TODO: Check with design */ + + --calendar-day-selectedSpan-background: var(--salt-selectable-background-blurSelected); + --calendar-day-selectedSpan-text-color: var(--salt-content-primary-foreground); + + --calendar-day-hoveredSpan-background: var(--salt-selectable-background-hover); + --calendar-day-hoveredSpan-text-color: var(--salt-content-primary-foreground); + + --calendar-day-hoveredOffset-background: var(--salt-selectable-background-hover); + --calendar-day-hoveredOffset-text-color: var(--salt-content-primary-foreground); + + --calendar-day-currentDay-borderColor: var(--salt-content-primary-foreground); /* TODO should not be foreground color */ + + /* Focus */ + --calendar-day-focused-outline: var(--salt-focused-outline); + --calendar-day-size: var(--salt-size-base); + --calendar-day-fontSize: var(--salt-text-fontSize); +} + +.saltCalendarDay { + width: var(--calendar-day-size); + height: var(--calendar-day-size); + color: var(--calendar-day-text-color); + background-color: var(--calendar-day-background); + font-size: var(--calendar-day-fontSize); + border: 0; + cursor: pointer; + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +.saltCalendarDay:focus-visible { + outline: var(--calendar-day-focused-outline); + outline-offset: calc(0px - var(--salt-focused-outlineWidth)); +} + +.saltCalendarDay-outOfRange { + color: var(--calendar-day-outOfRange-text-color); +} + +.saltCalendarDay:hover { + background: var(--calendar-day-background-hover); + color: var(--calendar-day-text-color-hover); +} + +.saltCalendarDay-unselectableMedium, +.saltCalendarDay-unselectableMedium:hover { + color: var(--calendar-day-blocked-text-color); + cursor: var(--calendar-day-blocked-cursor); + background: var(--calendar-day-blocked-background); +} + +.saltCalendarDay-today { + border: 1px solid var(--calendar-day-currentDay-borderColor); +} + +.saltCalendarDay-selectedSpan { + background: var(--calendar-day-selectedSpan-background); + color: var(--calendar-day-selectedSpan-text-color); +} + +.saltCalendarDay-hoveredSpan, +.saltCalendarDay-hoveredSpan:hover { + background: var(--calendar-day-hoveredSpan-background); + color: var(--calendar-day-hoveredSpan-text-color); +} + +.saltCalendarDay-hoveredOffset, +.saltCalendarDay-hoveredOffset:hover { + background: var(--calendar-day-hoveredOffset-background); + color: var(--calendar-day-hoveredOffset-text-color); +} + +.saltCalendarDay-selected, +.saltCalendarDay-selected:hover { + background: var(--calendar-day-selected-background); + color: var(--calendar-day-selected-text-color); +} + +.saltCalendarDay-selectedStart, +.saltCalendarDay-selectedStart:hover { + background: var(--calendar-day-selectedStart-background); + color: var(--calendar-day-selectedStart-text-color); +} + +.saltCalendarDay-selectedEnd, +.saltCalendarDay-selectedEnd:hover { + background: var(--calendar-day-selectedEnd-background); + color: var(--calendar-day-selectedEnd-text-color); +} + +.saltCalendarDay-selected:focus-visible { + outline-color: var(--calendar-day-selected-focused-outlineColor); +} + +.saltCalendarDay-selectedStart:focus-visible { + outline-color: var(--calendar-day-selectedStart-focused-outlineColor); +} + +.saltCalendarDay-selectedEnd:focus-visible { + outline-color: var(--calendar-day-selectedEnd-focused-outlineColor); +} + +.saltCalendarDay-unselectableLow, +.saltCalendarDay-unselectableLow:hover { + color: var(--calendar-day-unselectable-text-color); + background: var(--calendar-day-unselectable-background); + cursor: var(--calendar-day-unselectable-cursor); + text-decoration: line-through; +} + +.salt-density-high { + --calendar-day-blocked-icon-size: 14px; +} + +.salt-density-medium { + --calendar-day-blocked-icon-size: 18px; +} + +.salt-density-low { + --calendar-day-blocked-icon-size: 24px; +} + +.salt-density-touch { + --calendar-day-blocked-icon-size: 28px; +} + +.saltCalendarDay-blockedIcon { + fill: var(--calendar-day-blocked-icon-color); + position: absolute; + --icon-size: var(--calendar-day-blocked-icon-size); + pointer-events: none; + line-height: 1.29; +} + +.saltCalendarDay-hidden { + visibility: hidden; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.tsx new file mode 100644 index 000000000..2db437266 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarDay.tsx @@ -0,0 +1,86 @@ +import { makePrefixer, Tooltip, TooltipProps, useForkRef } from "@salt-ds/core"; +import { CloseIcon } from "@salt-ds/icons"; +import { clsx } from "clsx"; +import { ComponentPropsWithRef, forwardRef, ReactElement, useRef } from "react"; +import { DateValue } from "@internationalized/date"; + +import { DayStatus, useCalendarDay } from "../useCalendarDay"; +import "./CalendarDay.css"; +import { formatDate } from "./utils"; + +export type DateFormatter = (day: Date) => string | undefined; + +export interface CalendarDayProps + extends Omit, "children"> { + day: DateValue; + formatDate?: DateFormatter; + renderDayContents?: (date: DateValue, status: DayStatus) => ReactElement; + status?: DayStatus; + month: DateValue; + TooltipProps?: Partial; +} + +const withBaseName = makePrefixer("saltCalendarDay"); + +export const CalendarDay = forwardRef( + function CalendarDay(props, ref) { + const { className, day, renderDayContents, month, TooltipProps, ...rest } = + props; + + const dayRef = useRef(null); + const forkedRef = useForkRef(ref, dayRef); + + const { status, dayProps, unselectableReason } = useCalendarDay( + { + date: day, + month, + }, + dayRef + ); + const { outOfRange, today, unselectable, hidden } = status; + + return ( + + + + ); + } +); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.css new file mode 100644 index 000000000..4a2d7bc3b --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.css @@ -0,0 +1,5 @@ +.saltCalendarMonth-dateGrid { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.tsx new file mode 100644 index 000000000..f6e2799aa --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarMonth.tsx @@ -0,0 +1,75 @@ +import { + ComponentPropsWithRef, + forwardRef, + MouseEvent, + SyntheticEvent, +} from "react"; +import { clsx } from "clsx"; +import { makePrefixer } from "@salt-ds/core"; +import { DateValue } from "@internationalized/date"; +import { CalendarDay, CalendarDayProps } from "./CalendarDay"; +import { formatDate, generateVisibleDays } from "./utils"; + +import "./CalendarMonth.css"; +import { useCalendarContext } from "./CalendarContext"; + +export interface CalendarMonthProps extends ComponentPropsWithRef<"div"> { + date: DateValue; + hideOutOfRangeDates?: boolean; + renderDayContents?: CalendarDayProps["renderDayContents"]; + isVisible?: boolean; + TooltipProps?: CalendarDayProps["TooltipProps"]; +} + +const withBaseName = makePrefixer("saltCalendarMonth"); + +export const CalendarMonth = forwardRef( + function CalendarMonth(props, ref) { + const { + className, + date, + hideOutOfRangeDates, + isVisible, + renderDayContents, + onMouseLeave, + TooltipProps, + ...rest + } = props; + + const days = generateVisibleDays(date); + const { + helpers: { setHoveredDate }, + } = useCalendarContext(); + + const handleMouseLeave = (event: SyntheticEvent) => { + setHoveredDate(event, null); + onMouseLeave?.(event as MouseEvent); + }; + + return ( +
+
+ {days.map((day) => { + return ( + + ); + })} +
+
+ ); + } +); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.css new file mode 100644 index 000000000..7d42e7b4e --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.css @@ -0,0 +1,24 @@ +.salt-density-medium, +.salt-density-touch, +.salt-density-low { + --calendar-navigation-gap: calc(var(--salt-size-unit) * 0.5); +} + +.salt-density-high { + --calendar-navigation-gap: 0px; +} + +.saltCalendarNavigation-hideYearDropdown { + --calendar-navigation-gap: calc(var(--salt-size-unit) * 2); +} + +.saltCalendarNavigation { + display: grid; + grid-template-columns: min-content auto auto min-content; + grid-gap: var(--calendar-navigation-gap); + align-items: center; +} + +.saltCalendarNavigation-hideYearDropdown { + grid-template-columns: min-content auto min-content; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.tsx new file mode 100644 index 000000000..ff5613d55 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarNavigation.tsx @@ -0,0 +1,284 @@ +import { + Button, + ButtonProps, + makePrefixer, + Tooltip, + useId, +} from "@salt-ds/core"; +import { ChevronLeftIcon, ChevronRightIcon } from "@salt-ds/icons"; +import { clsx } from "clsx"; +import { + ComponentPropsWithRef, + forwardRef, + MouseEventHandler, + SyntheticEvent, +} from "react"; +import { Dropdown, DropdownProps } from "../../dropdown"; +import { ListItem, ListItemType } from "../../list"; + +import { useCalendarContext } from "./CalendarContext"; + +import "./CalendarNavigation.css"; +import { DateValue, isSameMonth, isSameYear } from "@internationalized/date"; +import { formatDate, monthDiff, monthsForLocale } from "./utils"; +import { SingleSelectionHandler } from "../../common-hooks"; + +type DropdownItem = { + value: DateValue; + disabled?: boolean; +}; + +type dateDropdownProps = DropdownProps; + +export interface CalendarNavigationProps extends ComponentPropsWithRef<"div"> { + MonthDropdownProps?: dateDropdownProps; + YearDropdownProps?: dateDropdownProps; + onMonthSelect?: dateDropdownProps["onChange"]; + onYearSelect?: dateDropdownProps["onChange"]; + onNavigateNext?: ButtonProps["onClick"]; + onNavigatePrevious?: ButtonProps["onClick"]; + hideYearDropdown?: boolean; +} + +const withBaseName = makePrefixer("saltCalendarNavigation"); + +function useCalendarNavigation() { + const { + state: { visibleMonth, minDate, maxDate }, + helpers: { + setVisibleMonth, + isDayVisible, + isOutsideAllowedYears, + isOutsideAllowedMonths, + }, + } = useCalendarContext(); + + const moveToNextMonth = (event: SyntheticEvent) => { + setVisibleMonth(event, visibleMonth.add({ months: 1 })); + }; + + const moveToPreviousMonth = (event: SyntheticEvent) => { + setVisibleMonth(event, visibleMonth.subtract({ months: 1 })); + }; + + const moveToMonth = (event: SyntheticEvent, month: DateValue) => { + let newMonth = month; + + if (!isOutsideAllowedYears(newMonth)) { + if (isOutsideAllowedMonths(newMonth)) { + // If month is navigable we should move to the closest navigable month + const navigableMonths = monthsForLocale(visibleMonth).filter( + (n) => !isOutsideAllowedMonths(n) + ); + newMonth = navigableMonths.reduce((closestMonth, currentMonth) => + Math.abs(monthDiff(currentMonth, newMonth)) < + Math.abs(monthDiff(closestMonth, newMonth)) + ? currentMonth + : closestMonth + ); + } + setVisibleMonth(event, newMonth); + } + }; + + const months = monthsForLocale(visibleMonth).map((month) => { + return { value: month, disabled: isOutsideAllowedMonths(month) }; + }); + + const years = [-2, -1, 0, 1, 2] + .map((delta) => ({ value: visibleMonth.add({ years: delta }) })) + .filter(({ value }) => !isOutsideAllowedYears(value)); + + const selectedMonth = months.find((item: DropdownItem) => + isSameMonth(item.value, visibleMonth) + ); + const selectedYear = years.find((item: DropdownItem) => + isSameYear(item.value, visibleMonth) + ); + + const canNavigatePrevious = !(minDate && isDayVisible(minDate)); + const canNavigateNext = !(maxDate && isDayVisible(maxDate)); + + return { + moveToNextMonth, + moveToPreviousMonth, + moveToMonth, + visibleMonth, + months, + years, + canNavigateNext, + canNavigatePrevious, + selectedMonth, + selectedYear, + }; +} + +const ListItemWithTooltip: ListItemType = ({ + item, + label, + ...props +}) => ( + + {label} + +); + +export const CalendarNavigation = forwardRef< + HTMLDivElement, + CalendarNavigationProps +>(function CalendarNavigation(props, ref) { + const { + className, + MonthDropdownProps, + YearDropdownProps, + hideYearDropdown, + ...rest + } = props; + + const { + moveToPreviousMonth, + moveToNextMonth, + moveToMonth, + months, + years, + canNavigateNext, + canNavigatePrevious, + visibleMonth, + selectedMonth, + selectedYear, + } = useCalendarNavigation(); + + const handleNavigatePrevious: MouseEventHandler = ( + event + ) => { + moveToPreviousMonth(event); + }; + + const handleNavigateNext: MouseEventHandler = (event) => { + moveToNextMonth(event); + }; + + const handleMonthSelect: SingleSelectionHandler = ( + event, + month + ) => { + if (month && event) { + moveToMonth(event, month.value); + } + }; + + const handleYearSelect: SingleSelectionHandler = ( + event, + year + ) => { + if (year && event) { + moveToMonth(event, year.value); + } + }; + + const monthDropdownId = useId(MonthDropdownProps?.id) || ""; + const monthDropdownLabelledBy = clsx( + MonthDropdownProps?.["aria-labelledby"], + // TODO need a prop on Dropdown to allow buttonId to be passed, should not make assumptions about internal + // id assignment like this + `${monthDropdownId}-control` + ); + + const yearDropdownId = useId(YearDropdownProps?.id) || ""; + const yearDropdownLabelledBy = clsx( + YearDropdownProps?.["aria-labelledby"], + `${yearDropdownId}-control` + ); + + const defaultItemToMonth = (date: DropdownItem) => { + if (hideYearDropdown) { + return formatDate(date.value, { month: "long" }); + } + return formatDate(date.value, { month: "short" }); + }; + + const defaultItemToYear = (date: DropdownItem) => { + return formatDate(date.value, { year: "numeric" }); + }; + + return ( +
+ + + + + source={months} + id={monthDropdownId} + aria-labelledby={monthDropdownLabelledBy} + aria-label="Month Dropdown" + {...MonthDropdownProps} + ListItem={ListItemWithTooltip} + selected={selectedMonth} + itemToString={defaultItemToMonth} + onSelectionChange={handleMonthSelect} + fullWidth + /> + {!hideYearDropdown && ( + + source={years} + id={yearDropdownId} + aria-labelledby={yearDropdownLabelledBy} + aria-label="Year Dropdown" + {...YearDropdownProps} + ListItem={ListItemWithTooltip} + selected={selectedYear} + onSelectionChange={handleYearSelect} + itemToString={defaultItemToYear} + fullWidth + /> + )} + + + +
+ ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.css b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.css new file mode 100644 index 000000000..b99319a73 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.css @@ -0,0 +1,21 @@ +.saltCalendarWeekHeader { + --calendar-week-header-underline-color: var(--salt-separable-secondary-borderColor); + --calendar-week-header-text-color: var(--salt-content-secondary-foreground); + --calendar-week-header-size: var(--salt-size-base); + --calendar-week-header-fontSize: var(--saltCalendar-week-header-fontSize, var(--salt-text-label-fontSize)); +} + +.saltCalendarWeekHeader { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; + box-shadow: inset 0 -1px 0 var(--calendar-week-header-underline-color); +} + +.saltCalendarWeekHeader-dayOfWeek { + width: var(--calendar-week-header-size); + height: var(--calendar-week-header-size); + color: var(--calendar-week-header-text-color); + line-height: var(--calendar-week-header-size); + font-size: var(--calendar-week-header-fontSize); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.tsx b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.tsx new file mode 100644 index 000000000..9101f021f --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/CalendarWeekHeader.tsx @@ -0,0 +1,37 @@ +import { ComponentPropsWithRef, forwardRef } from "react"; +import { clsx } from "clsx"; +import { makePrefixer } from "@salt-ds/core"; +import { daysForLocale } from "./utils"; + +import "./CalendarWeekHeader.css"; + +export type CalendarWeekHeaderProps = ComponentPropsWithRef<"div">; + +const withBaseName = makePrefixer("saltCalendarWeekHeader"); + +export const CalendarWeekHeader = forwardRef< + HTMLDivElement, + CalendarWeekHeaderProps +>(function CalendarWeekHeader({ className, ...rest }, ref) { + const weekdaysShort = daysForLocale("narrow"); + const weekdaysLong = daysForLocale("long"); + + return ( +
+ {weekdaysShort.map((day, index) => ( + + ))} +
+ ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/useFocusManagement.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/useFocusManagement.ts new file mode 100644 index 000000000..8247cb520 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/useFocusManagement.ts @@ -0,0 +1,70 @@ +import { + FocusEventHandler, + KeyboardEventHandler, + MouseEventHandler, +} from "react"; +import { useCalendarContext } from "./CalendarContext"; +import { DateValue, endOfWeek, startOfWeek } from "@internationalized/date"; +import { getCurrentLocale } from "./utils"; + +export function useFocusManagement({ date }: { date: DateValue }) { + const { + helpers: { setFocusedDate }, + } = useCalendarContext(); + const handleClick: MouseEventHandler = (event) => { + setFocusedDate(event, date); + }; + + const handleKeyDown: KeyboardEventHandler = (event) => { + let newDate = date; + switch (event.key) { + case "ArrowUp": + newDate = date.subtract({ weeks: 1 }); + break; + case "ArrowDown": + newDate = date.add({ weeks: 1 }); + break; + case "ArrowLeft": + newDate = date.subtract({ days: 1 }); + break; + case "ArrowRight": + newDate = date.add({ days: 1 }); + break; + case "Home": + newDate = startOfWeek(date, getCurrentLocale()); + break; + case "End": + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO bug in @internationalized/date + newDate = endOfWeek(date, getCurrentLocale()); + break; + case "PageUp": + if (event.shiftKey) { + newDate = date.subtract({ years: 1 }); + } else { + newDate = date.subtract({ months: 1 }); + } + break; + case "PageDown": + if (event.shiftKey) { + newDate = date.add({ years: 1 }); + } else { + newDate = date.add({ months: 1 }); + } + break; + default: + } + + setFocusedDate(event, newDate); + }; + + const handleFocus: FocusEventHandler = (event) => { + setFocusedDate(event, date); + }; + + return { + handleClick, + handleKeyDown, + handleFocus, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/utils.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/utils.ts new file mode 100644 index 000000000..e842d0fcd --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/internal/utils.ts @@ -0,0 +1,77 @@ +import { + createCalendar, + DateFormatter, + DateValue, + getLocalTimeZone, + isSameMonth, + startOfMonth, + startOfWeek, + startOfYear, + today, +} from "@internationalized/date"; + +const localTimezone = getLocalTimeZone(); + +export function getCurrentLocale() { + return navigator.languages[0]; +} + +export function getDateFormatter(options?: Intl.DateTimeFormatOptions) { + return new DateFormatter(getCurrentLocale(), options); +} + +export function formatDate( + date: DateValue, + options?: Intl.DateTimeFormatOptions +) { + const formatter = getDateFormatter(options); + return formatter.format(date.toDate(localTimezone)); +} + +export function getCalender() { + const calendarIdentifier = getDateFormatter().resolvedOptions().calendar; + return createCalendar(calendarIdentifier); +} + +type WeekdayFormat = Intl.DateTimeFormatOptions["weekday"]; + +export function daysForLocale(weekday: WeekdayFormat = "long") { + return [...Array(7).keys()].map((day) => + formatDate( + startOfWeek(today(getLocalTimeZone()), getCurrentLocale()).add({ + days: day, + }), + { weekday } + ) + ); +} + +export function monthsForLocale(currentYear: DateValue) { + const calendar = getCalender(); + return [...Array(calendar.getMonthsInYear(currentYear)).keys()].map((month) => + startOfYear(currentYear).add({ months: month }) + ); +} + +function mapDate(currentDate: DateValue, currentMonth: DateValue) { + return { + date: currentDate, + dateOfMonth: currentDate.month, + isCurrentMonth: isSameMonth(currentDate, currentMonth), + }; +} + +export function generateVisibleDays(currentMonth: DateValue) { + const totalDays = 6 * 7; + const currentLocale = getCurrentLocale(); + const startDate = startOfWeek(startOfMonth(currentMonth), currentLocale); + + return [...Array(totalDays).keys()].map((dayDelta) => { + const day = startDate.add({ days: dayDelta }); + return mapDate(day, currentMonth); + }); +} + +export function monthDiff(a: DateValue, b: DateValue) { + return b.month - a.month + 12 * (b.year - a.year); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendar.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendar.ts new file mode 100644 index 000000000..59ca29406 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendar.ts @@ -0,0 +1,208 @@ +import { + DateValue, + endOfMonth, + endOfYear, + getLocalTimeZone, + isSameDay, + startOfMonth, + startOfYear, + today, +} from "@internationalized/date"; +import { useControlled } from "@salt-ds/core"; +import { SyntheticEvent, useCallback, useEffect, useState } from "react"; +import { + UseMultiSelectionCalendarProps, + UseOffsetSelectionCalendarProps, + UseRangeSelectionCalendarProps, + useSelectionCalendar, + useSelectionCalendarProps, + UseSingleSelectionCalendarProps, +} from "./useSelection"; + +export type UnselectableInfo = + | { emphasis: "medium"; tooltip: string } + | { emphasis: "low"; tooltip?: string }; + +interface BaseUseCalendarProps { + defaultVisibleMonth?: DateValue; + onVisibleMonthChange?: ( + event: SyntheticEvent, + visibleMonth: DateValue + ) => void; + isDayUnselectable?: (date: DateValue) => UnselectableInfo | boolean | void; + visibleMonth?: DateValue; + hideOutOfRangeDates?: boolean; + hideYearDropdown?: boolean; + minDate?: DateValue; + maxDate?: DateValue; +} + +export type useCalendarProps = ( + | Omit + | Omit + | Omit + | Omit +) & + BaseUseCalendarProps; + +const defaultIsDayUnselectable = (): UnselectableInfo | false => false; + +export function useCalendar(props: useCalendarProps) { + const { + selectedDate, + defaultSelectedDate, + visibleMonth: visibleMonthProp, + hideYearDropdown, + hideOutOfRangeDates, + defaultVisibleMonth = today(getLocalTimeZone()), + onSelectedDateChange, + onVisibleMonthChange, + isDayUnselectable = defaultIsDayUnselectable, + minDate = hideYearDropdown + ? startOfYear(today(getLocalTimeZone())) + : undefined, + maxDate = hideYearDropdown + ? endOfYear(today(getLocalTimeZone())) + : undefined, + selectionVariant, + onHoveredDateChange, + hoveredDate, + // startDateOffset, + // endDateOffset, + } = props; + + const isOutsideAllowedDates = useCallback( + (date: DateValue) => { + return ( + (minDate != null && date.compare(minDate) < 0) || + (maxDate != null && date.compare(maxDate) > 0) + ); + }, + [maxDate, minDate] + ); + + const isOutsideAllowedMonths = (date: DateValue) => { + return ( + (minDate != null && endOfMonth(date).compare(minDate) < 0) || + (maxDate != null && startOfMonth(date).compare(maxDate) > 0) + ); + }; + + const isOutsideAllowedYears = (date: DateValue) => { + return ( + (minDate != null && endOfYear(date).compare(minDate) < 0) || + (maxDate != null && startOfYear(date).compare(maxDate) > 0) + ); + }; + + const isDaySelectable = useCallback( + (date?: DateValue) => + !(date && (isDayUnselectable(date) || isOutsideAllowedDates(date))), + [isDayUnselectable, isOutsideAllowedDates] + ); + + const selectionManager = useSelectionCalendar({ + defaultSelectedDate: defaultSelectedDate, + selectedDate, + onSelectedDateChange, + startDateOffset: + props.selectionVariant === "offset" + ? props.startDateOffset + : (date) => date, + endDateOffset: + props.selectionVariant === "offset" + ? props.endDateOffset + : (date) => date, + isDaySelectable, + selectionVariant, + onHoveredDateChange, + hoveredDate, + } as useSelectionCalendarProps); + + const [visibleMonth, setVisibleMonthState] = useControlled({ + controlled: visibleMonthProp ? startOfMonth(visibleMonthProp) : undefined, + default: startOfMonth(defaultVisibleMonth), + name: "Calendar", + state: "visibleMonth", + }); + + const [calendarFocused, setCalendarFocused] = useState(false); + + const [focusedDate, setFocusedDateState] = useState( + startOfMonth(visibleMonth) + ); + + const isDayVisible = useCallback( + (date: DateValue) => { + const startInsideDays = startOfMonth(visibleMonth); + + if (date.compare(startInsideDays) < 0) return false; + + const endInsideDays = endOfMonth(visibleMonth); + + return !(date.compare(endInsideDays) > 0); + }, + [visibleMonth] + ); + + const setVisibleMonth = useCallback( + (event: SyntheticEvent, newVisibleMonth: DateValue) => { + setVisibleMonthState(newVisibleMonth); + onVisibleMonthChange?.(event, newVisibleMonth); + }, + [onVisibleMonthChange, setVisibleMonthState] + ); + + const setFocusedDate = useCallback( + (event: SyntheticEvent, date: DateValue) => { + if (isSameDay(date, focusedDate) || isOutsideAllowedDates(date)) return; + + setFocusedDateState(date); + + const shouldTransition = + !isDayVisible(date) && + isDaySelectable(date) && + !isOutsideAllowedDates(date); + if (shouldTransition) { + setVisibleMonth(event, startOfMonth(date)); + } + }, + [ + focusedDate, + isDaySelectable, + isDayVisible, + isOutsideAllowedDates, + setVisibleMonth, + ] + ); + + useEffect(() => { + if (!isDayVisible(focusedDate)) { + setFocusedDateState(startOfMonth(visibleMonth)); + } + }, [isDayVisible, focusedDate, visibleMonth]); + + return { + state: { + visibleMonth, + focusedDate, + minDate, + maxDate, + selectionVariant, + hideOutOfRangeDates, + calendarFocused, + ...selectionManager.state, + }, + helpers: { + setVisibleMonth, + setFocusedDate, + setCalendarFocused, + isDayUnselectable, + isDayVisible, + isOutsideAllowedDates, + isOutsideAllowedMonths, + isOutsideAllowedYears, + ...selectionManager.helpers, + }, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendarDay.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendarDay.ts new file mode 100644 index 000000000..30f3021ab --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/useCalendarDay.ts @@ -0,0 +1,112 @@ +import { + DateValue, + getLocalTimeZone, + isSameDay, + isSameMonth, + isToday, +} from "@internationalized/date"; +import { + ComponentPropsWithoutRef, + FocusEventHandler, + KeyboardEventHandler, + MouseEventHandler, + RefObject, + useEffect, +} from "react"; +import { useCalendarContext } from "./internal/CalendarContext"; +import { useFocusManagement } from "./internal/useFocusManagement"; +import { useSelectionDay } from "./useSelection"; + +export type DayStatus = { + outOfRange?: boolean; + selected?: boolean; + today?: boolean; + unselectable?: "medium" | "low" | false; + focused?: boolean; + hidden?: boolean; +}; + +export interface useCalendarDayProps { + date: DateValue; + month: DateValue; +} + +export function useCalendarDay( + { date, month }: useCalendarDayProps, + ref: RefObject +) { + const { + state: { focusedDate, hideOutOfRangeDates, calendarFocused }, + helpers: { isDayUnselectable, isOutsideAllowedMonths }, + } = useCalendarContext(); + const selectionManager = useSelectionDay({ date }); + const focusManager = useFocusManagement({ date }); + + const handleClick: MouseEventHandler = (event) => { + selectionManager?.handleClick(event); + focusManager.handleClick(event); + }; + + const handleKeyDown: KeyboardEventHandler = (event) => { + focusManager.handleKeyDown(event); + selectionManager?.handleKeyDown(event); + }; + + const handleFocus: FocusEventHandler = (event) => { + focusManager.handleFocus(event); + }; + + const handleMouseOver: MouseEventHandler = (event) => { + selectionManager.handleMouseOver?.(event); + }; + + const eventHandlers = { + onClick: handleClick, + onKeyDown: handleKeyDown, + onFocus: handleFocus, + onMouseOver: handleMouseOver, + }; + + const outOfRange = !isSameMonth(date, month); + const focused = + isSameDay(date, focusedDate) && calendarFocused && !outOfRange; + const tabIndex = isSameDay(date, focusedDate) && !outOfRange ? 0 : -1; + const today = isToday(date, getLocalTimeZone()); + + const unselectableResult = + isDayUnselectable(date) || (outOfRange && isOutsideAllowedMonths(date)); + const unselectableReason = + typeof unselectableResult !== "boolean" ? unselectableResult?.tooltip : ""; + const unselectable = + typeof unselectableResult !== "boolean" + ? unselectableResult.emphasis + : unselectableResult + ? "low" + : false; + const hidden = hideOutOfRangeDates && outOfRange; + + useEffect(() => { + if (focused) { + ref.current?.focus({ preventScroll: true }); + } + }, [ref, focused]); + + return { + status: { + outOfRange, + today, + unselectable, + focused, + hidden, + ...selectionManager.status, + } as DayStatus, + dayProps: { + tabIndex, + "aria-current": today ? "date" : undefined, + "aria-hidden": hidden ? "true" : undefined, + ...eventHandlers, + ...selectionManager.dayProps, + } as ComponentPropsWithoutRef<"button">, + unselectableReason, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts new file mode 100644 index 000000000..9c5f9b094 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts @@ -0,0 +1,377 @@ +import { makePrefixer, useControlled } from "@salt-ds/core"; +import { clsx } from "clsx"; +import { KeyboardEventHandler, MouseEventHandler, SyntheticEvent } from "react"; +import { isPlainObject } from "../common-hooks/isPlainObject"; +import { useCalendarContext } from "./internal/CalendarContext"; +import { CalendarDate, DateValue, isSameDay } from "@internationalized/date"; + +interface BaseUseSelectionCalendarProps { + hoveredDate?: DateValue | null; + selectedDate?: SelectionVariantType | null; + defaultSelectedDate?: SelectionVariantType; + onSelectedDateChange?: ( + event: SyntheticEvent, + selectedDate: SelectionVariantType + ) => void; + isDaySelectable: (date?: DateValue) => boolean; + onHoveredDateChange?: ( + event: SyntheticEvent, + hoveredDate: DateValue | null + ) => void; +} + +type SingleSelectionValueType = DateValue; +type MultiSelectionValueType = DateValue[]; +export type RangeSelectionValueType = { + startDate?: DateValue; + endDate?: DateValue; +}; +type OffsetSelectionValueType = { + startDate?: DateValue; + endDate?: DateValue; +}; + +type AllSelectionValueType = + | SingleSelectionValueType + | MultiSelectionValueType + | RangeSelectionValueType + | OffsetSelectionValueType + | null; + +export interface UseOffsetSelectionCalendarProps + extends Omit< + BaseUseSelectionCalendarProps, + "startDateOffset" | "endDateOffset" + > { + selectionVariant: "offset"; + startDateOffset?: (date: DateValue) => DateValue; + endDateOffset?: (date: DateValue) => DateValue; +} + +export interface UseRangeSelectionCalendarProps + extends BaseUseSelectionCalendarProps { + selectionVariant: "range"; +} + +export interface UseMultiSelectionCalendarProps + extends BaseUseSelectionCalendarProps { + selectionVariant: "multiselect"; +} + +export interface UseSingleSelectionCalendarProps + extends BaseUseSelectionCalendarProps { + selectionVariant: "default"; +} + +export type useSelectionCalendarProps = + | UseSingleSelectionCalendarProps + | UseMultiSelectionCalendarProps + | UseRangeSelectionCalendarProps + | UseOffsetSelectionCalendarProps; + +function addOrRemoveFromArray( + array: AllSelectionValueType | null = [], + item: DateValue +) { + if (Array.isArray(array)) { + if (array.find((element) => isSameDay(element, item))) { + return array.filter((element) => !isSameDay(element, item)); + } + return array.concat(item); + } + return [item]; +} + +const defaultOffset = (date: DateValue) => date; + +function isRangeOrOffsetSelectionValue( + selectionValue?: AllSelectionValueType +): selectionValue is RangeSelectionValueType | OffsetSelectionValueType { + return selectionValue != null && isPlainObject(selectionValue); +} + +const withBaseName = makePrefixer("saltCalendarDay"); + +export function useSelectionCalendar(props: useSelectionCalendarProps) { + const { + hoveredDate: hoveredDateProp, + selectedDate: selectedDateProp, + defaultSelectedDate, + // onSelectedDateChange, + onHoveredDateChange, + isDaySelectable, + selectionVariant, + // startDateOffset, + // endDateOffset, + } = props; + const [selectedDate, setSelectedDateState] = useControlled({ + controlled: selectedDateProp, + default: defaultSelectedDate, + name: "Calendar", + state: "selectedDate", + }); + + const getStartDateOffset = (date: DateValue) => { + if (props.selectionVariant === "offset" && props.startDateOffset) { + return props.startDateOffset(date); + } else { + return defaultOffset(date); + } + }; + + const getEndDateOffset = (date: DateValue) => { + if (props.selectionVariant === "offset" && props.endDateOffset) { + return props.endDateOffset(date); + } else { + return defaultOffset(date); + } + }; + + const setSelectedDate = ( + event: SyntheticEvent, + newSelectedDate: DateValue + ) => { + if (isDaySelectable(newSelectedDate)) { + switch (props.selectionVariant) { + case "default": + setSelectedDateState(newSelectedDate); + props.onSelectedDateChange?.(event, newSelectedDate); + break; + case "multiselect": { + const newDates = addOrRemoveFromArray(selectedDate, newSelectedDate); + setSelectedDateState(newDates); + props.onSelectedDateChange?.(event, newDates); + break; + } + case "range": { + let base = selectedDate; + if (isRangeOrOffsetSelectionValue(base)) { + if (base?.startDate && base?.endDate) { + base = { startDate: newSelectedDate }; + } else if ( + base?.startDate && + newSelectedDate.compare(base.startDate) > 0 + ) { + base = { ...base, endDate: newSelectedDate }; + } else { + base = { startDate: newSelectedDate }; + } + } else { + base = { startDate: newSelectedDate }; + } + setSelectedDateState(base); + props.onSelectedDateChange?.(event, base); + break; + } + case "offset": { + const newRange = { + startDate: getStartDateOffset(newSelectedDate), + endDate: getEndDateOffset(newSelectedDate), + }; + setSelectedDateState(newRange); + props.onSelectedDateChange?.(event, newRange); + } + } + } + }; + + const isSelected = (date: DateValue) => { + switch (selectionVariant) { + case "default": + return ( + selectedDate instanceof CalendarDate && isSameDay(selectedDate, date) + ); + case "multiselect": + return ( + Array.isArray(selectedDate) && + !!selectedDate.find((element) => isSameDay(element, date)) + ); + default: + return false; + } + }; + + const [hoveredDate, setHoveredDateState] = useControlled({ + controlled: hoveredDateProp, + default: undefined, + name: "Calendar", + state: "hoveredDate", + }); + + const setHoveredDate = (event: SyntheticEvent, date: DateValue | null) => { + setHoveredDateState(date); + onHoveredDateChange?.(event, date); + }; + + const isHovered = (date: DateValue) => { + return !!hoveredDate && isSameDay(date, hoveredDate); + }; + + const isSelectedSpan = (date: DateValue) => { + if ( + (selectionVariant === "range" || selectionVariant === "offset") && + isRangeOrOffsetSelectionValue(selectedDate) && + selectedDate?.startDate && + selectedDate?.endDate + ) { + return ( + date.compare(selectedDate.startDate) > 0 && + date.compare(selectedDate.endDate) < 0 + ); + } + return false; + }; + const isHoveredSpan = (date: DateValue) => { + if ( + (selectionVariant === "range" || selectionVariant === "offset") && + isRangeOrOffsetSelectionValue(selectedDate) && + selectedDate.startDate && + !selectedDate.endDate && + hoveredDate + ) { + const isForwardRange = + hoveredDate.compare(selectedDate.startDate) > 0 && + ((date.compare(selectedDate.startDate) > 0 && + date.compare(hoveredDate) < 0) || + isSameDay(date, hoveredDate)); + + const isValidDayHovered = isDaySelectable(hoveredDate); + + return isForwardRange && isValidDayHovered; + } + return false; + }; + + const isSelectedStart = (date: DateValue) => { + if ( + (selectionVariant === "range" || selectionVariant === "offset") && + isRangeOrOffsetSelectionValue(selectedDate) && + selectedDate.startDate + ) { + return isSameDay(selectedDate.startDate, date); + } + return false; + }; + + const isSelectedEnd = (date: DateValue) => { + if ( + (selectionVariant === "range" || selectionVariant === "offset") && + isRangeOrOffsetSelectionValue(selectedDate) && + selectedDate.endDate + ) { + return isSameDay(selectedDate.endDate, date); + } + return false; + }; + + const isHoveredOffset = (date: DateValue) => { + if (hoveredDate && selectionVariant === "offset") { + const startDate = getStartDateOffset(hoveredDate); + const endDate = getEndDateOffset(hoveredDate); + + return ( + date.compare(startDate) >= 0 && + date.compare(endDate) <= 0 && + isDaySelectable(date) + ); + } + + return false; + }; + + return { + state: { + selectedDate, + hoveredDate, + }, + helpers: { + setSelectedDate, + isSelected, + setHoveredDate, + isHovered, + isSelectedSpan, + isHoveredSpan, + isSelectedStart, + isSelectedEnd, + isHoveredOffset, + }, + }; +} + +export function useSelectionDay({ date }: { date: DateValue }) { + const { + helpers: { + setSelectedDate, + isSelected, + setHoveredDate, + isSelectedSpan, + isHoveredSpan, + isSelectedStart, + isSelectedEnd, + isHovered, + isHoveredOffset, + isDayUnselectable, + }, + } = useCalendarContext(); + + const handleClick: MouseEventHandler = (event) => { + setSelectedDate(event, date); + }; + + const handleKeyDown: KeyboardEventHandler = (event) => { + switch (event.key) { + case "Space": + case "Enter": + setSelectedDate(event, date); + event.preventDefault(); + } + }; + + const handleMouseOver: MouseEventHandler = (event) => { + setHoveredDate(event, date); + }; + + const handleMouseLeave: MouseEventHandler = (event) => { + setHoveredDate(event, null); + }; + + const selected = isSelected(date); + const selectedSpan = isSelectedSpan(date); + const hoveredSpan = isHoveredSpan(date); + const selectedStart = isSelectedStart(date); + const selectedEnd = isSelectedEnd(date); + const hovered = isHovered(date); + const hoveredOffset = isHoveredOffset(date); + + return { + handleClick, + handleKeyDown, + handleMouseOver, + handleMouseLeave, + status: { + selected, + selectedSpan, + hoveredSpan, + selectedStart, + selectedEnd, + hovered, + hoveredOffset, + }, + dayProps: { + className: clsx({ + [withBaseName("selected")]: selected, + [withBaseName("selectedSpan")]: selectedSpan, + [withBaseName("hoveredSpan")]: hoveredSpan, + [withBaseName("selectedStart")]: selectedStart, + [withBaseName("selectedEnd")]: selectedEnd, + [withBaseName("hovered")]: hovered, + [withBaseName("hoveredOffset")]: hoveredOffset, + }), + "aria-pressed": + selected || selectedEnd || selectedStart || selectedSpan + ? "true" + : undefined, + "aria-disabled": isDayUnselectable(date) ? "true" : undefined, + }, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/isPlainObject.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/isPlainObject.ts new file mode 100644 index 000000000..b7f66ff1d --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/isPlainObject.ts @@ -0,0 +1,2 @@ +export const isPlainObject = (obj: unknown) => + Object.prototype.toString.call(obj) === "[object Object]"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/itemToString.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/itemToString.ts index aedc8263c..3f5b8c6b9 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/itemToString.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/itemToString.ts @@ -1,9 +1,8 @@ +import { isPlainObject } from "./isPlainObject"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ItemToStringFunction = (item: any) => string; -const isPlainObject = (obj: unknown) => - Object.prototype.toString.call(obj) === "[object Object]"; - export function itemToString(item: unknown): string { if (typeof item === "string") { return item; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css new file mode 100644 index 000000000..a2967b726 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css @@ -0,0 +1,11 @@ +.vuuDatePicker { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 1px; + padding: 0 2px; +} + +.vuuDatePicker-calendar { + padding: 2px; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx new file mode 100644 index 000000000..46745158a --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx @@ -0,0 +1,90 @@ +import { useCallback, useMemo } from "react"; +import { DateValue, today, getLocalTimeZone } from "@internationalized/date"; +import { clsx } from "clsx"; +import { Calendar } from "../calendar/Calendar"; +import { DatePickerInput } from "./input/DatePickerInput"; +import { CalendarIconButton } from "./internal/CalendarIconButton"; +import { DropdownBase } from "../dropdown"; +import { useBaseDatePicker } from "./useBaseDatePicker"; +import { useBaseDatePickerDropdown } from "./useBaseDatePickerDropdown"; +import { BaseDatePickerDropdownProps, BaseDatePickerProps } from "./types"; + +import "./DatePicker.css"; + +const baseClass = "vuuDatePicker"; + +export const DatePicker = (props: BaseDatePickerProps) => { + const { selectedDate, onSelectedDateChange, onBlur, className } = props; + const { visibleMonth, handleVisibleMonthChange, handleOnBlur } = + useBaseDatePicker({ variant: "default", selectedDate, onBlur }); + + const handleInputChange = useCallback( + (d: DateValue) => { + onSelectedDateChange(d); + handleVisibleMonthChange(d); + }, + [onSelectedDateChange] + ); + + return ( +
+ + +
+ ); +}; + +const DatePickerDropdown = (props: BaseDatePickerDropdownProps) => { + const { + closeOnSelection, + onSelectedDateChange, + onVisibleMonthChange, + className, + ...rest + } = props; + + const shouldCloseOnSelectionChange = useCallback( + () => !!closeOnSelection, + [closeOnSelection] + ); + + const { + triggererRef, + isOpen, + handleOpenChange, + handleVisibleMonthChange, + handleDateSelection, + } = useBaseDatePickerDropdown({ + onVisibleMonthChange, + onSelectedDateChange, + shouldCloseOnSelectionChange, + }); + + const defaultSelectedDate = useMemo(() => today(getLocalTimeZone()), []); + + return ( + + + + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx new file mode 100644 index 000000000..ea4e8acca --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx @@ -0,0 +1,98 @@ +import { useCallback, useMemo } from "react"; +import { Calendar } from "../calendar/Calendar"; +import { today, getLocalTimeZone } from "@internationalized/date"; +import { clsx } from "clsx"; +import { DateRangePickerInput } from "./input/DateRangePickerInput"; +import { BaseDatePickerDropdownProps, BaseDatePickerProps } from "./types"; +import { RangeSelectionValueType } from "../calendar"; +import { CalendarIconButton } from "./internal/CalendarIconButton"; +import { DropdownBase } from "../dropdown"; +import { useBaseDatePicker } from "./useBaseDatePicker"; +import { useBaseDatePickerDropdown } from "./useBaseDatePickerDropdown"; + +import "./DatePicker.css"; + +const baseClass = "vuuDatePicker"; + +export const DateRangePicker = ( + props: BaseDatePickerProps +) => { + const { selectedDate, onSelectedDateChange, className, onBlur } = props; + const { visibleMonth, handleVisibleMonthChange, handleOnBlur } = + useBaseDatePicker({ variant: "range", selectedDate, onBlur }); + + const handleInputChange = useCallback( + (r: RangeSelectionValueType) => { + onSelectedDateChange(r); + handleVisibleMonthChange(r.endDate ?? r.startDate); + }, + [onSelectedDateChange] + ); + + return ( +
+ + +
+ ); +}; + +const DateRangePickerDropdown = ( + props: BaseDatePickerDropdownProps +) => { + const { + onVisibleMonthChange, + onSelectedDateChange, + closeOnSelection, + className, + ...rest + } = props; + + const shouldCloseOnSelectionChange = useCallback( + (r: RangeSelectionValueType) => !!(closeOnSelection && r.endDate), + [closeOnSelection] + ); + + const { + triggererRef, + isOpen, + handleOpenChange, + handleVisibleMonthChange, + handleDateSelection, + } = useBaseDatePickerDropdown({ + onVisibleMonthChange, + onSelectedDateChange, + shouldCloseOnSelectionChange, + }); + + const defaultSelectedDate = useMemo( + () => ({ startDate: today(getLocalTimeZone()) }), + [] + ); + + return ( + + + + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts new file mode 100644 index 000000000..6b6443856 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts @@ -0,0 +1,2 @@ +export * from "./DatePicker"; +export * from "./DateRangePicker"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css new file mode 100644 index 000000000..8b9a72c0e --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css @@ -0,0 +1,13 @@ +.vuuDatePickerInput { + border: none; + width: 100%; + padding-left: 0; +} + +.vuuDatePickerInput:focus { + outline: none; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + display: none; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx new file mode 100644 index 000000000..8d4194f0e --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from "react"; +import { DateValue, CalendarDate } from "@internationalized/date"; +import { clsx } from "clsx"; +import { BasePickerInputProps } from "./types"; + +import "./DatePickerInput.css"; + +const baseClass = "vuuDatePickerInput"; + +type Props = BasePickerInputProps; + +export const DatePickerInput: React.FC = (props) => { + const { value, onChange, className } = props; + + const onInputChange = useCallback>( + (e) => { + const v = e.target.value; + if (v === "") return; + else onChange(toCalendarDate(new Date(v))); + }, + [onChange] + ); + + return ( + + ); +}; + +function toCalendarDate(d: Date): CalendarDate { + return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate()); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css new file mode 100644 index 000000000..675505ee0 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css @@ -0,0 +1,16 @@ +.vuuDateRangePickerInput { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.vuuDateRangePickerInput input:last-child { + text-align: right; +} + +.vuuDateRangePickerInput span { + width: fit-content; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx new file mode 100644 index 000000000..82ff0f48e --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx @@ -0,0 +1,37 @@ +import { useCallback } from "react"; +import { DateValue } from "@internationalized/date"; +import { clsx } from "clsx"; +import { DatePickerInput } from "./DatePickerInput"; +import { BasePickerInputProps } from "./types"; +import { RangeSelectionValueType } from "../../calendar"; + +import "./DateRangePickerInput.css"; + +const baseClass = "vuuDateRangePickerInput"; + +type Props = BasePickerInputProps; + +export const DateRangePickerInput: React.FC = (props) => { + const { value, onChange, className } = props; + + const getHandleInputChange = useCallback( + (k: keyof RangeSelectionValueType) => (d: DateValue) => { + onChange({ ...value, [k]: d }); + }, + [value, onChange] + ); + + return ( +
+ + + +
+ ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts new file mode 100644 index 000000000..0ab39966a --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts @@ -0,0 +1,7 @@ +import { PickerSelectionType } from "../types"; + +export interface BasePickerInputProps { + onChange: (selected: T) => void; + value?: T; + className?: string; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.css new file mode 100644 index 000000000..8a37ca869 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.css @@ -0,0 +1,8 @@ +.vuuDatePicker-calendarIconButton { + padding: 3px; +} + +.vuuDatePicker-calendarIconButton .saltIcon { + display: inline-block; + margin: 0; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx new file mode 100644 index 000000000..9b5e19ee6 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx @@ -0,0 +1,25 @@ +import { Button } from "@salt-ds/core"; +import { CalendarIcon } from "@salt-ds/icons"; +import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from "react"; +import clsx from "clsx"; + +import "./CalendarIconButton.css"; + +const baseClass = "vuuDatePicker-calendarIconButton"; + +export const CalendarIconButton = forwardRef(function CalendarIconButton( + { className, ...rest }: ComponentPropsWithoutRef, + ref: ForwardedRef +) { + return ( + + ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/types.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/types.ts new file mode 100644 index 000000000..971224fc6 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/types.ts @@ -0,0 +1,20 @@ +import { DateValue } from "@internationalized/date"; +import { CalendarProps } from "../calendar/Calendar"; +import { RangeSelectionValueType } from "../calendar"; + +export type PickerSelectionType = DateValue | RangeSelectionValueType; + +export interface BaseDatePickerProps + extends Pick { + onSelectedDateChange: (selected: T) => void; + selectedDate: T | undefined; + closeOnSelection?: boolean; + onBlur?: () => void; + className?: string; +} + +export interface BaseDatePickerDropdownProps + extends BaseDatePickerProps { + visibleMonth: DateValue | undefined; + onVisibleMonthChange: (d: DateValue) => void; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePicker.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePicker.ts new file mode 100644 index 000000000..f5c71e87b --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePicker.ts @@ -0,0 +1,36 @@ +import { useCallback, useState } from "react"; +import { BaseDatePickerProps } from "./types"; +import { DateValue } from "@internationalized/date"; +import { PickerSelectionType } from "./types"; +import { RangeSelectionValueType } from "../calendar"; + +type InheritedProps = Pick< + BaseDatePickerProps, + "onBlur" | "selectedDate" +>; +type Props = + | ({ variant: "range" } & InheritedProps) + | ({ variant: "default" } & InheritedProps); + +export function useBaseDatePicker(props: Props) { + const [visibleMonth, setVisibleMonth] = useState( + props.variant === "default" + ? props.selectedDate + : props.selectedDate?.startDate + ); + + const handleOnBlur: React.FocusEventHandler = useCallback( + (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + props.onBlur?.(); + } + }, + [props.onBlur] + ); + + return { + handleOnBlur, + visibleMonth, + handleVisibleMonthChange: setVisibleMonth, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePickerDropdown.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePickerDropdown.ts new file mode 100644 index 000000000..ac3ee2d50 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/useBaseDatePickerDropdown.ts @@ -0,0 +1,45 @@ +import { useCallback, useRef, useState } from "react"; +import { DateValue } from "@internationalized/date"; +import { PickerSelectionType } from "./types"; +import { BaseDatePickerDropdownProps } from "./types"; + +type Props = Pick< + BaseDatePickerDropdownProps, + "onSelectedDateChange" | "onVisibleMonthChange" +> & { + shouldCloseOnSelectionChange: (v: T) => boolean; +}; + +export function useBaseDatePickerDropdown({ + onVisibleMonthChange, + onSelectedDateChange, + shouldCloseOnSelectionChange, +}: Props) { + const [isOpen, setIsOpen] = useState(false); + const triggererRef = useRef(null); + + const handleDateSelection = useCallback( + (_: React.SyntheticEvent, d: T) => { + onSelectedDateChange(d); + if (shouldCloseOnSelectionChange(d)) setIsOpen(false); + triggererRef.current?.focus(); + }, + [onSelectedDateChange, shouldCloseOnSelectionChange] + ); + + const handleVisibleMonthChange = useCallback( + (_: React.SyntheticEvent, d: DateValue) => { + onVisibleMonthChange(d); + triggererRef.current?.focus(); + }, + [onVisibleMonthChange] + ); + + return { + isOpen, + handleOpenChange: setIsOpen, + triggererRef, + handleVisibleMonthChange, + handleDateSelection, + }; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts index 2f5351e7f..2c210aa31 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts @@ -1,4 +1,4 @@ -import { MouseOffset } from "./dragDropTypesNext"; +import { MouseOffset, MousePosition } from "./dragDropTypes"; export class DragDropState { /** Distance between start (top | left) of dragged element and point where user pressed to drag */ @@ -10,9 +10,9 @@ export class DragDropState { payload: unknown = null; - constructor(evt: MouseEvent, dragElement: HTMLElement) { + constructor(mousePosition: MousePosition, dragElement: HTMLElement) { this.initialDragElement = dragElement; - this.mouseOffset = this.getMouseOffset(evt, dragElement); + this.mouseOffset = this.getMouseOffset(mousePosition, dragElement); } /** Used to capture a ref to the Draggable JSX.Element */ @@ -24,8 +24,11 @@ export class DragDropState { this.payload = payload; } - private getMouseOffset(evt: MouseEvent, dragElement: HTMLElement) { - const { clientX, clientY } = evt; + private getMouseOffset( + mousePosition: MousePosition, + dragElement: HTMLElement + ) { + const { clientX, clientY } = mousePosition; const draggableRect = dragElement.getBoundingClientRect(); return { diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx index 89fb2095d..05103d83a 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DropIndicator.tsx @@ -1,6 +1,6 @@ import { Portal } from "@finos/vuu-popups"; import { forwardRef } from "react"; -import { Rect } from "./dragDropTypesNext"; +import { Rect } from "./dragDropTypes"; import "./DropIndicator.css"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypes.ts similarity index 97% rename from vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts rename to vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypes.ts index 6bd327b31..146c0b2fb 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypes.ts @@ -37,6 +37,11 @@ export type Direction = "fwd" | "bwd"; export const FWD: Direction = "fwd"; export const BWD: Direction = "bwd"; +export interface MousePosition { + clientX: number; + clientY: number; +} + export interface MouseOffset { x: number; y: number; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts index 9585f0323..48147feaa 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts @@ -1,6 +1,6 @@ import { orientationType } from "@finos/vuu-utils"; -import { ViewportRange } from "./dragDropTypesNext"; -import { Direction, Rect } from "./dragDropTypesNext"; +import { ViewportRange } from "./dragDropTypes"; +import { Direction, Rect } from "./dragDropTypes"; const LEFT_RIGHT = ["left", "right"]; const TOP_BOTTOM = ["top", "bottom"]; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/index.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/index.ts index 2267fde99..6e2d1f1ea 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/index.ts @@ -1,6 +1,6 @@ -export * from "./dragDropTypesNext"; +export * from "./dragDropTypes"; export * from "./DragDropProvider"; export * from "./DragDropState"; -export * from "./useDragDropNext"; +export * from "./useDragDrop"; export * from "./drop-target-utils"; export * from "./useGlobalDragDrop"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts index 5a7ab37a4..1dc08ac11 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts @@ -1,6 +1,6 @@ import type { orientationType } from "@finos/vuu-utils"; import { useCallback, useMemo, useRef } from "react"; -import { Direction } from "./dragDropTypesNext"; +import { Direction } from "./dragDropTypes"; import { createDragSpacer as createDragDisplacer } from "./Draggable"; import { MeasuredDropTarget, diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDrop.tsx similarity index 95% rename from vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx rename to vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDrop.tsx index 5aea4b058..860903271 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDrop.tsx @@ -16,7 +16,8 @@ import { InternalDragDropProps, InternalDragHookResult, MouseOffset, -} from "./dragDropTypesNext"; + MousePosition, +} from "./dragDropTypes"; import { Draggable } from "./Draggable"; import { cloneElement, @@ -29,7 +30,7 @@ import { import { ScrollStopHandler, useAutoScroll } from "./useAutoScroll"; import { useDragDropCopy } from "./useDragDropCopy"; import { useDragDropIndicator } from "./useDragDropIndicator"; -import { useDragDropNaturalMovement } from "./useDragDropNaturalMovementNext"; +import { useDragDropNaturalMovement } from "./useDragDropNaturalMovement"; import { ResumeDragHandler } from "./useGlobalDragDrop"; const NULL_DRAG_DROP_RESULT = { @@ -71,7 +72,7 @@ const dragThreshold = 3; const getDraggableElement = ( el: EventTarget | null, query: string -): HTMLElement => (el as HTMLElement).closest(query) as HTMLElement; +): HTMLElement => (el as HTMLElement)?.closest(query) as HTMLElement; const getLastElement = ( container: HTMLElement, @@ -83,7 +84,7 @@ const getLastElement = ( return [lastElement, isOverflowElement(lastElement)]; }; -export const useDragDropNext: DragDropHook = ({ +export const useDragDrop: DragDropHook = ({ allowDragDrop, containerRef, draggableClassName, @@ -112,6 +113,10 @@ export const useDragDropNext: DragDropHook = ({ const mouseDownTimer = useRef(null); /** do we actually have scrollable content */ const isScrollableRef = useRef(false); + + /** save this on mousedown. We cannot rely on the target of mousemove being same element*/ + const mousedownElementRef = useRef(null); + /** current mouse position */ const mousePosRef = useRef({ x: 0, y: 0 }); /** mouse position when mousedown initiated drag */ @@ -463,10 +468,10 @@ export const useDragDropNext: DragDropHook = ({ ); const dragStart = useCallback( - (evt: MouseEvent) => { - const { target } = evt; - const dragElement = getDraggableElement(target, itemQuery); + (mousePosition: MousePosition) => { const { current: container } = containerRef; + const { current: target } = mousedownElementRef; + const dragElement = getDraggableElement(target, itemQuery); if (container && dragElement) { const scrollableContainer = getScrollableContainer( container, @@ -483,7 +488,7 @@ export const useDragDropNext: DragDropHook = ({ const draggableRect = dragElement.getBoundingClientRect(); const dragDropState = (dragDropStateRef.current = new DragDropState( - evt, + mousePosition, dragElement )); @@ -512,6 +517,8 @@ export const useDragDropNext: DragDropHook = ({ onDragStart?.(dragDropState); attachDragHandlers(); + + mousedownElementRef.current = null; } }, [ @@ -529,9 +536,10 @@ export const useDragDropNext: DragDropHook = ({ const preDragMouseMoveHandler = useCallback( (evt: MouseEvent) => { + const { current: mouseDownPosition } = startPosRef; const { CLIENT_POS, POS } = dimensions(orientation); const { [CLIENT_POS]: clientPos } = evt; - const mouseMoveDistance = Math.abs(clientPos - startPosRef.current[POS]); + const mouseMoveDistance = Math.abs(clientPos - mouseDownPosition[POS]); if (mouseMoveDistance > dragThreshold && containerRef.current) { if (mouseDownTimer.current) { window.clearTimeout(mouseDownTimer.current); @@ -540,7 +548,11 @@ export const useDragDropNext: DragDropHook = ({ document.removeEventListener("mousemove", preDragMouseMoveHandler); document.removeEventListener("mouseup", preDragMouseUpHandler, false); - dragStart(evt); + const mousePosition = { + clientX: mouseDownPosition.x, + clientY: mouseDownPosition.y, + }; + dragStart(mousePosition); } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -559,7 +571,6 @@ export const useDragDropNext: DragDropHook = ({ const mouseDownHandler: MouseEventHandler = useCallback( (evt) => { // TODO runtime check here for valid drop targets ? - const { current: container } = containerRef; // We don't want to prevent other handlers on this element from working // but we do want to stop a drag drop being initiated on a bubbled event. @@ -568,6 +579,12 @@ export const useDragDropNext: DragDropHook = ({ const { clientX, clientY } = evt; mousePosRef.current.x = startPosRef.current.x = clientX; mousePosRef.current.y = startPosRef.current.y = clientY; + mousedownElementRef.current = evt.target as HTMLElement; + + const mousePosition = { + clientX, + clientY, + }; document.addEventListener("mousemove", preDragMouseMoveHandler, false); document.addEventListener("mouseup", preDragMouseUpHandler, false); @@ -581,7 +598,7 @@ export const useDragDropNext: DragDropHook = ({ false ); document.removeEventListener("mouseup", preDragMouseUpHandler, false); - dragStart(evt.nativeEvent); + dragStart(mousePosition); }, 500); } }, diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts index e4ec1744c..76de2164f 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts @@ -4,7 +4,7 @@ import { InternalDragDropProps, InternalDragHookResult, ViewportRange, -} from "./dragDropTypesNext"; +} from "./dragDropTypes"; export const useDragDropCopy = ({ selected, diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx index f86509b8c..adbeef7a3 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx @@ -5,7 +5,7 @@ import { InternalDragHookResult, Direction, ViewportRange, -} from "./dragDropTypesNext"; +} from "./dragDropTypes"; import { useDropIndicator } from "./useDropIndicator"; import { diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovement.tsx similarity index 99% rename from vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx rename to vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovement.tsx index 37845c616..29f3837df 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovement.tsx @@ -5,7 +5,7 @@ import { InternalDragDropProps, InternalDragHookResult, ViewportRange, -} from "./dragDropTypesNext"; +} from "./dragDropTypes"; import { useDragDisplacers } from "./useDragDisplacers"; import { dispatchMouseEvent } from "@finos/vuu-utils"; import { @@ -28,7 +28,6 @@ export const useDragDropNaturalMovement = ({ viewportRange, }: InternalDragDropProps): InternalDragHookResult => { const dragDirectionRef = useRef(); - const isScrollable = useRef(false); /** current position of dragged element */ const dragPosRef = useRef(-1); diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts index 579a452c2..b9ff3db7c 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts @@ -2,7 +2,7 @@ import { boxContainsPoint, dispatchCustomEvent } from "@finos/vuu-utils"; import { useCallback, useRef } from "react"; import { MeasuredTarget } from "./DragDropProvider"; import { DragDropState } from "./DragDropState"; -import { MouseOffset } from "./dragDropTypesNext"; +import { MouseOffset } from "./dragDropTypes"; export type ResumeDragHandler = (dragDropState: DragDropState) => boolean; export type GlobalDropHandler = (dragDropState: DragDropState) => void; @@ -93,9 +93,6 @@ export const useGlobalDragDrop = ({ const resumeDrag = useCallback( (dragDropState) => { - console.log(`resume drag of `, { - el: dragDropState.draggableElement, - }); dragDropStateRef.current = dragDropState; document.addEventListener("mousemove", dragMouseMoveHandler, false); document.addEventListener("mouseup", dragMouseUpHandler, false); diff --git a/vuu-ui/packages/vuu-ui-controls/src/index.ts b/vuu-ui/packages/vuu-ui-controls/src/index.ts index befd98f23..5c6814c8e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/index.ts @@ -1,6 +1,8 @@ +export * from "./calendar"; export * from "./combo-box"; export * from "./common-hooks"; export * from "./cycle-state-button"; +export * from "./date-picker"; export * from "./drag-drop"; export * from "./dropdown"; export * from "./editable"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts index 4e34c13ac..c0f26181c 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -17,7 +17,7 @@ import { SelectionStrategy, SingleSelectionHandler, } from "../common-hooks"; -import { DragStartHandler, useDragDropNext as useDragDrop } from "../drag-drop"; +import { DragStartHandler, useDragDrop as useDragDrop } from "../drag-drop"; import { closestListItemIndex, useCollapsibleGroups, diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useTabstrip.ts b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useTabstrip.ts index a6413ef25..418205c0a 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useTabstrip.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/useTabstrip.ts @@ -8,7 +8,7 @@ import { useCallback, useRef, } from "react"; -import { useDragDropNext as useDragDrop } from "../drag-drop"; +import { useDragDrop as useDragDrop } from "../drag-drop"; import { isTabMenuOptions } from "./TabMenuOptions"; import { getIndexOfSelectedTab } from "./tabstrip-dom-utils"; import { useAnimatedSelectionThumb } from "./useAnimatedSelectionThumb"; diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index 2e9ccb66a..fa310ee7d 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -961,3 +961,21 @@ export const applyDefaultColumnConfig = ( return columns; } }; + +export const getColumnByName = ( + schema: TableSchema, + name?: string +): SchemaColumn | undefined => { + if (name === undefined) { + return undefined; + } else { + const column = schema.columns.find((col) => col.name === name); + if (column) { + return column; + } else { + throw Error( + `getColumnByName no column '${name}' in schema for ${schema.table.table}` + ); + } + } +}; diff --git a/vuu-ui/packages/vuu-utils/src/keyset.ts b/vuu-ui/packages/vuu-utils/src/keyset.ts index e503c202b..883ba358d 100644 --- a/vuu-ui/packages/vuu-utils/src/keyset.ts +++ b/vuu-ui/packages/vuu-utils/src/keyset.ts @@ -14,7 +14,7 @@ export class KeySet { public next(): number { if (this.free.length > 0) { - return this.free.pop() as number; + return this.free.shift() as number; } else { return this.nextKeyValue++; } @@ -59,7 +59,7 @@ export class KeySet { public toDebugString() { return Array.from(this.keys.entries()) - .map((k, v) => `${k}=>${v}`) + .map(([k, v]) => `${k}=>${v}`) .join(","); } } diff --git a/vuu-ui/packages/vuu-utils/src/row-utils.ts b/vuu-ui/packages/vuu-utils/src/row-utils.ts index 698f50d0f..9196e2424 100644 --- a/vuu-ui/packages/vuu-utils/src/row-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/row-utils.ts @@ -1,5 +1,6 @@ -import { DataSourceRow } from "@finos/vuu-data-types"; -import { MutableRefObject } from "react"; +//TODO this all probably belongs in vuu-table +import type { DataSourceRow } from "@finos/vuu-data-types"; +import type { MutableRefObject } from "react"; import { metadataKeys } from "./column-utils"; const { IDX } = metadataKeys; @@ -10,29 +11,68 @@ export type RowOffsetFunc = ( ) => number; export type RowAtPositionFunc = (position: number) => number; -export type RowPositioning = [RowOffsetFunc, RowAtPositionFunc]; +/** + * RowOffset function, RowAtPosition function, isVirtualScroll + */ +export type RowPositioning = [RowOffsetFunc, RowAtPositionFunc, boolean]; export const actualRowPositioning = (rowHeight: number): RowPositioning => [ (row) => row[IDX] * rowHeight, (position) => Math.floor(position / rowHeight), + false, ]; +/** + * return functions for determining a) the pixel offset to apply to a row, given the + * row index and b) the index of the row at a given scroll offset. This implementation + * is used when we are forced to 'virtualise' scrolling - because the number of rows + * is high enough that we cannot create a large enough HTML content container. + * + * @param rowHeight + * @param virtualisedExtent + * @param pctScrollTop + * @returns + */ export const virtualRowPositioning = ( rowHeight: number, - additionalPixelsNeeded: number, + virtualisedExtent: number, pctScrollTop: MutableRefObject ): RowPositioning => [ (row) => { - const rowOffset = pctScrollTop.current * additionalPixelsNeeded; + const rowOffset = pctScrollTop.current * virtualisedExtent; return row[IDX] * rowHeight - rowOffset; }, + /* + Return index position of closest row + */ (position) => { - const rowOffset = pctScrollTop.current * additionalPixelsNeeded; - const result = Math.floor((position + rowOffset) / rowHeight); - return result; + const rowOffset = pctScrollTop.current * virtualisedExtent; + return Math.round((position + rowOffset) / rowHeight); }, + true, ]; +export const getRowElementAtIndex = ( + container: HTMLDivElement, + rowIndex: number +) => { + if (rowIndex === -1) { + return null; + } else { + const activeRow = container.querySelector( + `[aria-rowindex="${rowIndex + 1}"]` + ) as HTMLElement; + + if (activeRow) { + return activeRow; + } else { + throw Error( + `getRowElementAtIndex no row found for index index ${rowIndex}` + ); + } + } +}; + export const getIndexFromRowElement = (rowElement: HTMLElement) => { const rowIndex = rowElement.ariaRowIndex; if (rowIndex != null) { diff --git a/vuu-ui/packages/vuu-utils/test/keyset.test.js b/vuu-ui/packages/vuu-utils/test/keyset.test.js index d09c94c82..fd25f6e99 100644 --- a/vuu-ui/packages/vuu-utils/test/keyset.test.js +++ b/vuu-ui/packages/vuu-utils/test/keyset.test.js @@ -49,7 +49,7 @@ describe("KeySet", () => { keySet.reset({ from: 2, to: 12 }); expect(keySet.keys.size).toEqual(10); expect([...keySet.keys.keys()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect([...keySet.keys.values()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 1, 0]); + expect([...keySet.keys.values()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 0, 1]); }); it("re-initialises a keyset, forwards, no overlap", () => { @@ -59,7 +59,7 @@ describe("KeySet", () => { expect([...keySet.keys.keys()]).toEqual([ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ]); - expect([...keySet.keys.values()]).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); it("re-initialises a keyset, backwards, with overlap", () => { @@ -69,7 +69,7 @@ describe("KeySet", () => { expect([...keySet.keys.keys()]).toEqual([ 10, 11, 12, 13, 14, 15, 16, 17, 8, 9, ]); - expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 9, 8]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); it("re-initialises a keyset, backwards, no overlap", () => { @@ -77,7 +77,7 @@ describe("KeySet", () => { keySet.reset({ from: 0, to: 10 }); expect(keySet.keys.size).toEqual(10); expect([...keySet.keys.keys()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - expect([...keySet.keys.values()]).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); diff --git a/vuu-ui/sample-apps/app-vuu-example/package.json b/vuu-ui/sample-apps/app-vuu-example/package.json index a0781411f..dd7970cb9 100644 --- a/vuu-ui/sample-apps/app-vuu-example/package.json +++ b/vuu-ui/sample-apps/app-vuu-example/package.json @@ -19,7 +19,8 @@ "@fontsource/open-sans": "^4.5.13", "@salt-ds/core": "1.13.2", "@salt-ds/lab": "1.0.0-alpha.15", - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-local": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-table-types": "0.0.26", "@finos/vuu-data-react": "0.0.26", "@finos/vuu-layout": "0.0.26", diff --git a/vuu-ui/sample-apps/feature-basket-trading/package.json b/vuu-ui/sample-apps/feature-basket-trading/package.json index 23f691808..1fd612d02 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/package.json +++ b/vuu-ui/sample-apps/feature-basket-trading/package.json @@ -21,7 +21,7 @@ "@finos/vuu-table-types": "0.0.26" }, "dependencies": { - "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-remote": "0.0.26", "@finos/vuu-data-react": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx index fa2cdf6cc..edfc2189d 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx @@ -38,7 +38,6 @@ export const BasketSelectorRow = ({ return (
=17.0.2", "react-dom": ">=17.0.2", diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index 1a3d92c22..dbac178c9 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -156,6 +156,8 @@ const ShellWithNewTheme = () => { }; export const ShellWithNewThemeAndLayoutManagement = () => { + document.cookie = `vuu-username=${user.username}`; + return ( diff --git a/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx new file mode 100644 index 000000000..471bcb816 --- /dev/null +++ b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx @@ -0,0 +1,143 @@ +import { ArrayDataSource } from "@finos/vuu-data-local"; +import { ArrayProxy, RowAtIndexFunc } from "@finos/vuu-data-test"; +import { DataSource } from "@finos/vuu-data-types"; +import { Toolbar } from "@finos/vuu-layout/src"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { noScrolling, Table } from "@finos/vuu-table"; +import { ColumnDescriptor, TableConfig } from "@finos/vuu-table-types"; +import { Button, Input } from "@salt-ds/core"; +import { ScrollingAPI } from "@finos/vuu-table"; +import { + ChangeEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from "react"; + +let displaySequence = 1; + +type RowGenerator = ( + columns: string[] +) => RowAtIndexFunc; + +export type ColumnGenerator = (count: number) => ColumnDescriptor[]; + +const columnGenerator: ColumnGenerator = (count) => { + return [{ name: "row number", width: 150 }].concat( + Array(count) + .fill(true) + .map((_, i) => { + const name = `column ${i + 1}`; + return { name, width: 150 }; + }) + ); +}; + +const rowGenerator: RowGenerator = (columns: string[]) => (index) => { + return [`row ${index + 1}`].concat( + Array(columns.length) + .fill(true) + .map((v, j) => `value ${j + 1} @ ${index + 1}`) + ); +}; + +export const SimpleTable = () => { + const config = useMemo( + () => ({ + columns: columnGenerator(5), + }), + [] + ); + + const dataSource = useMemo(() => { + const data = new ArrayProxy( + 1_000_000_000, + rowGenerator(config.columns.map((col) => col.name)) + ); + return new ArrayDataSource({ + columnDescriptors: config.columns, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data, + }); + }, [config.columns]); + + return ( + + ); +}; +SimpleTable.displaySequence = displaySequence++; + +export const TableScrollingAPI = () => { + const [rowInputValue, setRowInputValue] = useState(""); + const [scrollPosition, setScrollPosition] = useState(""); + const scrollingAPI = useRef(noScrolling); + + const handleChangeRowInput = useCallback< + ChangeEventHandler + >((evt) => { + const { value } = evt.target as HTMLInputElement; + setRowInputValue(value); + }, []); + const handleChangeScrollPosition = useCallback< + ChangeEventHandler + >((evt) => { + const { value } = evt.target as HTMLInputElement; + setScrollPosition(value); + }, []); + + const handleScrollToIndex = useCallback(() => { + const rowIndex = parseInt(rowInputValue); + if (!isNaN(rowIndex)) { + scrollingAPI.current.scrollToIndex(rowIndex); + } + }, [rowInputValue]); + + const handleScrollToPosition = useCallback(() => { + const rowIndex = parseInt(rowInputValue); + if (!isNaN(rowIndex)) { + scrollingAPI.current.scrollToIndex(rowIndex); + } + }, [rowInputValue]); + + const config = useMemo( + () => ({ + columns: columnGenerator(5), + zebraStripes: true, + }), + [] + ); + + const dataSource = useMemo(() => { + const data = new ArrayProxy( + 1_000_000_000, + rowGenerator(config.columns.map((col) => col.name)) + ); + return new ArrayDataSource({ + columnDescriptors: config.columns, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data, + }); + }, [config.columns]); + + return ( + <> +
+ + + + + + + + ); +}; +TableScrollingAPI.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Table/index.ts b/vuu-ui/showcase/src/examples/Table/index.ts index 55ccc428b..b936a857e 100644 --- a/vuu-ui/showcase/src/examples/Table/index.ts +++ b/vuu-ui/showcase/src/examples/Table/index.ts @@ -2,4 +2,5 @@ export * as TableList from "./TableList.examples"; export * as Table from "./Table.examples"; export * as BASKET from "./BASKET.examples"; export * as SIMUL from "./SIMUL.examples"; +export * as BigData from "./BigData.examples"; export * as TableLayout from "./TableLayout.examples"; diff --git a/vuu-ui/showcase/src/examples/UiControls/Calendar.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/Calendar.examples.tsx new file mode 100644 index 000000000..c08708885 --- /dev/null +++ b/vuu-ui/showcase/src/examples/UiControls/Calendar.examples.tsx @@ -0,0 +1,30 @@ +import { Calendar } from "@finos/vuu-ui-controls"; +import { CalendarDate, DateValue } from "@internationalized/date"; +import { useState } from "react"; + +let displaySequence = 1; + +export const DefaultCalendar = () => { + const [date, setDate] = useState(new CalendarDate(2024, 1, 1)); + const hoveredDate = new CalendarDate(2024, 1, 11); + + const isDayUnselectable = (d: DateValue) => { + return d.compare(new CalendarDate(2024, 1, 11)) === 0; + }; + + return ( +
+ setDate(d)} + hoveredDate={hoveredDate} + onHoveredDateChange={(_, d) => console.log({ d })} + isDayUnselectable={isDayUnselectable} + hideOutOfRangeDates={true} + /> +
+ ); +}; + +DefaultCalendar.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx new file mode 100644 index 000000000..9c75218f1 --- /dev/null +++ b/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx @@ -0,0 +1,45 @@ +import { DatePicker, DateRangePicker } from "@finos/vuu-ui-controls"; +import { CalendarDate, DateValue } from "@internationalized/date"; +import { useState } from "react"; + +let displaySequence = 1; + +export const DefaultDatePicker = () => { + const [date, setDate] = useState(new CalendarDate(2024, 1, 1)); + + const onBlur = () => console.log("onBlur"); + + return ( +
+ +
+ ); +}; +DefaultDatePicker.displaySequence = displaySequence++; + +export const DefaultDateRangePicker = () => { + const [date, setDate] = useState<{ + startDate?: DateValue; + endDate?: DateValue; + }>({ + startDate: new CalendarDate(2024, 1, 1), + }); + + return ( +
+ +
+ ); +}; +DefaultDateRangePicker.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index 02e11d8cf..1c4e6ad58 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -1,4 +1,6 @@ +export * as Calendar from "./Calendar.examples"; export * as ComboBox from "./Combobox.examples"; +export * as DatePicker from "./DatePicker.examples"; export * as DragDrop from "./DragDrop.examples"; export * as Dropdown from "./Dropdown.examples"; export * as EditableLabel from "./EditableLabel.examples"; diff --git a/vuu-ui/showcase/src/examples/html/components/BigScrollable.ts b/vuu-ui/showcase/src/examples/html/components/BigScrollable.ts new file mode 100644 index 000000000..e69de29bb diff --git a/vuu-ui/showcase/src/examples/utils/ArrayLike.ts b/vuu-ui/showcase/src/examples/utils/ArrayLike.ts deleted file mode 100644 index 950d057c7..000000000 --- a/vuu-ui/showcase/src/examples/utils/ArrayLike.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { WindowRange } from "@finos/vuu-utils"; - -export class ArrayLike { - public range: WindowRange; - public data: T[]; - public length = 0; - - constructor(input: T[], size: number, range: WindowRange) { - this.range = range; - this.data = input; - this.length = size; - - const handler = { - get: (target: ArrayLike, prop: string | symbol): any => { - if (prop === "length") { - return target.length; - } - if (prop === "slice") { - return target.slice; - } - if (prop === "toString") { - return target.debug; - } - if (typeof prop === "string") { - const index = parseInt(prop, 10); - if (!isNaN(index)) { - return target.getItem(index); - } - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return target.data[prop]; - }, - set: (target: ArrayLike, prop: string, newVal: any) => { - if (prop === "length") { - target.length = newVal; - return true; - } - if (prop === "data") { - target.data = newVal; - return true; - } - if (prop === "range") { - target.range = newVal; - return true; - } - throw Error(`ArrayLike is immutable except for length`); - }, - }; - return new Proxy(this, handler); - } - - getItem = (index: number) => { - const offset = this.range.from; - return this.data[index - offset]; - }; - - slice = (from: number, to: number) => { - const offset = this.range.from; - const out = []; - for (let i = from; i < to; i++) { - const index = i - offset; - if (this.data[index] !== undefined) { - out.push(this.data[index]); - } else { - out.push({ label: "???", value: "" }); - } - } - return out; - }; - - debug = () => { - return `ArrayLike: range ${this.range.from} - ${this.range.to} data - ${JSON.stringify(this.data[0])} - ${JSON.stringify( - this.data[this.data.length - 1] - )}`; - }; -} diff --git a/vuu-ui/showcase/src/examples/utils/index.ts b/vuu-ui/showcase/src/examples/utils/index.ts index a71de83f3..c98847882 100644 --- a/vuu-ui/showcase/src/examples/utils/index.ts +++ b/vuu-ui/showcase/src/examples/utils/index.ts @@ -1,5 +1,3 @@ -export * from "./ArrayLike"; -export * from "./ArrayProxy"; export * from "./ErrorDisplay"; export * from "./useAutoLoginToVuuServer"; export * from "./useTestDataSource";