Add ability to reorder apps on the home screen
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, asc, inArray } from "drizzle-orm";
|
||||
import { eq, asc, inArray, sql } from "drizzle-orm";
|
||||
import { db } from "@workspace/db";
|
||||
import {
|
||||
appsTable,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
userRolesTable,
|
||||
rolePermissionsTable,
|
||||
rolesTable,
|
||||
userAppOrdersTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAuth, requireAdmin } from "../middlewares/auth";
|
||||
import {
|
||||
@@ -15,13 +16,12 @@ import {
|
||||
GetAppParams,
|
||||
UpdateAppParams,
|
||||
DeleteAppParams,
|
||||
UpdateMyAppOrderBody,
|
||||
} from "@workspace/api-zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
|
||||
async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$inferSelect[]> {
|
||||
const userRoleRows = await db
|
||||
.select({ roleName: rolesTable.name, roleId: userRolesTable.roleId })
|
||||
.from(userRolesTable)
|
||||
@@ -30,18 +30,30 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
|
||||
const isAdmin = userRoleRows.some((r) => r.roleName === "admin");
|
||||
|
||||
if (isAdmin) {
|
||||
const apps = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
res.json(apps);
|
||||
return;
|
||||
}
|
||||
// Pull the user's preferred order, then order by COALESCE(user_order, app.sort_order, name).
|
||||
const orderedRows = await db
|
||||
.select({
|
||||
app: appsTable,
|
||||
userSort: userAppOrdersTable.sortOrder,
|
||||
})
|
||||
.from(appsTable)
|
||||
.leftJoin(
|
||||
userAppOrdersTable,
|
||||
sql`${userAppOrdersTable.appId} = ${appsTable.id} and ${userAppOrdersTable.userId} = ${userId}`,
|
||||
)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(
|
||||
sql`CASE WHEN ${userAppOrdersTable.sortOrder} IS NULL THEN 1 ELSE 0 END`,
|
||||
asc(userAppOrdersTable.sortOrder),
|
||||
asc(appsTable.sortOrder),
|
||||
asc(appsTable.nameEn),
|
||||
);
|
||||
|
||||
const apps = orderedRows.map((r) => r.app);
|
||||
|
||||
if (isAdmin) return apps;
|
||||
|
||||
const userRoleIds = userRoleRows.map((r) => r.roleId);
|
||||
|
||||
const userPermissionIds = userRoleIds.length > 0
|
||||
? (await db
|
||||
.select({ permissionId: rolePermissionsTable.permissionId })
|
||||
@@ -50,29 +62,12 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
).map((r) => r.permissionId)
|
||||
: [];
|
||||
|
||||
const apps = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(
|
||||
eq(appsTable.isActive, true),
|
||||
)
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
|
||||
const appsWithPermissions = await db
|
||||
.select({ appId: appPermissionsTable.appId })
|
||||
.from(appPermissionsTable);
|
||||
|
||||
const restrictedAppIds = new Set(appsWithPermissions.map((r) => r.appId));
|
||||
|
||||
if (restrictedAppIds.size === 0) {
|
||||
const unrestricted = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
res.json(unrestricted);
|
||||
return;
|
||||
}
|
||||
if (restrictedAppIds.size === 0) return apps;
|
||||
|
||||
const allowedRestrictedAppIds = userPermissionIds.length > 0
|
||||
? (await db
|
||||
@@ -83,12 +78,48 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
: [];
|
||||
|
||||
const allowedAppIdSet = new Set(allowedRestrictedAppIds);
|
||||
|
||||
const visibleApps = apps.filter(
|
||||
return apps.filter(
|
||||
(app) => !restrictedAppIds.has(app.id) || allowedAppIdSet.has(app.id),
|
||||
);
|
||||
}
|
||||
|
||||
res.json(visibleApps);
|
||||
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
res.json(visible);
|
||||
});
|
||||
|
||||
router.put("/me/app-order", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
const parsed = UpdateMyAppOrderBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { order } = parsed.data;
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
const visibleIds = new Set(visible.map((a) => a.id));
|
||||
|
||||
const filtered = order.filter((id) => visibleIds.has(id));
|
||||
const seen = new Set<number>();
|
||||
const dedup = filtered.filter((id) => {
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(userAppOrdersTable).where(eq(userAppOrdersTable.userId, userId));
|
||||
if (dedup.length > 0) {
|
||||
await tx.insert(userAppOrdersTable).values(
|
||||
dedup.map((appId, idx) => ({ userId, appId, sortOrder: idx })),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const updated = await getVisibleAppsForUser(userId);
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.post("/apps", requireAdmin, async (req, res): Promise<void> => {
|
||||
|
||||
@@ -78,6 +78,9 @@
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@uppy/aws-s3": "^5.1.0",
|
||||
"@uppy/core": "^5.2.0",
|
||||
"@uppy/dashboard": "^5.1.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "wouter";
|
||||
import {
|
||||
@@ -10,13 +10,31 @@ import {
|
||||
getListNotificationsQueryKey,
|
||||
useLogout,
|
||||
useUpdateLanguage,
|
||||
useUpdateMyAppOrder,
|
||||
getGetMeQueryKey,
|
||||
type App,
|
||||
} from "@workspace/api-client-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { Bell, LogOut, Globe } from "lucide-react";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import i18n from "@/i18n";
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
rectSortingStrategy,
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
function gradientForColor(color: string): string {
|
||||
const c = (color ?? "").toLowerCase();
|
||||
@@ -51,6 +69,22 @@ function AppIcon({ app, onClick }: { app: { nameAr: string; nameEn: string; icon
|
||||
);
|
||||
}
|
||||
|
||||
function SortableAppIcon({ app, onClick }: { app: App; onClick: () => void }) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: app.id });
|
||||
const style: React.CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
zIndex: isDragging ? 20 : "auto",
|
||||
touchAction: "none",
|
||||
};
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
<AppIcon app={app} onClick={onClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { t, i18n: i18nInstance } = useTranslation();
|
||||
const [, setLocation] = useLocation();
|
||||
@@ -60,6 +94,37 @@ export default function HomePage() {
|
||||
const lang = i18nInstance.language;
|
||||
|
||||
const { data: apps } = useListApps({ query: { queryKey: getListAppsQueryKey() } });
|
||||
const updateOrder = useUpdateMyAppOrder();
|
||||
const [orderedApps, setOrderedApps] = useState<App[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (apps && !updateOrder.isPending) setOrderedApps(apps);
|
||||
}, [apps, updateOrder.isPending]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 5 } }),
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id || !orderedApps) return;
|
||||
const oldIndex = orderedApps.findIndex((a) => a.id === active.id);
|
||||
const newIndex = orderedApps.findIndex((a) => a.id === over.id);
|
||||
if (oldIndex < 0 || newIndex < 0) return;
|
||||
const next = arrayMove(orderedApps, oldIndex, newIndex);
|
||||
setOrderedApps(next);
|
||||
updateOrder.mutate(
|
||||
{ data: { order: next.map((a) => a.id) } },
|
||||
{
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries({ queryKey: getListAppsQueryKey() });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const sortableIds = useMemo(() => (orderedApps ?? []).map((a) => a.id), [orderedApps]);
|
||||
const { data: stats } = useGetHomeStats({ query: { queryKey: getGetHomeStatsQueryKey() } });
|
||||
const { data: notifications } = useListNotifications({ query: { queryKey: getListNotificationsQueryKey() } });
|
||||
|
||||
@@ -108,7 +173,7 @@ export default function HomePage() {
|
||||
);
|
||||
};
|
||||
|
||||
const dockApps = apps?.filter((a) => ["services", "chat", "notifications", "admin"].includes(a.slug)) ?? [];
|
||||
const dockApps = (orderedApps ?? apps)?.filter((a) => ["services", "chat", "notifications", "admin"].includes(a.slug)) ?? [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen os-bg flex flex-col overflow-hidden relative">
|
||||
@@ -189,15 +254,19 @@ export default function HomePage() {
|
||||
{/* Apps Grid */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-4">{t("home.myApps")}</h3>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-4">
|
||||
{apps?.map((app) => (
|
||||
<AppIcon
|
||||
key={app.id}
|
||||
app={app}
|
||||
onClick={() => setLocation(app.route)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sortableIds} strategy={rectSortingStrategy}>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-4">
|
||||
{orderedApps?.map((app) => (
|
||||
<SortableAppIcon
|
||||
key={app.id}
|
||||
app={app}
|
||||
onClick={() => setLocation(app.route)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -264,6 +264,11 @@ export interface AppSettings {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMyAppOrderBody {
|
||||
/** App IDs in the desired display order */
|
||||
order: number[];
|
||||
}
|
||||
|
||||
export interface UpdateAppSettingsBody {
|
||||
/**
|
||||
* @minLength 1
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
UpdateAppBody,
|
||||
UpdateAppSettingsBody,
|
||||
UpdateLanguageBody,
|
||||
UpdateMyAppOrderBody,
|
||||
UpdateServiceBody,
|
||||
UpdateUserBody,
|
||||
UserProfile,
|
||||
@@ -928,6 +929,92 @@ export const useDeleteApp = <
|
||||
return useMutation(getDeleteAppMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Set the current user's preferred home apps order
|
||||
*/
|
||||
export const getUpdateMyAppOrderUrl = () => {
|
||||
return `/api/me/app-order`;
|
||||
};
|
||||
|
||||
export const updateMyAppOrder = async (
|
||||
updateMyAppOrderBody: UpdateMyAppOrderBody,
|
||||
options?: RequestInit,
|
||||
): Promise<App[]> => {
|
||||
return customFetch<App[]>(getUpdateMyAppOrderUrl(), {
|
||||
...options,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(updateMyAppOrderBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateMyAppOrderMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateMyAppOrderBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateMyAppOrderBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateMyAppOrder"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>,
|
||||
{ data: BodyType<UpdateMyAppOrderBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateMyAppOrder(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMyAppOrderMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>
|
||||
>;
|
||||
export type UpdateMyAppOrderMutationBody = BodyType<UpdateMyAppOrderBody>;
|
||||
export type UpdateMyAppOrderMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Set the current user's preferred home apps order
|
||||
*/
|
||||
export const useUpdateMyAppOrder = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateMyAppOrderBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMyAppOrder>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateMyAppOrderBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateMyAppOrderMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get application settings (public)
|
||||
*/
|
||||
|
||||
@@ -236,6 +236,33 @@ paths:
|
||||
"204":
|
||||
description: Deleted
|
||||
|
||||
/me/app-order:
|
||||
put:
|
||||
operationId: updateMyAppOrder
|
||||
tags: [apps]
|
||||
summary: Set the current user's preferred home apps order
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpdateMyAppOrderBody"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated apps in new order
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/App"
|
||||
"400":
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
# Settings
|
||||
/settings:
|
||||
get:
|
||||
@@ -1191,6 +1218,16 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
UpdateMyAppOrderBody:
|
||||
type: object
|
||||
required: [order]
|
||||
properties:
|
||||
order:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
description: App IDs in the desired display order
|
||||
|
||||
UpdateAppSettingsBody:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -197,6 +197,33 @@ export const DeleteAppParams = zod.object({
|
||||
id: zod.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Set the current user's preferred home apps order
|
||||
*/
|
||||
export const UpdateMyAppOrderBody = zod.object({
|
||||
order: zod
|
||||
.array(zod.number())
|
||||
.describe("App IDs in the desired display order"),
|
||||
});
|
||||
|
||||
export const UpdateMyAppOrderResponseItem = zod.object({
|
||||
id: zod.number(),
|
||||
slug: zod.string(),
|
||||
nameAr: zod.string(),
|
||||
nameEn: zod.string(),
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
iconName: zod.string(),
|
||||
route: zod.string(),
|
||||
color: zod.string(),
|
||||
isActive: zod.boolean(),
|
||||
isSystem: zod.boolean(),
|
||||
sortOrder: zod.number(),
|
||||
createdAt: zod.coerce.date(),
|
||||
updatedAt: zod.coerce.date(),
|
||||
});
|
||||
export const UpdateMyAppOrderResponse = zod.array(UpdateMyAppOrderResponseItem);
|
||||
|
||||
/**
|
||||
* @summary Get application settings (public)
|
||||
*/
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from "./services";
|
||||
export * from "./conversations";
|
||||
export * from "./notifications";
|
||||
export * from "./settings";
|
||||
export * from "./user-app-orders";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { pgTable, integer, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { usersTable } from "./users";
|
||||
import { appsTable } from "./apps";
|
||||
|
||||
export const userAppOrdersTable = pgTable(
|
||||
"user_app_orders",
|
||||
{
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
appId: integer("app_id")
|
||||
.notNull()
|
||||
.references(() => appsTable.id, { onDelete: "cascade" }),
|
||||
sortOrder: integer("sort_order").notNull(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.appId] })],
|
||||
);
|
||||
|
||||
export type UserAppOrder = typeof userAppOrdersTable.$inferSelect;
|
||||
Generated
+56
@@ -343,6 +343,15 @@ importers:
|
||||
|
||||
artifacts/teaboy-os:
|
||||
dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.1.0)
|
||||
'@uppy/aws-s3':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(@uppy/core@5.2.0)
|
||||
@@ -748,6 +757,28 @@ packages:
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
react: 19.1.0
|
||||
|
||||
'@dnd-kit/core@6.3.1':
|
||||
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
|
||||
peerDependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0
|
||||
|
||||
'@dnd-kit/sortable@10.0.0':
|
||||
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.3.0
|
||||
react: 19.1.0
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
react: 19.1.0
|
||||
|
||||
'@drizzle-team/brocli@0.10.2':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
@@ -4567,6 +4598,31 @@ snapshots:
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.1(react@19.1.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.1.0)
|
||||
react: 19.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
|
||||
'@esbuild-kit/core-utils@3.3.2':
|
||||
|
||||
Reference in New Issue
Block a user