From a6d1a5321caa3c1bd58876406e1565b88e851ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ra=C4=8D=C3=A1k?= Date: Thu, 30 Jan 2025 16:03:35 +0100 Subject: [PATCH] Add admin for multidimensional data pages --- adminSiteClient/AdminApp.tsx | 542 ++++++++++-------- adminSiteClient/AdminSidebar.tsx | 6 + adminSiteClient/ChartViewIndexPage.tsx | 5 +- adminSiteClient/ImagesIndexPage.tsx | 4 +- adminSiteClient/MultiDimIndexPage.tsx | 255 ++++++++ adminSiteServer/apiRouter.ts | 14 +- adminSiteServer/apiRoutes/charts.ts | 4 +- adminSiteServer/apiRoutes/mdims.ts | 72 ++- adminSiteServer/chartConfigHelpers.ts | 8 +- adminSiteServer/multiDim.ts | 55 +- db/model/MultiDimDataPage.ts | 10 + db/model/MultiDimXChartConfigs.ts | 2 +- package.json | 1 + .../types/src/dbTypes/ChartConfigs.ts | 4 +- .../types/src/domainTypes/Various.ts | 4 +- yarn.lock | 36 ++ 16 files changed, 731 insertions(+), 291 deletions(-) create mode 100644 adminSiteClient/MultiDimIndexPage.tsx diff --git a/adminSiteClient/AdminApp.tsx b/adminSiteClient/AdminApp.tsx index 4f61bf023c0..d1f41e61227 100644 --- a/adminSiteClient/AdminApp.tsx +++ b/adminSiteClient/AdminApp.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import * as React from "react" import { Admin } from "./Admin.js" import { ChartEditorPage } from "./ChartEditorPage.js" @@ -45,6 +46,15 @@ import { IndicatorChartEditorPage } from "./IndicatorChartEditorPage.js" import { ChartViewEditorPage } from "./ChartViewEditorPage.js" import { ChartViewIndexPage } from "./ChartViewIndexPage.js" import { ImageIndexPage } from "./ImagesIndexPage.js" +import { MultiDimIndexPage } from "./MultiDimIndexPage.js" + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}) @observer class AdminErrorMessage extends React.Component<{ admin: Admin }> { @@ -111,262 +121,284 @@ export class AdminApp extends React.Component<{ const { admin, gitCmsBranchName } = this.props return ( - - -
- - - - ( - - )} - /> - - ( - - )} - /> - ( - - )} - /> - - - ( - - )} - /> - - ( - - + + +
+ + + + ( + - - )} - /> - ( - - - - )} - /> - } - /> - } - /> - ( - - )} - /> - - ( - - )} - /> - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - - ( - - )} - /> - - - - ( - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - - - - - } - /> - - -
-
-
+ )} + /> + + ( + + )} + /> + ( + + )} + /> + + + ( + + )} + /> + + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + )} + /> + } + /> + ( + + )} + /> + + ( + + )} + /> + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + + ( + + )} + /> + + + + ( + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + + + } + /> + +
+
+
+
+ ) } } diff --git a/adminSiteClient/AdminSidebar.tsx b/adminSiteClient/AdminSidebar.tsx index 7b21ab70e40..170ab15c8da 100644 --- a/adminSiteClient/AdminSidebar.tsx +++ b/adminSiteClient/AdminSidebar.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { faChartBar, + faChartLine, faFile, faTable, faSkullCrossbones, @@ -38,6 +39,11 @@ export const AdminSidebar = (): React.ReactElement => ( Narrative charts +
  • + + Multi-dims + +
  • Posts diff --git a/adminSiteClient/ChartViewIndexPage.tsx b/adminSiteClient/ChartViewIndexPage.tsx index 7021d33bf82..bdea94a6376 100644 --- a/adminSiteClient/ChartViewIndexPage.tsx +++ b/adminSiteClient/ChartViewIndexPage.tsx @@ -1,11 +1,10 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react" import * as React from "react" -import { Button, Flex, Input, Space, Table } from "antd" +import { Button, Flex, Input, Space, Table, TableColumnsType } from "antd" import { AdminLayout } from "./AdminLayout.js" import { AdminAppContext } from "./AdminAppContext.js" import { Timeago } from "./Forms.js" -import { ColumnsType } from "antd/es/table/InternalTable.js" import { ApiChartViewOverview } from "../adminShared/AdminTypes.js" import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings.js" import { Link } from "./Link.js" @@ -20,7 +19,7 @@ function createColumns(ctx: { text: string | null | undefined ) => React.ReactElement | string deleteFn: (chartViewId: number) => void -}): ColumnsType { +}): TableColumnsType { return [ { title: "Preview", diff --git a/adminSiteClient/ImagesIndexPage.tsx b/adminSiteClient/ImagesIndexPage.tsx index 09a4400c863..2eb969c7b00 100644 --- a/adminSiteClient/ImagesIndexPage.tsx +++ b/adminSiteClient/ImagesIndexPage.tsx @@ -14,6 +14,7 @@ import { Popconfirm, Popover, Table, + TableColumnsType, Upload, notification, } from "antd" @@ -21,7 +22,6 @@ import { AdminLayout } from "./AdminLayout.js" import { AdminAppContext } from "./AdminAppContext.js" import { DbEnrichedImageWithUserId, DbPlainUser } from "@ourworldindata/types" import { Timeago } from "./Forms.js" -import { ColumnsType } from "antd/es/table/InternalTable.js" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faClose, @@ -246,7 +246,7 @@ function createColumns({ users: UserMap usage: Dictionary notificationApi: NotificationInstance -}): ColumnsType { +}): TableColumnsType { return [ { title: "Preview", diff --git a/adminSiteClient/MultiDimIndexPage.tsx b/adminSiteClient/MultiDimIndexPage.tsx new file mode 100644 index 00000000000..4007df40768 --- /dev/null +++ b/adminSiteClient/MultiDimIndexPage.tsx @@ -0,0 +1,255 @@ +import { + useMutation, + UseMutationResult, + useQuery, + useQueryClient, +} from "@tanstack/react-query" +import { createContext, useContext, useEffect, useMemo, useState } from "react" +import { + Flex, + Input, + Popconfirm, + Switch, + Table, + TableColumnsType, + notification, +} from "antd" +import { Admin } from "./Admin.js" +import { AdminLayout } from "./AdminLayout.js" +import { AdminAppContext } from "./AdminAppContext.js" +import urljoin from "url-join" +import { + BAKED_BASE_URL, + BAKED_GRAPHER_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL, +} from "../settings/clientSettings.js" +import { dayjs, Json } from "@ourworldindata/utils" +import { Link } from "./Link.js" + +type ApiMultiDim = { + id: number + title: string + slug: string + updatedAt: string + published: boolean +} + +type MultiDim = Omit & { + updatedAt: Date +} + +function createColumns( + publishMutation: UseMutationResult< + unknown, + unknown, + { id: number; published: boolean }, + unknown + > +): TableColumnsType { + return [ + { + title: "Preview", + dataIndex: "thumbnailUrl", + width: 100, + key: "thumbnail", + render: (_, record) => { + return ( +
    + + Preview + +
    + ) + }, + }, + { + title: "Title", + dataIndex: "title", + key: "title", + render: (text, record) => + record.published ? ( + + {text} + + ) : ( + text + ), + sorter: (a, b) => a.title.localeCompare(b.title), + }, + { + title: "Slug", + dataIndex: "slug", + key: "slug", + // render: (text, record) => { + // const handleSave = async () => { + // await api.patchMdim(record.id, { slug: value }) + // setIsEditing(false) + // notificationApi.success({ + // message: "Slug updated successfully", + // placement: "bottomRight", + // }) + // } + + // return isEditing ? ( + // setValue(e.target.value)} + // onPressEnter={handleSave} + // onBlur={handleSave} + // autoFocus + // /> + // ) : ( + //
    setIsEditing(true)} + // style={{ cursor: "pointer" }} + // > + // {value} + // + // (click to edit) + // + //
    + // ) + // }, + }, + { + title: "Last updated", + dataIndex: "updatedAt", + key: "updatedAt", + defaultSortOrder: "descend", + render: (updatedAt) => { + const date = dayjs(updatedAt) + return {date.fromNow()} + }, + sorter: (a, b) => a.updatedAt.getTime() - b.updatedAt.getTime(), + }, + { + title: "Published", + dataIndex: "published", + key: "published", + align: "center", + render: (published, record) => ( + + publishMutation.mutate({ + id: record.id, + published: !published, + }) + } + okText="Yes" + cancelText="No" + > + + + ), + }, + ] +} + +const NotificationContext = createContext(null) + +async function fetchMultiDims(admin: Admin) { + const result = await admin.getJSON<{ multiDims: MultiDim[] }>( + "/api/multi-dims.json" + ) + return result.multiDims.map((mdim) => ({ + ...mdim, + updatedAt: new Date(mdim.updatedAt), // TODO: Is the date correct in staging/prod? + })) +} + +async function patchMultiDim(admin: Admin, id: number, data: Json) { + return await admin.requestJSON(`/api/multi-dims/${id}`, data, "PATCH") +} + +export function MultiDimIndexPage() { + const { admin } = useContext(AdminAppContext) + const [notificationApi, notificationContextHolder] = + notification.useNotification() + const [searchValue, setSearchValue] = useState("") + const queryClient = useQueryClient() + + useEffect(() => { + admin.loadingIndicatorSetting = "off" + return () => { + admin.loadingIndicatorSetting = "default" + } + }, [admin]) + + const { data } = useQuery({ + queryKey: ["multiDims"], + queryFn: () => fetchMultiDims(admin), + }) + + const mutation = useMutation({ + mutationFn: ({ id, published }: { id: number; published: boolean }) => + patchMultiDim(admin, id, { published }), + onSuccess: (_, { published }) => { + notificationApi.info({ + message: published ? "Published" : "Unpublished", + description: Check deploy progress, + placement: "bottomRight", + }) + return queryClient.invalidateQueries({ queryKey: ["multiDims"] }) + }, + }) + + const filteredMdims = useMemo( + () => + data?.filter((mdim) => + [mdim.title, mdim.slug].some((field) => + field.toLowerCase().includes(searchValue.toLowerCase()) + ) + ), + [data, searchValue] + ) + + const columns = createColumns(mutation) + + return ( + + + {notificationContextHolder} +
    + + setSearchValue(e.target.value)} + style={{ width: 500, marginBottom: 20 }} + /> + + x.id} + /> + + + + ) +} diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index 7a57a01899c..329447e3f80 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -41,7 +41,11 @@ import { deleteImageHandler, getImageUsageHandler, } from "./apiRoutes/images.js" -import { handleMultiDimDataPageRequest } from "./apiRoutes/mdims.js" +import { + handlePutMultiDim, + handleGetMultiDims, + handlePatchMultiDim, +} from "./apiRoutes/mdims.js" import { fetchAllWork, fetchNamespaces, @@ -258,11 +262,9 @@ deleteRouteWithRWTransaction(apiRouter, "/images/:id", deleteImageHandler) getRouteWithROTransaction(apiRouter, "/images/usage", getImageUsageHandler) // Mdim routes -putRouteWithRWTransaction( - apiRouter, - "/multi-dim/:slug", - handleMultiDimDataPageRequest -) +getRouteWithROTransaction(apiRouter, "/multi-dims.json", handleGetMultiDims) +putRouteWithRWTransaction(apiRouter, "/multi-dim/:slug", handlePutMultiDim) +patchRouteWithRWTransaction(apiRouter, "/multi-dims/:id", handlePatchMultiDim) // Misc routes getRouteWithROTransaction(apiRouter, "/all-work", fetchAllWork) diff --git a/adminSiteServer/apiRoutes/charts.ts b/adminSiteServer/apiRoutes/charts.ts index cc5a1f17c6b..ff528c3fb6c 100644 --- a/adminSiteServer/apiRoutes/charts.ts +++ b/adminSiteServer/apiRoutes/charts.ts @@ -210,7 +210,7 @@ const updateExistingChart = async ( shouldInherit?: boolean } ): Promise<{ - chartConfigId: Base64String + chartConfigId: string patchConfig: GrapherInterface fullConfig: GrapherInterface }> => { @@ -367,7 +367,7 @@ export const saveGrapher = async ( // Execute the actual database update or creation let chartId: number - let chartConfigId: Base64String + let chartConfigId: string let patchConfig: GrapherInterface let fullConfig: GrapherInterface if (existingConfig) { diff --git a/adminSiteServer/apiRoutes/mdims.ts b/adminSiteServer/apiRoutes/mdims.ts index 7880662928e..0532290a637 100644 --- a/adminSiteServer/apiRoutes/mdims.ts +++ b/adminSiteServer/apiRoutes/mdims.ts @@ -1,17 +1,57 @@ -import { JsonError, MultiDimDataPageConfigRaw } from "@ourworldindata/types" -import { isMultiDimDataPagePublished } from "../../db/model/MultiDimDataPage.js" -import { isValidSlug } from "../../serverUtils/serverUtil.js" +import { + JsonError, + MultiDimDataPageConfigRaw, + MultiDimDataPagesTableName, +} from "@ourworldindata/types" +import { + getMultiDimDataPageById, + isMultiDimDataPagePublished, +} from "../../db/model/MultiDimDataPage.js" +import { expectInt, isValidSlug } from "../../serverUtils/serverUtil.js" import { FEATURE_FLAGS, FeatureFlagFeature, } from "../../settings/clientSettings.js" -import { createMultiDimConfig } from "../multiDim.js" +import { createMultiDimConfig, setMultiDimPublished } from "../multiDim.js" import { triggerStaticBuild } from "./routeUtils.js" import { Request } from "../authentication.js" import * as db from "../../db/db.js" import e from "express" -export async function handleMultiDimDataPageRequest( +export async function handleGetMultiDims( + _req: Request, + _res: e.Response>, + trx: db.KnexReadonlyTransaction +) { + try { + const results = await db.knexRaw<{ + id: number + slug: string + title: string + updatedAt: string + published: number + }>( + trx, + `-- sql + SELECT + id, + slug, + config->>'$.title.title' as title, + updatedAt, + published + FROM ${MultiDimDataPagesTableName}` + ) + const multiDims = results.map((row) => ({ + ...row, + published: Boolean(row.published), + })) + return { multiDims } + } catch (error) { + throw new JsonError("Failed to fetch multi-dims", 500, { cause: error }) + } +} + +export async function handlePutMultiDim( req: Request, res: e.Response>, trx: db.KnexReadWriteTransaction @@ -22,6 +62,7 @@ export async function handleMultiDimDataPageRequest( } const rawConfig = req.body as MultiDimDataPageConfigRaw const id = await createMultiDimConfig(trx, slug, rawConfig) + if ( FEATURE_FLAGS.has(FeatureFlagFeature.MultiDimDataPage) && (await isMultiDimDataPagePublished(trx, slug)) @@ -33,3 +74,24 @@ export async function handleMultiDimDataPageRequest( } return { success: true, id } } + +export async function handlePatchMultiDim( + req: Request, + res: e.Response>, + trx: db.KnexReadWriteTransaction +) { + const id = expectInt(req.params.id) + const multiDim = await getMultiDimDataPageById(trx, id) + if (!multiDim) { + throw new JsonError(`Multi-dimensional data page not found`, 404) + } + const { published } = req.body + if (published !== multiDim.published) { + await setMultiDimPublished(trx, multiDim, published) + await triggerStaticBuild( + res.locals.user, + `${published ? "Publishing" : "Unpublishing"} multidimensional chart ${multiDim.slug}` + ) + } + return { success: true } +} diff --git a/adminSiteServer/chartConfigHelpers.ts b/adminSiteServer/chartConfigHelpers.ts index 970357d2115..f4ef854c079 100644 --- a/adminSiteServer/chartConfigHelpers.ts +++ b/adminSiteServer/chartConfigHelpers.ts @@ -28,7 +28,7 @@ import { export const retrieveChartConfigFromDbAndSaveToR2 = async ( knex: db.KnexReadonlyTransaction, - chartConfigId: Base64String, + chartConfigId: string, r2Path?: { directory: R2GrapherConfigDirectory; filename: string } ) => { // We need to get the full config and the md5 hash from the database instead of @@ -69,7 +69,7 @@ export const retrieveChartConfigFromDbAndSaveToR2 = async ( export const updateChartConfigInDbAndR2 = async ( knex: db.KnexReadWriteTransaction, - chartConfigId: Base64String, + chartConfigId: string, patchConfig: GrapherInterface, fullConfig: GrapherInterface ) => { @@ -86,11 +86,11 @@ export const updateChartConfigInDbAndR2 = async ( export const saveNewChartConfigInDbAndR2 = async ( knex: db.KnexReadWriteTransaction, - chartConfigId: Base64String | undefined, + chartConfigId: string | undefined, patchConfig: GrapherInterface, fullConfig: GrapherInterface ) => { - chartConfigId ??= uuidv7() as Base64String + chartConfigId ??= uuidv7() await knex(ChartConfigsTableName).insert({ id: chartConfigId, diff --git a/adminSiteServer/multiDim.ts b/adminSiteServer/multiDim.ts index d018dcbaa55..3c761d2aa24 100644 --- a/adminSiteServer/multiDim.ts +++ b/adminSiteServer/multiDim.ts @@ -7,6 +7,7 @@ import { import { Base64String, ChartConfigsTableName, + DbEnrichedMultiDimDataPage, DbPlainMultiDimDataPage, DbPlainMultiDimXChartConfig, DbRawChartConfig, @@ -14,12 +15,14 @@ import { IndicatorConfig, IndicatorEntryBeforePreProcessing, IndicatorsBeforePreProcessing, + JsonError, MultiDimDataPageConfigEnriched, MultiDimDataPageConfigPreProcessed, MultiDimDataPageConfigRaw, MultiDimDataPagesTableName, MultiDimDimensionChoices, MultiDimXChartConfigsTableName, + parseChartConfigsRow, View, } from "@ourworldindata/types" import { @@ -28,10 +31,7 @@ import { slugify, } from "@ourworldindata/utils" import * as db from "../db/db.js" -import { - isMultiDimDataPagePublished, - upsertMultiDimDataPage, -} from "../db/model/MultiDimDataPage.js" +import { upsertMultiDimDataPage } from "../db/model/MultiDimDataPage.js" import { upsertMultiDimXChartConfigs } from "../db/model/MultiDimXChartConfigs.js" import { getMergedGrapherConfigsForVariables, @@ -250,8 +250,6 @@ export async function createMultiDimConfig( ) const reusedChartConfigIds = new Set() const { grapherConfigSchema } = config - // TODO: Remove when we build a way to publish mdims in the admin. - const isPublished = await isMultiDimDataPagePublished(knex, slug) const enrichedViews = await Promise.all( config.views.map(async (view) => { @@ -263,9 +261,6 @@ export async function createMultiDimConfig( selectedEntityNames: config.defaultSelection ?? [], slug, } - if (isPublished) { - mainGrapherConfig.isPublished = true - } let viewGrapherConfig = {} if (view.config) { viewGrapherConfig = grapherConfigSchema @@ -329,3 +324,45 @@ export async function createMultiDimConfig( } return multiDimId } + +async function getChartConfigsByIds( + knex: db.KnexReadonlyTransaction, + ids: string[] +) { + const rows = await knex(ChartConfigsTableName) + .select("id", "patch", "full") + .whereIn("id", ids) + return new Map(rows.map((row) => [row.id, parseChartConfigsRow(row)])) +} + +export async function setMultiDimPublished( + knex: db.KnexReadWriteTransaction, + multiDim: DbEnrichedMultiDimDataPage, + published: boolean +) { + const chartConfigs = await getChartConfigsByIds( + knex, + multiDim.config.views.map((view) => view.fullConfigId) + ) + + await Promise.all( + multiDim.config.views.map(async (view) => { + const { fullConfigId: chartConfigId } = view + const chartConfig = chartConfigs.get(chartConfigId) + if (!chartConfig) { + throw new JsonError( + `Chart config not found id=${chartConfigId}`, + 404 + ) + } + const { patch, full } = chartConfig + patch.isPublished = published + full.isPublished = published + await updateChartConfigInDbAndR2(knex, chartConfigId, patch, full) + }) + ) + + await knex(MultiDimDataPagesTableName) + .where({ id: multiDim.id }) + .update({ published }) +} diff --git a/db/model/MultiDimDataPage.ts b/db/model/MultiDimDataPage.ts index 25161092c22..fea4ef0edae 100644 --- a/db/model/MultiDimDataPage.ts +++ b/db/model/MultiDimDataPage.ts @@ -104,3 +104,13 @@ export const getMultiDimDataPageBySlug = async ( return row ? enrichRow(row) : undefined } + +export async function getMultiDimDataPageById( + knex: KnexReadonlyTransaction, + id: number +): Promise { + const row = await knex(MultiDimDataPagesTableName) + .where({ id }) + .first() + return row ? enrichRow(row) : undefined +} diff --git a/db/model/MultiDimXChartConfigs.ts b/db/model/MultiDimXChartConfigs.ts index 358cba3e4ee..3c94792f4da 100644 --- a/db/model/MultiDimXChartConfigs.ts +++ b/db/model/MultiDimXChartConfigs.ts @@ -6,7 +6,7 @@ import { KnexReadWriteTransaction } from "../db.js" export async function upsertMultiDimXChartConfigs( knex: KnexReadWriteTransaction, - data: DbInsertMultiDimXChartConfig + data: Partial ): Promise { const result = await knex( MultiDimXChartConfigsTableName diff --git a/package.json b/package.json index 3dd22971340..f35c351ead3 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@sentry/vite-plugin": "^2.23.0", "@sinclair/typebox": "^0.28.5", "@slack/web-api": "^7.1.0", + "@tanstack/react-query": "4", "@tippyjs/react": "^4.2.6", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "algoliasearch": "^4.23.2", diff --git a/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts b/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts index 05e9df99261..b8184c0ceb7 100644 --- a/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts +++ b/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts @@ -31,8 +31,8 @@ export function serializeChartConfig(config: GrapherInterface): JsonString { } export function parseChartConfigsRow( - row: DbRawChartConfig -): DbEnrichedChartConfig { + row: Pick +): Pick { return { ...row, patch: parseChartConfig(row.patch), diff --git a/packages/@ourworldindata/types/src/domainTypes/Various.ts b/packages/@ourworldindata/types/src/domainTypes/Various.ts index fd601e16449..c4c63448717 100644 --- a/packages/@ourworldindata/types/src/domainTypes/Various.ts +++ b/packages/@ourworldindata/types/src/domainTypes/Various.ts @@ -61,8 +61,8 @@ export enum TaggableType { // Exception format that can be easily given as an API error export class JsonError extends Error { status: number - constructor(message: string, status?: number) { - super(message) + constructor(message: string, status?: number, options?: ErrorOptions) { + super(message, options) this.status = status ?? 400 } } diff --git a/yarn.lock b/yarn.lock index 7873bad0501..09964430ec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5345,6 +5345,32 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:4.36.1": + version: 4.36.1 + resolution: "@tanstack/query-core@npm:4.36.1" + checksum: 10/7c648872cd491bcab2aa4c18e0b7ca130c072f05c277a5876977fa3bfa87634bbfde46e9d249236587d78c39866889a02e4e202b478dc6074ff96093732ae56d + languageName: node + linkType: hard + +"@tanstack/react-query@npm:4": + version: 4.36.1 + resolution: "@tanstack/react-query@npm:4.36.1" + dependencies: + "@tanstack/query-core": "npm:4.36.1" + use-sync-external-store: "npm:^1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 10/764b860c3ac8d254fc6b07e01054a0f58058644d59626c724b213293fbf1e31c198cbb26e4c32c0d16dcaec0353c0ae19147d9c667675b31f8cea1d64f1ff4ac + languageName: node + linkType: hard + "@testing-library/dom@npm:^8.0.0": version: 8.13.0 resolution: "@testing-library/dom@npm:8.13.0" @@ -11766,6 +11792,7 @@ __metadata: "@sentry/vite-plugin": "npm:^2.23.0" "@sinclair/typebox": "npm:^0.28.5" "@slack/web-api": "npm:^7.1.0" + "@tanstack/react-query": "npm:4" "@testing-library/jest-dom": "npm:^6.1.3" "@testing-library/react": "npm:^12.1.5" "@tippyjs/react": "npm:^4.2.6" @@ -20886,6 +20913,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/08bf581a8a2effaefc355e9d18ed025d436230f4cc973db2f593166df357cf63e47b9097b6e5089b594758bde322e1737754ad64905e030d70f8ff7ee671fd01 + languageName: node + linkType: hard + "usehooks-ts@npm:^3.1.0": version: 3.1.0 resolution: "usehooks-ts@npm:3.1.0"