@stoker-platform/web-app 0.5.137 → 0.5.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/src/List.tsx +159 -174
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.139
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: improve list chart animation
8
+
9
+ ## 0.5.138
10
+
11
+ ### Patch Changes
12
+
13
+ - fix: improve list selection
14
+
3
15
  ## 0.5.137
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoker-platform/web-app",
3
- "version": "0.5.137",
3
+ "version": "0.5.139",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "scripts": {
package/src/List.tsx CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  Chart,
5
5
  CollectionMeta,
6
6
  CollectionSchema,
7
+ Filter,
7
8
  FormList,
8
9
  Metric,
9
10
  RelationList,
@@ -100,6 +101,96 @@ import { getSortingValue } from "./utils/getSortingValue"
100
101
 
101
102
  export const description = "A list of records in a table. The content area has a search bar in the header."
102
103
 
104
+ type AreaChartData = { date: string; metric1: number; metric2?: number }
105
+
106
+ interface AreaMetricChartProps {
107
+ chartConfig: ChartConfig
108
+ chartData: AreaChartData[]
109
+ timeRange: string | undefined
110
+ timezone: string
111
+ showMetric2: boolean
112
+ showLegend: boolean
113
+ }
114
+
115
+ const AreaMetricChart = ({
116
+ chartConfig,
117
+ chartData,
118
+ timeRange,
119
+ timezone,
120
+ showMetric2,
121
+ showLegend,
122
+ }: AreaMetricChartProps) => {
123
+ const filteredData = useMemo(() => {
124
+ return chartData?.filter((item) => {
125
+ const date = new Date(item.date)
126
+ let daysToSubtract = 90
127
+ if (timeRange === "30d") {
128
+ daysToSubtract = 30
129
+ } else if (timeRange === "7d") {
130
+ daysToSubtract = 7
131
+ }
132
+ const startDate = DateTime.now().setZone(timezone).toJSDate()
133
+ const endDate = DateTime.now().setZone(timezone).toJSDate()
134
+ startDate.setDate(startDate.getDate() - daysToSubtract)
135
+ return date >= startDate && date <= endDate
136
+ })
137
+ }, [chartData, timeRange, timezone])
138
+
139
+ return (
140
+ <ChartContainer config={chartConfig} className="aspect-auto h-[173px] w-full">
141
+ <AreaChart data={filteredData}>
142
+ <defs>
143
+ <linearGradient id="fill1" x1="0" y1="0" x2="0" y2="1">
144
+ <stop offset="5%" stopColor="var(--chart-dark)" stopOpacity={0.8} />
145
+ <stop offset="95%" stopColor="var(--chart-light)" stopOpacity={0.1} />
146
+ </linearGradient>
147
+ <linearGradient id="fill2" x1="0" y1="0" x2="0" y2="1">
148
+ <stop offset="5%" stopColor="var(--chart-dark)" stopOpacity={0.8} />
149
+ <stop offset="95%" stopColor="var(--chart-light)" stopOpacity={0.1} />
150
+ </linearGradient>
151
+ </defs>
152
+ <CartesianGrid vertical={false} className="last:opacity-0" />
153
+ <XAxis
154
+ dataKey="date"
155
+ tickLine={false}
156
+ axisLine={false}
157
+ tickMargin={8}
158
+ minTickGap={32}
159
+ tickFormatter={(value) => {
160
+ const date = new Date(value)
161
+ return date.toLocaleDateString("en-US", {
162
+ month: "short",
163
+ day: "numeric",
164
+ })
165
+ }}
166
+ />
167
+ <YAxis hide padding={{ top: 16 }} />
168
+ <ChartTooltip
169
+ cursor={false}
170
+ content={
171
+ <ChartTooltipContent
172
+ labelFormatter={(value) => {
173
+ return new Date(value).toLocaleDateString("en-US", {
174
+ month: "short",
175
+ day: "numeric",
176
+ })
177
+ }}
178
+ indicator="dot"
179
+ />
180
+ }
181
+ />
182
+ <Area dataKey="metric1" type="natural" fill="url(#fill1)" stroke="var(--chart-dark)" stackId="a" />
183
+ {showMetric2 && (
184
+ <Area dataKey="metric2" type="natural" fill="url(#fill2)" stroke="var(--chart-light)" stackId="a" />
185
+ )}
186
+ {showLegend && <ChartLegend className="pb-3" content={<ChartLegendContent />} />}
187
+ </AreaChart>
188
+ </ChartContainer>
189
+ )
190
+ }
191
+
192
+ const AreaMetricChartMemo = memo(AreaMetricChart)
193
+
103
194
  interface ListProps {
104
195
  collection: CollectionSchema
105
196
  list: StokerRecord[] | undefined
@@ -552,9 +643,18 @@ export function List({
552
643
  return list || []
553
644
  }, [isPreloadCacheEnabled, isServerReadOnly, list, search])
554
645
 
646
+ const selectedRecords = useMemo(() => {
647
+ const selectedIds = Object.keys(rowSelection)
648
+ if (selectedIds.length === 0) return []
649
+ return selectedIds
650
+ .map((id) => searchList.find((record) => record.id === id))
651
+ .filter((record): record is StokerRecord => record !== undefined)
652
+ }, [rowSelection, searchList])
653
+
555
654
  const table = useReactTable<StokerRecord>({
556
655
  data: searchList,
557
656
  columns,
657
+ getRowId: (row) => row.id,
558
658
  getCoreRowModel: getCoreRowModel(),
559
659
  getPaginationRowModel: getPaginationRowModel(),
560
660
  onSortingChange: (sortingUpdater) => {
@@ -1080,11 +1180,7 @@ export function List({
1080
1180
  const titles = await getCachedConfigValue(customization, ["collections", labels.collection, "admin", "titles"])
1081
1181
  const recordTitle = titles?.record || labels.record
1082
1182
 
1083
- Object.keys(rowSelection).forEach((row) => {
1084
- const key = row as unknown as number
1085
- if (!list) return
1086
- // eslint-disable-next-line security/detect-object-injection
1087
- const record = list[key]
1183
+ selectedRecords.forEach((record) => {
1088
1184
  if (isGlobalLoading.get(record.id)?.server) {
1089
1185
  alert(
1090
1186
  `Record ${record.id} is currently being written to the server. Please wait for it to finish before deleting.`,
@@ -1121,10 +1217,19 @@ export function List({
1121
1217
  removeOptimisticDelete(labels.collection, record.id)
1122
1218
  }
1123
1219
  })
1220
+ setRowSelection({})
1124
1221
  toast({
1125
- description: `Deleting ${Object.keys(rowSelection).length} ${Object.keys(rowSelection).length > 1 ? "records" : "record"}.`,
1222
+ description: `Deleting ${selectedRecords.length} ${selectedRecords.length > 1 ? "records" : "record"}.`,
1126
1223
  })
1127
- }, [collection, rowSelection, list, isGlobalLoading, softDeleteField, softDeleteTimestampField, recordTitleField])
1224
+ }, [
1225
+ collection,
1226
+ selectedRecords,
1227
+ isGlobalLoading,
1228
+ softDeleteField,
1229
+ softDeleteTimestampField,
1230
+ recordTitleField,
1231
+ isServerReadOnly,
1232
+ ])
1128
1233
 
1129
1234
  const sortingField = getField(fields, sorting[0]?.id)
1130
1235
 
@@ -1318,6 +1423,10 @@ export function List({
1318
1423
  )
1319
1424
  }, [metrics])
1320
1425
 
1426
+ const statusFilter = useMemo(() => {
1427
+ return filters?.find((filter: Filter) => filter.type === "status")
1428
+ }, [filters])
1429
+
1321
1430
  return (
1322
1431
  <>
1323
1432
  {!formList && (meta?.title || collectionTitle) && (
@@ -1408,11 +1517,7 @@ export function List({
1408
1517
 
1409
1518
  const chartData =
1410
1519
  // eslint-disable-next-line security/detect-object-injection
1411
- (metricsValues[index] as {
1412
- date: string
1413
- metric1: number
1414
- metric2?: number
1415
- }[]) || []
1520
+ (metricsValues[index] as AreaChartData[]) || []
1416
1521
 
1417
1522
  const chartConfig = {
1418
1523
  visitors: {
@@ -1428,22 +1533,6 @@ export function List({
1428
1533
  },
1429
1534
  } satisfies ChartConfig
1430
1535
 
1431
- const filteredData = chartData?.filter((item) => {
1432
- const date = new Date(item.date)
1433
- let daysToSubtract = 90
1434
- // eslint-disable-next-line security/detect-object-injection
1435
- if (timeRange[metricTitle] === "30d") {
1436
- daysToSubtract = 30
1437
- // eslint-disable-next-line security/detect-object-injection
1438
- } else if (timeRange[metricTitle] === "7d") {
1439
- daysToSubtract = 7
1440
- }
1441
- const startDate = DateTime.now().setZone(timezone).toJSDate()
1442
- const endDate = DateTime.now().setZone(timezone).toJSDate()
1443
- startDate.setDate(startDate.getDate() - daysToSubtract)
1444
- return date >= startDate && date <= endDate
1445
- })
1446
-
1447
1536
  return (
1448
1537
  <div key={`metric-${index}`} className="grid gap-3 flex-1 min-w-0">
1449
1538
  <Card className="pt-0 w-full" key={`metric-${index}`}>
@@ -1485,111 +1574,15 @@ export function List({
1485
1574
  </Select>
1486
1575
  </CardHeader>
1487
1576
  <CardContent className="flex-1 px-2 sm:px-6 pb-0">
1488
- <ChartContainer
1489
- config={chartConfig}
1490
- className="aspect-auto h-[173px] w-full"
1491
- >
1492
- <AreaChart data={filteredData}>
1493
- <defs>
1494
- <linearGradient
1495
- id="fill1"
1496
- x1="0"
1497
- y1="0"
1498
- x2="0"
1499
- y2="1"
1500
- >
1501
- <stop
1502
- offset="5%"
1503
- stopColor="var(--chart-dark)"
1504
- stopOpacity={0.8}
1505
- />
1506
- <stop
1507
- offset="95%"
1508
- stopColor="var(--chart-light)"
1509
- stopOpacity={0.1}
1510
- />
1511
- </linearGradient>
1512
- <linearGradient
1513
- id="fill2"
1514
- x1="0"
1515
- y1="0"
1516
- x2="0"
1517
- y2="1"
1518
- >
1519
- <stop
1520
- offset="5%"
1521
- stopColor="var(--chart-dark)"
1522
- stopOpacity={0.8}
1523
- />
1524
- <stop
1525
- offset="95%"
1526
- stopColor="var(--chart-light)"
1527
- stopOpacity={0.1}
1528
- />
1529
- </linearGradient>
1530
- </defs>
1531
- <CartesianGrid
1532
- vertical={false}
1533
- className="last:opacity-0"
1534
- />
1535
- <XAxis
1536
- dataKey="date"
1537
- tickLine={false}
1538
- axisLine={false}
1539
- tickMargin={8}
1540
- minTickGap={32}
1541
- tickFormatter={(value) => {
1542
- const date = new Date(value)
1543
- return date.toLocaleDateString(
1544
- "en-US",
1545
- {
1546
- month: "short",
1547
- day: "numeric",
1548
- },
1549
- )
1550
- }}
1551
- />
1552
- <YAxis hide padding={{ top: 16 }} />
1553
- <ChartTooltip
1554
- cursor={false}
1555
- content={
1556
- <ChartTooltipContent
1557
- labelFormatter={(value) => {
1558
- return new Date(
1559
- value,
1560
- ).toLocaleDateString("en-US", {
1561
- month: "short",
1562
- day: "numeric",
1563
- })
1564
- }}
1565
- indicator="dot"
1566
- />
1567
- }
1568
- />
1569
- <Area
1570
- dataKey="metric1"
1571
- type="natural"
1572
- fill="url(#fill1)"
1573
- stroke="var(--chart-dark)"
1574
- stackId="a"
1575
- />
1576
- {metricField2 && (
1577
- <Area
1578
- dataKey="metric2"
1579
- type="natural"
1580
- fill="url(#fill2)"
1581
- stroke="var(--chart-light)"
1582
- stackId="a"
1583
- />
1584
- )}
1585
- {metricField1 && metricField2 && (
1586
- <ChartLegend
1587
- className="pb-3"
1588
- content={<ChartLegendContent />}
1589
- />
1590
- )}
1591
- </AreaChart>
1592
- </ChartContainer>
1577
+ <AreaMetricChartMemo
1578
+ chartConfig={chartConfig}
1579
+ chartData={chartData}
1580
+ // eslint-disable-next-line security/detect-object-injection
1581
+ timeRange={timeRange[metricTitle]}
1582
+ timezone={timezone}
1583
+ showMetric2={!!metricField2}
1584
+ showLegend={!!(metricField1 && metricField2)}
1585
+ />
1593
1586
  </CardContent>
1594
1587
  </div>
1595
1588
  </Card>
@@ -1662,6 +1655,7 @@ export function List({
1662
1655
  path={[labels.collection]}
1663
1656
  onSuccess={() => {
1664
1657
  setIsUpdateDialogOpen(false)
1658
+ setRowSelection({})
1665
1659
  setTimeout(() => {
1666
1660
  updateButtonRef.current?.focus()
1667
1661
  }, 0)
@@ -1672,17 +1666,7 @@ export function List({
1672
1666
  onSaveRecord={() => {
1673
1667
  setOptimisticList()
1674
1668
  }}
1675
- rowSelection={Object.keys(rowSelection)
1676
- .map((row) => {
1677
- const key = row as unknown as number
1678
- if (!list) return undefined
1679
- // eslint-disable-next-line security/detect-object-injection
1680
- return list[key]
1681
- })
1682
- .filter(
1683
- (record): record is StokerRecord =>
1684
- record !== undefined,
1685
- )}
1669
+ rowSelection={selectedRecords}
1686
1670
  />
1687
1671
  </div>
1688
1672
  </div>
@@ -1690,36 +1674,37 @@ export function List({
1690
1674
  </div>,
1691
1675
  document.body,
1692
1676
  )}
1693
- {collectionAccess("Delete", collectionPermissions) && (
1694
- <AlertDialog>
1695
- <AlertDialogTrigger asChild>
1696
- <Button
1697
- type="button"
1698
- variant="destructive"
1699
- disabled={
1700
- connectionStatus === "offline" &&
1701
- (disableOfflineDelete || serverWriteOnly || collection.auth)
1702
- }
1703
- >
1704
- Delete Selected
1705
- </Button>
1706
- </AlertDialogTrigger>
1707
- <AlertDialogContent>
1708
- <AlertDialogHeader>
1709
- <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
1710
- <AlertDialogDescription className="hidden">
1711
- This action delete the selected records.
1712
- </AlertDialogDescription>
1713
- </AlertDialogHeader>
1714
- <AlertDialogFooter>
1715
- <AlertDialogCancel>Cancel</AlertDialogCancel>
1716
- <AlertDialogAction onClick={handleDelete}>
1717
- Delete selected
1718
- </AlertDialogAction>
1719
- </AlertDialogFooter>
1720
- </AlertDialogContent>
1721
- </AlertDialog>
1722
- )}
1677
+ {collectionAccess("Delete", collectionPermissions) &&
1678
+ !(statusFilter?.value === "trash") && (
1679
+ <AlertDialog>
1680
+ <AlertDialogTrigger asChild>
1681
+ <Button
1682
+ type="button"
1683
+ variant="destructive"
1684
+ disabled={
1685
+ connectionStatus === "offline" &&
1686
+ (disableOfflineDelete || serverWriteOnly || collection.auth)
1687
+ }
1688
+ >
1689
+ Delete Selected
1690
+ </Button>
1691
+ </AlertDialogTrigger>
1692
+ <AlertDialogContent>
1693
+ <AlertDialogHeader>
1694
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
1695
+ <AlertDialogDescription className="hidden">
1696
+ This action delete the selected records.
1697
+ </AlertDialogDescription>
1698
+ </AlertDialogHeader>
1699
+ <AlertDialogFooter>
1700
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
1701
+ <AlertDialogAction onClick={handleDelete}>
1702
+ Delete selected
1703
+ </AlertDialogAction>
1704
+ </AlertDialogFooter>
1705
+ </AlertDialogContent>
1706
+ </AlertDialog>
1707
+ )}
1723
1708
  </div>
1724
1709
  )}
1725
1710
  {pagesLoaded && list && (