Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114d9ce321 | ||
|
|
3d3e3059a9 | ||
|
|
9ae8cb2768 | ||
|
|
99346f288b | ||
|
|
41fe5928f1 | ||
|
|
6eb59d6f53 | ||
|
|
2c6b7a818e | ||
|
|
fd10788838 | ||
|
|
3a1adab3e5 | ||
|
|
e67d603570 | ||
|
|
0449716805 | ||
|
|
e0a77948e2 | ||
|
|
a2c4b29bfd | ||
|
|
5501ee6a7d | ||
|
|
f6592b27a1 | ||
|
|
970b92cd95 | ||
|
|
e182d28605 | ||
|
|
19d86daa87 | ||
|
|
1bdfc44ba1 | ||
|
|
0ab0be0aa4 | ||
|
|
cf998b474b | ||
|
|
737e2f8c42 | ||
|
|
c33b09c5d4 | ||
|
|
bc0627ca9c | ||
|
|
c4ab2c35ab | ||
|
|
d44e7e9b52 | ||
|
|
1340880fef | ||
|
|
e79efe06bb | ||
|
|
c16974b8db | ||
|
|
b0c5de82f4 | ||
|
|
d99b20d3ef | ||
|
|
d8eaef859a |
No files matched your search
@@ -148,7 +148,9 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");
|
||||
const integrationCategories = integrationDefs[searchParams.kind].category.flat();
|
||||
const supportsSearchEngine =
|
||||
integrationCategories.includes("search") && !integrationCategories.includes("mediaSearch");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
|
||||
|
||||
@@ -85,6 +85,7 @@ const BoardItemMenuInner = ({ offset, item, resetErrorBoundary }: BoardItemMenuP
|
||||
),
|
||||
integrationSupport: "supportedIntegrations" in currentDefinition,
|
||||
settings,
|
||||
appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined,
|
||||
},
|
||||
{
|
||||
title(t) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
||||
import { constructIntegrationPermissions } from "@homarr/auth/shared";
|
||||
import { createId, objectEntries } from "@homarr/common";
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
@@ -14,11 +16,9 @@ import {
|
||||
integrations,
|
||||
integrationSecrets,
|
||||
integrationUserPermissions,
|
||||
searchEngines,
|
||||
} from "@homarr/db/schema";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import {
|
||||
getIconUrl,
|
||||
getIntegrationKindsByCategory,
|
||||
getPermissionsWithParents,
|
||||
integrationCategories,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
integrationSavePermissionsSchema,
|
||||
integrationUpdateSchema,
|
||||
} from "@homarr/validation/integration";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
@@ -41,6 +42,7 @@ import { MissingSecretError, testConnectionAsync } from "./integration-test-conn
|
||||
import { mapTestConnectionError } from "./map-test-connection-error";
|
||||
|
||||
const logger = createLogger({ module: "integrationRouter" });
|
||||
const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch");
|
||||
|
||||
export const integrationRouter = createTRPCRouter({
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
@@ -80,7 +82,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -128,7 +130,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -179,7 +181,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -314,20 +316,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
url: input.url,
|
||||
});
|
||||
|
||||
if (
|
||||
input.attemptSearchEngineCreation &&
|
||||
integrationDefs[input.kind].category.flatMap((category) => category).includes("search")
|
||||
) {
|
||||
const icon = getIconUrl(input.kind);
|
||||
await ctx.db.insert(searchEngines).values({
|
||||
id: createId(),
|
||||
name: input.name,
|
||||
integrationId,
|
||||
type: "fromIntegration",
|
||||
iconUrl: icon,
|
||||
short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
|
||||
});
|
||||
}
|
||||
// Media request search is a first-class Spotlight action. Do not create a search engine for it.
|
||||
}),
|
||||
update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
@@ -495,7 +484,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
|
||||
inherited: dbGroupPermissions.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
users: userPermissions
|
||||
@@ -503,7 +492,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
user,
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
|
||||
}),
|
||||
groups: dbGroupIntegrationPermission
|
||||
@@ -514,7 +503,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
};
|
||||
@@ -618,8 +607,149 @@ export const integrationRouter = createTRPCRouter({
|
||||
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
||||
return await integrationInstance.searchAsync(encodeURI(input.query));
|
||||
}),
|
||||
mediaRequestSearchTargets: protectedProcedure.query(async ({ ctx }) => {
|
||||
const integrationsWithAccess = await getAccessibleMediaRequestSearchIntegrationsAsync(ctx);
|
||||
|
||||
return integrationsWithAccess.map((integration) => {
|
||||
const permissions = constructIntegrationPermissions(integration, ctx.session);
|
||||
|
||||
return {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
url: integration.url,
|
||||
permissions: {
|
||||
hasInteractAccess: permissions.hasInteractAccess,
|
||||
},
|
||||
};
|
||||
});
|
||||
}),
|
||||
searchMediaRequests: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string(),
|
||||
integrationIds: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const query = input.query.trim();
|
||||
if (query.length === 0) return [];
|
||||
|
||||
const integrationsWithAccess = await getAccessibleMediaRequestSearchIntegrationsAsync(ctx, input.integrationIds);
|
||||
|
||||
const results = await Promise.all(
|
||||
integrationsWithAccess.map(async (integration) => {
|
||||
const integrationInstance = await createIntegrationAsync({
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
url: integration.url,
|
||||
kind: integration.kind as (typeof mediaRequestSearchKinds)[number],
|
||||
externalUrl: integration.app?.href ?? null,
|
||||
decryptedSecrets: integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
});
|
||||
const permissions = constructIntegrationPermissions(integration, ctx.session);
|
||||
|
||||
return await integrationInstance.searchAsync(encodeURI(query)).then((searchResults) =>
|
||||
searchResults.map((result) => ({
|
||||
...result,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
url: integration.url,
|
||||
permissions: {
|
||||
hasInteractAccess: permissions.hasInteractAccess,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return results.flat();
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr", "seerr"))
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr", "seerr"))
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
});
|
||||
|
||||
interface IntegrationRouterContext {
|
||||
db: Database;
|
||||
session: Parameters<typeof hasQueryAccessToIntegrationsAsync>[2];
|
||||
}
|
||||
|
||||
const getAccessibleMediaRequestSearchIntegrationsAsync = async (
|
||||
ctx: IntegrationRouterContext,
|
||||
integrationIds?: string[],
|
||||
) => {
|
||||
const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, ctx.session?.user.id ?? ""),
|
||||
});
|
||||
|
||||
const integrationsFromDb = await ctx.db.query.integrations.findMany({
|
||||
where:
|
||||
integrationIds && integrationIds.length > 0
|
||||
? and(inArray(integrations.id, integrationIds), inArray(integrations.kind, mediaRequestSearchKinds))
|
||||
: inArray(integrations.kind, mediaRequestSearchKinds),
|
||||
orderBy: asc(integrations.name),
|
||||
with: {
|
||||
app: true,
|
||||
secrets: true,
|
||||
items: {
|
||||
with: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
userPermissions: {
|
||||
where: eq(integrationUserPermissions.userId, ctx.session?.user.id ?? ""),
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(
|
||||
integrationGroupPermissions.groupId,
|
||||
groupsOfCurrentUser.map((group) => group.groupId),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (integrationIds && integrationsFromDb.length !== integrationIds.length) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "One or more media request search integrations were not found",
|
||||
});
|
||||
}
|
||||
|
||||
const integrationsWithAccess = await Promise.all(
|
||||
integrationsFromDb.map(async (integration) => ({
|
||||
integration,
|
||||
hasAccess: await hasQueryAccessToIntegrationsAsync(ctx.db, [integration], ctx.session),
|
||||
})),
|
||||
);
|
||||
|
||||
if (integrationIds && integrationsWithAccess.some(({ hasAccess }) => !hasAccess)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User does not have permission to query one or more media request search integrations",
|
||||
});
|
||||
}
|
||||
|
||||
return integrationsWithAccess.filter(({ hasAccess }) => hasAccess).map(({ integration }) => integration);
|
||||
};
|
||||
|
||||
interface UpdateSecretInput {
|
||||
integrationId: string;
|
||||
value: string;
|
||||
@@ -640,35 +770,6 @@ interface AddSecretInput {
|
||||
kind: IntegrationSecretKind;
|
||||
}
|
||||
|
||||
const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
|
||||
const searchEngines = await db.query.searchEngines.findMany({
|
||||
columns: {
|
||||
short: true,
|
||||
},
|
||||
});
|
||||
|
||||
const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short.toLowerCase());
|
||||
const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();
|
||||
|
||||
if (!usedShortNames.includes(nameByIntegrationName)) {
|
||||
return nameByIntegrationName;
|
||||
}
|
||||
|
||||
// 8 is max length constraint
|
||||
for (let i = 2; i < 9999999; i++) {
|
||||
const generatedName = `${nameByIntegrationName}${i}`;
|
||||
if (usedShortNames.includes(generatedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return generatedName;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
|
||||
);
|
||||
};
|
||||
|
||||
const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
||||
await db.insert(integrationSecrets).values({
|
||||
kind: input.kind,
|
||||
|
||||
@@ -6,12 +6,9 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { asc, eq, like } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { searchEngines, users } from "@homarr/db/schema";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
const logger = createLogger({ module: "searchEngineRouter" });
|
||||
@@ -148,20 +145,6 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
limit: input.limit,
|
||||
});
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-create")
|
||||
.input(searchEngineManageSchema)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { encryptSecret } from "@homarr/common/server";
|
||||
import { apps, integrations, integrationSecrets } from "@homarr/db/schema";
|
||||
import { apps, integrations, integrationSecrets, integrationUserPermissions, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
@@ -59,6 +59,127 @@ describe("all should return all integrations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("mediaRequestSearchTargets should return accessible media search integrations", () => {
|
||||
test("with integration use access should only return media request search integrations", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(["integration-use-all"]),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values([
|
||||
{
|
||||
id: "1",
|
||||
name: "Jellyseerr",
|
||||
kind: "jellyseerr",
|
||||
url: "http://jellyseerr.local",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin",
|
||||
url: "http://jellyfin.local",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await caller.mediaRequestSearchTargets();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.id).toBe("1");
|
||||
expect(result[0]!.kind).toBe("jellyseerr");
|
||||
});
|
||||
|
||||
test("without access should not return media request search integrations", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values({
|
||||
id: "1",
|
||||
name: "Jellyseerr",
|
||||
kind: "jellyseerr",
|
||||
url: "http://jellyseerr.local",
|
||||
});
|
||||
|
||||
const result = await caller.mediaRequestSearchTargets();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("with direct integration use access should return media request search integrations", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values({
|
||||
id: "1",
|
||||
name: "Jellyseerr",
|
||||
kind: "jellyseerr",
|
||||
url: "http://jellyseerr.local",
|
||||
});
|
||||
await db.insert(users).values({ id: defaultUserId });
|
||||
await db.insert(integrationUserPermissions).values({
|
||||
integrationId: "1",
|
||||
userId: defaultUserId,
|
||||
permission: "use",
|
||||
});
|
||||
|
||||
const result = await caller.mediaRequestSearchTargets();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.id).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchMediaRequests should validate scoped integrations", () => {
|
||||
test("with a non media search integration should throw not found", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(["integration-use-all"]),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values({
|
||||
id: "1",
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin",
|
||||
url: "http://jellyfin.local",
|
||||
});
|
||||
|
||||
await expect(caller.searchMediaRequests({ query: "matrix", integrationIds: ["1"] })).rejects.toThrow(
|
||||
"One or more media request search integrations were not found",
|
||||
);
|
||||
});
|
||||
|
||||
test("without query access to a scoped media search integration should throw forbidden", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values({
|
||||
id: "1",
|
||||
name: "Jellyseerr",
|
||||
kind: "jellyseerr",
|
||||
url: "http://jellyseerr.local",
|
||||
});
|
||||
|
||||
await expect(caller.searchMediaRequests({ query: "matrix", integrationIds: ["1"] })).rejects.toThrow(
|
||||
"User does not have permission to query one or more media request search integrations",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("byId should return an integration by id", () => {
|
||||
test("with full access should return an integration by id", async () => {
|
||||
const db = createDb();
|
||||
@@ -208,7 +329,7 @@ describe("create should create a new integration", () => {
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
});
|
||||
|
||||
test("with create integration access should create a new integration when creating search engine", async () => {
|
||||
test("with create integration access should not create a search engine for media request search integrations", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
@@ -243,12 +364,7 @@ describe("create should create a new integration", () => {
|
||||
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
|
||||
expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id);
|
||||
expect(dbSearchEngine!.short).toBe("j");
|
||||
expect(dbSearchEngine!.name).toBe(input.name);
|
||||
expect(dbSearchEngine!.iconUrl).toBe(
|
||||
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
|
||||
);
|
||||
expect(dbSearchEngine).toBeUndefined();
|
||||
});
|
||||
|
||||
test("with create integration access should create a new integration with new linked app", async () => {
|
||||
|
||||
@@ -542,7 +542,7 @@ export const userRouter = createTRPCRouter({
|
||||
.set({
|
||||
pingIconsEnabled: input.pingIconsEnabled,
|
||||
})
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
.where(eq(users.id, input.id));
|
||||
}),
|
||||
changeDdgBangs: protectedProcedure
|
||||
.input(convertIntersectionToZodObject(userDdgBangsSchema.and(byIdSchema)))
|
||||
@@ -603,7 +603,7 @@ export const userRouter = createTRPCRouter({
|
||||
.set({
|
||||
firstDayOfWeek: input.firstDayOfWeek,
|
||||
})
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
.where(eq(users.id, input.id));
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useImperativeHandle, useRef } from "react";
|
||||
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
|
||||
import type { z } from "zod/v4";
|
||||
@@ -17,6 +17,19 @@ import { findBestIconMatch } from "./icon-matcher";
|
||||
|
||||
type FormType = z.infer<typeof appManageSchema>;
|
||||
|
||||
const toFormValues = (values: FormType | undefined): FormType => ({
|
||||
name: values?.name ?? "",
|
||||
description: values?.description ?? "",
|
||||
iconUrl: values?.iconUrl ?? "",
|
||||
href: values?.href ?? "",
|
||||
pingUrl: values?.pingUrl ?? "",
|
||||
});
|
||||
|
||||
export interface AppFormHandle {
|
||||
submit: () => Promise<boolean>;
|
||||
isDirty: () => boolean;
|
||||
}
|
||||
|
||||
interface AppFormProps {
|
||||
showBackToOverview: boolean;
|
||||
buttonLabels: {
|
||||
@@ -24,8 +37,10 @@ interface AppFormProps {
|
||||
submitAndCreateAnother?: string;
|
||||
};
|
||||
initialValues?: FormType;
|
||||
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
|
||||
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void | Promise<void>;
|
||||
isPending: boolean;
|
||||
hideButtons?: boolean;
|
||||
formRef?: React.Ref<AppFormHandle>;
|
||||
}
|
||||
|
||||
export const AppForm = ({
|
||||
@@ -34,19 +49,31 @@ export const AppForm = ({
|
||||
handleSubmit: originalHandleSubmit,
|
||||
initialValues,
|
||||
isPending,
|
||||
hideButtons,
|
||||
formRef,
|
||||
}: AppFormProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const form = useZodForm(appManageSchema, {
|
||||
initialValues: {
|
||||
name: initialValues?.name ?? "",
|
||||
description: initialValues?.description ?? "",
|
||||
iconUrl: initialValues?.iconUrl ?? "",
|
||||
href: initialValues?.href ?? "",
|
||||
pingUrl: initialValues?.pingUrl ?? "",
|
||||
},
|
||||
initialValues: toFormValues(initialValues),
|
||||
});
|
||||
|
||||
const initialValuesKey = [
|
||||
initialValues?.name,
|
||||
initialValues?.description,
|
||||
initialValues?.iconUrl,
|
||||
initialValues?.href,
|
||||
initialValues?.pingUrl,
|
||||
].join("\0");
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialValues || form.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.initialize(toFormValues(initialValues));
|
||||
}, [initialValuesKey, form, initialValues]);
|
||||
|
||||
// Debounce the name value with 200ms delay
|
||||
const [debouncedName] = useDebouncedValue(form.values.name, 200);
|
||||
|
||||
@@ -62,6 +89,29 @@ export const AppForm = ({
|
||||
originalHandleSubmit(values, redirect, afterSuccess);
|
||||
};
|
||||
|
||||
useImperativeHandle(
|
||||
formRef,
|
||||
() => ({
|
||||
submit: () =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
form.onSubmit(
|
||||
async (values) => {
|
||||
try {
|
||||
await Promise.resolve(handleSubmit(values));
|
||||
form.initialize(values);
|
||||
resolve(true);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
() => resolve(false),
|
||||
)();
|
||||
}),
|
||||
isDirty: () => form.isDirty(),
|
||||
}),
|
||||
[form, handleSubmit],
|
||||
);
|
||||
|
||||
const [opened, { open, close }] = useDisclosure((initialValues?.pingUrl?.length ?? 0) > 0);
|
||||
|
||||
const handleClickDifferentUrlPing: ChangeEventHandler<HTMLInputElement> = () => {
|
||||
@@ -92,32 +142,32 @@ export const AppForm = ({
|
||||
}
|
||||
}, [debouncedName, iconsData]);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
|
||||
<IconPicker {...form.getInputProps("iconUrl")} />
|
||||
<Textarea
|
||||
{...form.getInputProps("description")}
|
||||
label={t("app.field.description.label")}
|
||||
autosize
|
||||
minRows={2}
|
||||
resize="vertical"
|
||||
/>
|
||||
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
|
||||
const formFields = (
|
||||
<Stack>
|
||||
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
|
||||
<IconPicker {...form.getInputProps("iconUrl")} />
|
||||
<Textarea
|
||||
{...form.getInputProps("description")}
|
||||
label={t("app.field.description.label")}
|
||||
autosize
|
||||
minRows={2}
|
||||
resize="vertical"
|
||||
/>
|
||||
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
|
||||
|
||||
<Checkbox
|
||||
checked={opened}
|
||||
onChange={handleClickDifferentUrlPing}
|
||||
label={t("app.field.useDifferentUrlForPing.checkbox.label")}
|
||||
description={t("app.field.useDifferentUrlForPing.checkbox.description")}
|
||||
mt="md"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={opened}
|
||||
onChange={handleClickDifferentUrlPing}
|
||||
label={t("app.field.useDifferentUrlForPing.checkbox.label")}
|
||||
description={t("app.field.useDifferentUrlForPing.checkbox.description")}
|
||||
mt="md"
|
||||
/>
|
||||
|
||||
<Collapse expanded={opened}>
|
||||
<TextInput {...form.getInputProps("pingUrl")} />
|
||||
</Collapse>
|
||||
<Collapse expanded={opened}>
|
||||
<TextInput {...form.getInputProps("pingUrl")} />
|
||||
</Collapse>
|
||||
|
||||
{!hideButtons && (
|
||||
<Group justify="end">
|
||||
{showBackToOverview && (
|
||||
<Button variant="default" component={Link} href="/manage/apps">
|
||||
@@ -139,7 +189,13 @@ export const AppForm = ({
|
||||
{buttonLabels.submit}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
if (hideButtons) {
|
||||
return formFields;
|
||||
}
|
||||
|
||||
return <form onSubmit={form.onSubmit(handleSubmit)}>{formFields}</form>;
|
||||
};
|
||||
@@ -12,12 +12,14 @@ interface SeriesInformation {
|
||||
overview: string;
|
||||
seasons: SerieSeason[];
|
||||
posterPath: string;
|
||||
requestedSeasons?: number[];
|
||||
}
|
||||
|
||||
interface MovieInformation {
|
||||
id: number;
|
||||
overview: string;
|
||||
posterPath: string;
|
||||
requestedSeasons?: number[];
|
||||
}
|
||||
|
||||
export type MediaInformation = SeriesInformation | MovieInformation;
|
||||
|
||||
@@ -60,6 +60,9 @@ export class OverseerrIntegration
|
||||
text: "overview" in result ? result.overview : undefined,
|
||||
type: result.mediaType,
|
||||
inLibrary: result.mediaInfo !== undefined,
|
||||
availability: result.mediaInfo
|
||||
? this.mapAvailability(result.mediaInfo.status as UpstreamMediaAvailability, false)
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -70,7 +73,12 @@ export class OverseerrIntegration
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
});
|
||||
return await mediaInformationSchema.parseAsync(await response.json());
|
||||
const data = await mediaInformationSchema.parseAsync(await response.json());
|
||||
const requestedSeasons = [
|
||||
...new Set(data.mediaInfo?.requests?.flatMap((req) => req.seasons.map((s) => s.seasonNumber)) ?? []),
|
||||
];
|
||||
const { mediaInfo: _strip, ...rest } = data;
|
||||
return { ...rest, requestedSeasons };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,6 +349,22 @@ interface MovieInformation {
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const mediaInfoRequestsSchema = z
|
||||
.object({
|
||||
requests: z
|
||||
.array(
|
||||
z.object({
|
||||
seasons: z.array(
|
||||
z.object({
|
||||
seasonNumber: z.number(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const mediaInformationSchema = z.union([
|
||||
z.object({
|
||||
id: z.number(),
|
||||
@@ -355,14 +379,22 @@ const mediaInformationSchema = z.union([
|
||||
),
|
||||
numberOfSeasons: z.number(),
|
||||
posterPath: z.string().startsWith("/"),
|
||||
mediaInfo: mediaInfoRequestsSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
overview: z.string(),
|
||||
posterPath: z.string().startsWith("/"),
|
||||
mediaInfo: mediaInfoRequestsSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
const searchMediaInfoSchema = z
|
||||
.object({
|
||||
status: z.number(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const searchSchema = z.object({
|
||||
results: z
|
||||
.array(
|
||||
@@ -373,7 +405,7 @@ const searchSchema = z.object({
|
||||
name: z.string(),
|
||||
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
overview: z.string(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
@@ -381,14 +413,14 @@ const searchSchema = z.object({
|
||||
title: z.string(),
|
||||
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
overview: z.string(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
mediaType: z.literal("person"),
|
||||
name: z.string(),
|
||||
profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { Badge, Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
|
||||
import type { MRT_ColumnDef, MRT_Row } from "mantine-react-table";
|
||||
import { MRT_Table } from "mantine-react-table";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -16,13 +16,13 @@ interface RequestMediaModalProps {
|
||||
}
|
||||
|
||||
export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions, innerProps }) => {
|
||||
const { data, isPending: isPendingQuery } = clientApi.searchEngine.getMediaRequestOptions.useQuery({
|
||||
const { data, isPending: isPendingQuery } = clientApi.integration.getMediaRequestOptions.useQuery({
|
||||
integrationId: innerProps.integrationId,
|
||||
mediaId: innerProps.mediaId,
|
||||
mediaType: innerProps.mediaType,
|
||||
});
|
||||
|
||||
const { mutate, isPending: isPendingMutation } = clientApi.searchEngine.requestMedia.useMutation({
|
||||
const { mutate, isPending: isPendingMutation } = clientApi.integration.requestMedia.useMutation({
|
||||
onSuccess() {
|
||||
actions.closeModal();
|
||||
showSuccessNotification({
|
||||
@@ -33,6 +33,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
|
||||
const isPending = isPendingQuery || isPendingMutation;
|
||||
const t = useI18n();
|
||||
const requestedSeasons = new Set(data?.requestedSeasons ?? []);
|
||||
|
||||
const columns = useMemo<MRT_ColumnDef<Season>[]>(
|
||||
() => [
|
||||
@@ -44,8 +45,18 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
accessorKey: "episodeCount",
|
||||
header: t("search.engine.media.request.modal.table.header.episodes"),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: t("search.engine.media.request.modal.table.header.status"),
|
||||
Cell: ({ row }) =>
|
||||
requestedSeasons.has(row.original.seasonNumber) ? (
|
||||
<Badge size="xs" color="violet" variant="light">
|
||||
{t("search.engine.media.request.modal.table.status.requested")}
|
||||
</Badge>
|
||||
) : null,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[data?.requestedSeasons],
|
||||
);
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
@@ -56,7 +67,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
enablePagination: false,
|
||||
enableSorting: false,
|
||||
enableSelectAll: true,
|
||||
enableRowSelection: true,
|
||||
enableRowSelection: (row: MRT_Row<Season>) => !requestedSeasons.has(row.original.seasonNumber),
|
||||
mantineTableProps: {
|
||||
highlightOnHover: false,
|
||||
striped: "odd",
|
||||
|
||||
@@ -44,6 +44,7 @@ const optionMapping: OptionMapping = {
|
||||
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
|
||||
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
|
||||
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
|
||||
withBorder: () => undefined,
|
||||
},
|
||||
calendar: {
|
||||
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./src/context";
|
||||
export * from "./src/preferences/definitions";
|
||||
@@ -0,0 +1,78 @@
|
||||
export const userPreferenceDefinitions = [
|
||||
{
|
||||
key: "colorScheme",
|
||||
kind: "select",
|
||||
guest: true,
|
||||
mutationGroup: "colorScheme",
|
||||
aliases: ["theme", "dark", "light", "auto"],
|
||||
},
|
||||
{ key: "locale", kind: "select", guest: true, mutationGroup: "locale", aliases: ["language", "translation"] },
|
||||
{
|
||||
key: "defaultSearchEngineId",
|
||||
kind: "searchEngine",
|
||||
guest: false,
|
||||
mutationGroup: "searchPreferences",
|
||||
aliases: ["search", "search engine"],
|
||||
},
|
||||
{
|
||||
key: "openSearchInNewTab",
|
||||
kind: "boolean",
|
||||
guest: false,
|
||||
mutationGroup: "searchPreferences",
|
||||
aliases: ["search", "new tab", "external"],
|
||||
},
|
||||
{
|
||||
key: "ddgBangs",
|
||||
kind: "boolean",
|
||||
guest: false,
|
||||
mutationGroup: "searchPreferences",
|
||||
aliases: ["duckduckgo", "bangs", "search"],
|
||||
},
|
||||
{
|
||||
key: "firstDayOfWeek",
|
||||
kind: "select",
|
||||
guest: false,
|
||||
mutationGroup: "firstDayOfWeek",
|
||||
aliases: ["calendar", "week", "weekday"],
|
||||
},
|
||||
{
|
||||
key: "homeBoardId",
|
||||
kind: "board",
|
||||
guest: false,
|
||||
mutationGroup: "homeBoards",
|
||||
aliases: ["home", "board", "dashboard"],
|
||||
},
|
||||
{
|
||||
key: "mobileHomeBoardId",
|
||||
kind: "board",
|
||||
guest: false,
|
||||
mutationGroup: "homeBoards",
|
||||
aliases: ["mobile", "home", "board", "dashboard"],
|
||||
},
|
||||
{
|
||||
key: "pingIconsEnabled",
|
||||
kind: "boolean",
|
||||
guest: false,
|
||||
mutationGroup: "pingIconsEnabled",
|
||||
aliases: ["ping", "icon", "status"],
|
||||
},
|
||||
{
|
||||
key: "fullPreferencesPage",
|
||||
kind: "link",
|
||||
guest: false,
|
||||
mutationGroup: null,
|
||||
aliases: ["settings", "manage", "profile"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type UserPreferenceDefinition = (typeof userPreferenceDefinitions)[number];
|
||||
export type UserPreferenceKey = UserPreferenceDefinition["key"];
|
||||
export type UserPreferenceKind = UserPreferenceDefinition["kind"];
|
||||
export type UserPreferenceMutationGroup = Exclude<UserPreferenceDefinition["mutationGroup"], null>;
|
||||
|
||||
export const userPreferenceDefinitionByKey = Object.fromEntries(
|
||||
userPreferenceDefinitions.map((definition) => [definition.key, definition]),
|
||||
) as Record<UserPreferenceKey, UserPreferenceDefinition>;
|
||||
|
||||
export const visiblePreferenceDefinitions = (isAuthenticated: boolean) =>
|
||||
userPreferenceDefinitions.filter((definition) => definition.guest || isAuthenticated);
|
||||
@@ -29,6 +29,7 @@
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
|
||||
@@ -33,7 +33,10 @@ export const ChildrenActionItem = ({ childrenOptions, action, query, setChildren
|
||||
<Spotlight.Action
|
||||
renderRoot={renderRoot}
|
||||
onClick={onClick}
|
||||
closeSpotlightOnTrigger={interaction.type !== "children"}
|
||||
closeSpotlightOnTrigger={
|
||||
interaction.type !== "children" &&
|
||||
(interaction.type !== "javaScript" || interaction.closeSpotlightOnTrigger !== false)
|
||||
}
|
||||
className={classes.spotlightAction}
|
||||
>
|
||||
<action.Component {...childrenOptions.option} />
|
||||
|
||||
@@ -44,6 +44,9 @@ export const SpotlightGroupActionItem = <TOption extends Record<string, unknown>
|
||||
setQuery(interaction.query);
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
} else if (interaction.type === "mode") {
|
||||
if (interaction.query !== undefined) {
|
||||
setQuery(interaction.query);
|
||||
}
|
||||
setMode(interaction.mode);
|
||||
} else if (interaction.type === "children") {
|
||||
setChildrenOptions(interaction);
|
||||
@@ -58,7 +61,8 @@ export const SpotlightGroupActionItem = <TOption extends Record<string, unknown>
|
||||
interaction.type !== "mode" &&
|
||||
interaction.type !== "children" &&
|
||||
interaction.type !== "none" &&
|
||||
interaction.type !== "setQuery"
|
||||
interaction.type !== "setQuery" &&
|
||||
(interaction.type !== "javaScript" || interaction.closeSpotlightOnTrigger !== false)
|
||||
}
|
||||
className={classes.spotlightAction}
|
||||
>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
||||
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||
import { IconArrowLeft, IconSearch, IconX } from "@tabler/icons-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { OpenMediaRequestSearchOptions } from "../index";
|
||||
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
||||
import type { SearchMode } from "../lib/mode";
|
||||
import { searchModes } from "../modes";
|
||||
import { useHomeEmptyGroups } from "../modes/help/home-empty-groups";
|
||||
import { selectAction, spotlightStore } from "../spotlight-store";
|
||||
import { useHomeEmptyGroupsWithPreferences } from "../modes/help/home-empty-groups";
|
||||
import {
|
||||
mediaRequestSearchEvent,
|
||||
mediaRequestSearchScopeAtom,
|
||||
selectAction,
|
||||
spotlightActions,
|
||||
spotlightStore,
|
||||
} from "../spotlight-store";
|
||||
import { SpotlightChildrenActions } from "./actions/children-actions";
|
||||
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
||||
|
||||
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
||||
type ChildrenOptions = inferSearchInteractionOptions<"children">;
|
||||
|
||||
const defaultMode = "home";
|
||||
export const Spotlight = () => {
|
||||
@@ -31,7 +40,6 @@ export const Spotlight = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
||||
return (
|
||||
<SpotlightWithActiveMode key={mode} modeState={searchModeState} queryState={queryState} activeMode={activeMode} />
|
||||
);
|
||||
@@ -46,13 +54,57 @@ interface SpotlightWithActiveModeProps {
|
||||
const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: SpotlightWithActiveModeProps) => {
|
||||
const [query, setQuery] = queryState;
|
||||
const [mode, setMode] = modeState;
|
||||
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
||||
const [childrenStack, setChildrenStack] = useState<ChildrenOptions[]>([]);
|
||||
const childrenOptions = childrenStack.at(-1) ?? null;
|
||||
const t = useI18n();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// Works as always the same amount of hooks are executed
|
||||
const setMediaRequestSearchScope = useSetAtom(mediaRequestSearchScopeAtom);
|
||||
const useGroups = "groups" in activeMode ? () => activeMode.groups : activeMode.useGroups;
|
||||
const groups = useGroups();
|
||||
const homeEmptyGroups = useHomeEmptyGroups();
|
||||
const homeEmptyGroups = useHomeEmptyGroupsWithPreferences();
|
||||
const hasModeCharacter = activeMode.modeKey !== defaultMode && activeMode.character !== undefined;
|
||||
|
||||
const clearChildrenStack = useCallback(() => {
|
||||
setChildrenStack([]);
|
||||
}, []);
|
||||
|
||||
const pushChildrenOptions = useCallback(
|
||||
(options: ChildrenOptions) => {
|
||||
setChildrenStack((currentStack) => [...currentStack, options]);
|
||||
setQuery("");
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
},
|
||||
[setQuery],
|
||||
);
|
||||
|
||||
const popChildrenOptions = useCallback(() => {
|
||||
setChildrenStack((currentStack) => currentStack.slice(0, -1));
|
||||
setQuery("");
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
}, [setQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMediaRequestSearch = (event: Event) => {
|
||||
const { integrationIds, query } = (event as CustomEvent<OpenMediaRequestSearchOptions>).detail ?? {};
|
||||
setMediaRequestSearchScope({ integrationIds });
|
||||
setMode("media");
|
||||
clearChildrenStack();
|
||||
setQuery(query ?? "");
|
||||
spotlightActions.open();
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
};
|
||||
|
||||
window.addEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
|
||||
return () => {
|
||||
window.removeEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
|
||||
};
|
||||
}, [setMediaRequestSearchScope, setMode, setQuery, clearChildrenStack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "media") {
|
||||
setMediaRequestSearchScope({});
|
||||
}
|
||||
}, [mode, setMediaRequestSearchScope]);
|
||||
|
||||
return (
|
||||
<MantineSpotlight.Root
|
||||
@@ -61,7 +113,7 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
|
||||
overlayProps={{ blur: 2, backgroundOpacity: 0.2 }}
|
||||
onSpotlightClose={() => {
|
||||
setMode(defaultMode);
|
||||
setChildrenOptions(null);
|
||||
clearChildrenStack();
|
||||
setQuery("");
|
||||
}}
|
||||
query={query}
|
||||
@@ -69,17 +121,14 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
|
||||
const sanitizedQuery = mode === "external" && nextQuery.startsWith("!") ? nextQuery.slice(1) : nextQuery;
|
||||
setQuery(sanitizedQuery);
|
||||
|
||||
// Only switch modes when a single trigger character is typed
|
||||
if (sanitizedQuery.length !== 1) return;
|
||||
|
||||
const modeToActivate = searchModes.find((mode) => mode.character === sanitizedQuery);
|
||||
const modeToActivate = searchModes.find((searchMode) => searchMode.character === sanitizedQuery);
|
||||
if (!modeToActivate) return;
|
||||
|
||||
setMode(modeToActivate.modeKey);
|
||||
setChildrenOptions(null);
|
||||
|
||||
clearChildrenStack();
|
||||
setQuery("");
|
||||
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
}}
|
||||
store={spotlightStore}
|
||||
@@ -87,13 +136,13 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
|
||||
<MantineSpotlight.Search
|
||||
placeholder={`${t("search.placeholder")}...`}
|
||||
ref={inputRef}
|
||||
leftSectionWidth={activeMode.modeKey !== defaultMode ? 80 : 48}
|
||||
leftSectionWidth={hasModeCharacter ? 80 : 48}
|
||||
leftSection={
|
||||
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
||||
<Center w={48} h="100%">
|
||||
<IconSearch stroke={1.5} />
|
||||
</Center>
|
||||
{activeMode.modeKey !== defaultMode ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
||||
{hasModeCharacter ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
||||
</Group>
|
||||
}
|
||||
styles={{
|
||||
@@ -102,31 +151,53 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
|
||||
},
|
||||
}}
|
||||
rightSection={
|
||||
mode === defaultMode ? null : (
|
||||
mode === defaultMode && childrenStack.length === 0 ? null : (
|
||||
<ActionIcon
|
||||
title={
|
||||
childrenStack.length > 0
|
||||
? t("search.mode.command.group.preferences.children.detail.backAction")
|
||||
: t("common.action.close")
|
||||
}
|
||||
onClick={() => {
|
||||
if (childrenStack.length > 0) {
|
||||
popChildrenOptions();
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setMode(defaultMode);
|
||||
setChildrenOptions(null);
|
||||
clearChildrenStack();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
variant="subtle"
|
||||
aria-label={t("common.action.close")}
|
||||
aria-label={
|
||||
childrenStack.length > 0
|
||||
? t("search.mode.command.group.preferences.children.detail.backAction")
|
||||
: t("common.action.close")
|
||||
}
|
||||
>
|
||||
<IconX stroke={1.5} />
|
||||
{childrenStack.length > 0 ? <IconArrowLeft stroke={1.5} /> : <IconX stroke={1.5} />}
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
value={query}
|
||||
onKeyDown={(event) => {
|
||||
if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
|
||||
if (query.length !== 0 || event.key !== "Backspace") return;
|
||||
|
||||
if (childrenStack.length > 0) {
|
||||
popChildrenOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode !== defaultMode) {
|
||||
setMode(defaultMode);
|
||||
setChildrenOptions(null);
|
||||
clearChildrenStack();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{childrenOptions ? (
|
||||
<Group>
|
||||
<Group key={childrenStack.length}>
|
||||
<childrenOptions.DetailComponent options={childrenOptions.option as never} />
|
||||
</Group>
|
||||
) : null}
|
||||
@@ -134,25 +205,21 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
|
||||
<MantineSpotlight.ActionsList>
|
||||
{childrenOptions ? (
|
||||
<SpotlightChildrenActions
|
||||
key={childrenStack.length}
|
||||
childrenOptions={childrenOptions}
|
||||
query={query}
|
||||
setChildrenOptions={setChildrenOptions}
|
||||
setChildrenOptions={pushChildrenOptions}
|
||||
/>
|
||||
) : (
|
||||
<SpotlightActionGroups
|
||||
setQuery={setQuery}
|
||||
setMode={(mode) => {
|
||||
setMode(mode);
|
||||
setChildrenOptions(null);
|
||||
setMode={(nextMode) => {
|
||||
setMode(nextMode);
|
||||
clearChildrenStack();
|
||||
setTimeout(() => selectAction(0, spotlightStore));
|
||||
}}
|
||||
setChildrenOptions={(options) => {
|
||||
setChildrenOptions(options);
|
||||
|
||||
setTimeout(() => {
|
||||
setQuery("");
|
||||
selectAction(0, spotlightStore);
|
||||
});
|
||||
pushChildrenOptions(options);
|
||||
}}
|
||||
query={query}
|
||||
groups={mode === defaultMode && query.length === 0 ? [...homeEmptyGroups] : groups}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { spotlightActions } from "./spotlight-store";
|
||||
import { mediaRequestSearchEvent, spotlightActions } from "./spotlight-store";
|
||||
|
||||
export { Spotlight } from "./components/spotlight";
|
||||
export { openSpotlight };
|
||||
export { openSpotlight, openMediaRequestSearch };
|
||||
export {
|
||||
SpotlightProvider,
|
||||
useRegisterSpotlightContextResults,
|
||||
@@ -11,3 +11,18 @@ export {
|
||||
} from "./modes/home/context";
|
||||
|
||||
const openSpotlight = spotlightActions.open;
|
||||
|
||||
export interface OpenMediaRequestSearchOptions {
|
||||
integrationIds?: string[];
|
||||
query?: string;
|
||||
}
|
||||
|
||||
const openMediaRequestSearch = (options: OpenMediaRequestSearchOptions = {}) => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent<OpenMediaRequestSearchOptions>(mediaRequestSearchEvent, { detail: options }));
|
||||
}
|
||||
|
||||
spotlightActions.open();
|
||||
};
|
||||
|
||||
export { mediaRequestSearchEvent };
|
||||
@@ -10,9 +10,12 @@ const createSearchInteraction = <TType extends string>(type: TType) => ({
|
||||
// This is used to define search interactions with their options
|
||||
const searchInteractions = [
|
||||
createSearchInteraction("link").optionsType<{ href: string; newTab?: boolean }>(),
|
||||
createSearchInteraction("javaScript").optionsType<{ onSelect: () => MaybePromise<void> }>(),
|
||||
createSearchInteraction("javaScript").optionsType<{
|
||||
onSelect: () => MaybePromise<void>;
|
||||
closeSpotlightOnTrigger?: boolean;
|
||||
}>(),
|
||||
createSearchInteraction("setQuery").optionsType<{ query: string }>(),
|
||||
createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"] }>(),
|
||||
createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"]; query?: string }>(),
|
||||
createSearchInteraction("children").optionsType<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
useActions: CreateChildrenOptionsProps<any>["useActions"];
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
|
||||
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
||||
import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
|
||||
import { useUserPreference } from "../../../preferences/use-user-preference";
|
||||
import { LanguageIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../../../lib/children";
|
||||
import { createCheckmarkPreferenceAction, PreferenceDetailHeader } from "./preferences/action-row";
|
||||
|
||||
export const languageChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: (_, query) => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const currentLocale = useCurrentLocale();
|
||||
const { value: currentLocale, setValue: setLocale, isPending } = useUserPreference("locale");
|
||||
|
||||
return supportedLanguages
|
||||
.map((localeKey) => ({ localeKey, configuration: localeConfigurations[localeKey] }))
|
||||
.filter(
|
||||
@@ -18,7 +19,7 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
|
||||
configuration.name.toLowerCase().includes(normalizedQuery) ||
|
||||
configuration.translatedName.toLowerCase().includes(normalizedQuery),
|
||||
)
|
||||
.sort(
|
||||
.toSorted(
|
||||
(languageA, languageB) =>
|
||||
Math.min(
|
||||
languageA.configuration.name.toLowerCase().indexOf(normalizedQuery),
|
||||
@@ -29,38 +30,25 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
|
||||
languageB.configuration.translatedName.toLowerCase().indexOf(normalizedQuery),
|
||||
),
|
||||
)
|
||||
.map(({ localeKey, configuration }) => ({
|
||||
key: localeKey,
|
||||
Component() {
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap">
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{configuration.name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
({configuration.translatedName})
|
||||
</Text>
|
||||
</Group>
|
||||
.map(({ localeKey, configuration }) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: localeKey,
|
||||
isSelected: localeKey === currentLocale,
|
||||
onSelect: () => setLocale(localeKey as never),
|
||||
isPending,
|
||||
labelContent: (
|
||||
<Group wrap="nowrap">
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{configuration.name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
({configuration.translatedName})
|
||||
</Text>
|
||||
</Group>
|
||||
{localeKey === currentLocale && <IconCheck color="currentColor" size={24} />}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction() {
|
||||
const { changeLocale } = useChangeLocale();
|
||||
|
||||
return { type: "javaScript", onSelect: () => changeLocale(localeKey) };
|
||||
},
|
||||
}));
|
||||
},
|
||||
DetailComponent: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm">
|
||||
<Text>{t("search.mode.command.group.globalCommand.option.language.children.detail.title")}</Text>
|
||||
</Stack>
|
||||
);
|
||||
),
|
||||
}),
|
||||
);
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey="locale.children.detail.title" />,
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Center, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { interaction } from "../../../../lib/interaction";
|
||||
|
||||
export const PreferenceDetailHeader = ({ titleKey }: { titleKey: string }) => {
|
||||
const tOption = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
const t = useScopedI18n("search.mode.command.group.preferences");
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm" gap="xs">
|
||||
<Text>{tOption(titleKey as never)}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("children.detail.backHint")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const createCheckmarkPreferenceAction = ({
|
||||
key,
|
||||
label,
|
||||
labelContent,
|
||||
isSelected,
|
||||
onSelect,
|
||||
dimmed,
|
||||
isPending,
|
||||
}: {
|
||||
key: string;
|
||||
label?: string;
|
||||
labelContent?: ReactNode;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
dimmed?: boolean;
|
||||
isPending?: boolean;
|
||||
}) => ({
|
||||
key,
|
||||
Component() {
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%" opacity={isPending ? 0.5 : 1}>
|
||||
{labelContent ?? <Text c={dimmed ? "dimmed" : undefined}>{label}</Text>}
|
||||
{isSelected && <IconCheck color="currentColor" size={24} />}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: isPending ? () => undefined : onSelect,
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
});
|
||||
|
||||
export const createLoadingPreferenceAction = () => ({
|
||||
key: "loading",
|
||||
Component() {
|
||||
return (
|
||||
<Center mx="md" my="sm">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: () => undefined,
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { settingsChildrenOptions } from "./settings-children";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createCheckmarkPreferenceAction } from "./action-row";
|
||||
|
||||
interface SelectablePreferenceItem {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const createSelectablePreferenceActions = ({
|
||||
query,
|
||||
currentValue,
|
||||
noneLabel,
|
||||
unavailableLabel,
|
||||
items,
|
||||
onSelect,
|
||||
isPending,
|
||||
}: {
|
||||
query: string;
|
||||
currentValue: string | null;
|
||||
noneLabel: string;
|
||||
unavailableLabel: string;
|
||||
items: SelectablePreferenceItem[];
|
||||
onSelect: (value: string | null) => void;
|
||||
isPending?: boolean;
|
||||
}) => {
|
||||
const hasCurrentSelection = currentValue !== null && !items.some((item) => item.value === currentValue);
|
||||
const orphanedItem: SelectablePreferenceItem | null = hasCurrentSelection
|
||||
? { key: `orphaned-${currentValue}`, label: unavailableLabel, value: currentValue }
|
||||
: null;
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const filteredItems = items.filter((item) => item.label.toLowerCase().includes(normalizedQuery));
|
||||
const visibleItems = orphanedItem ? [orphanedItem, ...filteredItems] : filteredItems;
|
||||
|
||||
const noneAction = createCheckmarkPreferenceAction({
|
||||
key: "none",
|
||||
label: noneLabel,
|
||||
isSelected: currentValue === null,
|
||||
onSelect: () => onSelect(null),
|
||||
dimmed: true,
|
||||
isPending,
|
||||
});
|
||||
|
||||
const itemActions = visibleItems.map((item) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
isSelected: currentValue === item.value,
|
||||
onSelect: () => onSelect(item.value),
|
||||
dimmed: item.key.startsWith("orphaned-"),
|
||||
isPending,
|
||||
}),
|
||||
);
|
||||
|
||||
return [noneAction, ...itemActions];
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Stack, Switch, Text } from "@mantine/core";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { visiblePreferenceDefinitions } from "@homarr/settings";
|
||||
import { localeConfigurations } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import type { ChildrenAction } from "../../../../lib/children";
|
||||
import { interaction } from "../../../../lib/interaction";
|
||||
import { useUserPreferences } from "../../../../preferences/use-user-preference";
|
||||
import { preferenceChildrenOptionsByKey, preferenceIcons } from "../../../../preferences/preference-registry";
|
||||
|
||||
dayjs.extend(localeData);
|
||||
|
||||
type SettingsAction = ChildrenAction<Record<string, unknown>>;
|
||||
|
||||
const BooleanRow = ({
|
||||
label,
|
||||
Icon,
|
||||
checked,
|
||||
isPending,
|
||||
}: {
|
||||
label: string;
|
||||
Icon: TablerIcon;
|
||||
checked: boolean;
|
||||
isPending: boolean;
|
||||
}) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
<Switch checked={checked} readOnly size="md" disabled={isPending} styles={{ track: { cursor: "pointer" } }} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
const SelectRow = ({ label, Icon, valueLabel }: { label: string; Icon: TablerIcon; valueLabel: string }) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{valueLabel}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const LinkRow = ({ label, Icon }: { label: string; Icon: TablerIcon }) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const SELECT_KINDS = new Set(["select", "searchEngine", "board"]);
|
||||
|
||||
const useSettingsActions = (_: Record<string, unknown>, query: string): SettingsAction[] => {
|
||||
const t = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
const tColorScheme = useScopedI18n("common.colorScheme.options");
|
||||
const { data: session } = useSession();
|
||||
const isAuthenticated = Boolean(session?.user);
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const preferences = useUserPreferences();
|
||||
|
||||
const boardsQuery = clientApi.board.getAllBoards.useQuery(undefined, { enabled: isAuthenticated });
|
||||
const searchEnginesQuery = clientApi.searchEngine.getSelectable.useQuery(undefined, { enabled: isAuthenticated });
|
||||
|
||||
const valueLabelResolvers: Record<string, () => string> = {
|
||||
colorScheme: () => tColorScheme(preferences.getPreference("colorScheme").value as ColorScheme),
|
||||
locale: () => {
|
||||
const val = preferences.getPreference("locale").value;
|
||||
return localeConfigurations[val as keyof typeof localeConfigurations]?.name ?? String(val);
|
||||
},
|
||||
defaultSearchEngineId: () =>
|
||||
searchEnginesQuery.data?.find((e) => e.value === preferences.getPreference("defaultSearchEngineId").value)
|
||||
?.label ?? t("searchEngine.none" as never),
|
||||
firstDayOfWeek: () => dayjs.weekdays(false)[preferences.getPreference("firstDayOfWeek").value as DayOfWeek] ?? "",
|
||||
homeBoardId: () =>
|
||||
boardsQuery.data?.find((b) => b.id === preferences.getPreference("homeBoardId").value)?.name ??
|
||||
t("board.none" as never),
|
||||
mobileHomeBoardId: () =>
|
||||
boardsQuery.data?.find((b) => b.id === preferences.getPreference("mobileHomeBoardId").value)?.name ??
|
||||
t("board.none" as never),
|
||||
};
|
||||
|
||||
return visiblePreferenceDefinitions(isAuthenticated)
|
||||
.map((definition): SettingsAction | null => {
|
||||
const label = t(`${definition.key}.label` as never);
|
||||
const Icon = preferenceIcons[definition.key];
|
||||
|
||||
const matchesSearch = [label, ...definition.aliases].some((v) => v.toLowerCase().includes(normalizedQuery));
|
||||
if (!matchesSearch) return null;
|
||||
|
||||
if (definition.kind === "boolean") {
|
||||
const pref = preferences.getPreference(definition.key);
|
||||
return {
|
||||
key: definition.key,
|
||||
Component: () => (
|
||||
<BooleanRow label={label} Icon={Icon} checked={pref.value as boolean} isPending={pref.isPending} />
|
||||
),
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: pref.isPending ? () => undefined : () => pref.setValue(!(pref.value as boolean) as never),
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.kind === "link") {
|
||||
return {
|
||||
key: definition.key,
|
||||
Component: () => <LinkRow label={label} Icon={Icon} />,
|
||||
useInteraction: interaction.link(() => ({ href: `/manage/users/${session?.user.id ?? ""}/general` })),
|
||||
};
|
||||
}
|
||||
|
||||
if (SELECT_KINDS.has(definition.kind)) {
|
||||
const children = preferenceChildrenOptionsByKey[definition.key];
|
||||
if (!children) return null;
|
||||
return {
|
||||
key: definition.key,
|
||||
Component: () => (
|
||||
<SelectRow label={label} Icon={Icon} valueLabel={valueLabelResolvers[definition.key]?.() ?? ""} />
|
||||
),
|
||||
useInteraction: interaction.children(children),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((action): action is SettingsAction => action !== null);
|
||||
};
|
||||
|
||||
export const settingsChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: useSettingsActions,
|
||||
DetailComponent: () => {
|
||||
const t = useScopedI18n("search.mode.command.group.preferences");
|
||||
return (
|
||||
<Stack mx="md" my="sm" gap="xs">
|
||||
<Text>{t("title")}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("children.detail.backHint")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Group, Text, useComputedColorScheme, useMantineColorScheme } from "@mantine/core";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import {
|
||||
IconBox,
|
||||
IconCategoryPlus,
|
||||
IconDeviceDesktop,
|
||||
IconFileImport,
|
||||
IconLanguage,
|
||||
IconMailForward,
|
||||
IconMoon,
|
||||
IconPlug,
|
||||
IconSun,
|
||||
IconUserPlus,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -22,7 +18,6 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import { createGroup } from "../../lib/group";
|
||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
import { languageChildrenOptions } from "./children/language";
|
||||
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
@@ -52,33 +47,9 @@ export const globalCommandGroup = createGroup<Command>({
|
||||
},
|
||||
useOptions() {
|
||||
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const computedColorScheme = useComputedColorScheme("light");
|
||||
const { data: session } = useSession();
|
||||
|
||||
const commands: (Command & { hidden?: boolean })[] = [
|
||||
{
|
||||
commandKey: "colorScheme",
|
||||
icon: colorScheme === "auto" ? IconDeviceDesktop : computedColorScheme === "dark" ? IconSun : IconMoon,
|
||||
name:
|
||||
colorScheme === "auto"
|
||||
? tOption("colorScheme.auto")
|
||||
: tOption(`colorScheme.${computedColorScheme === "dark" ? "light" : "dark"}`),
|
||||
useInteraction: () => {
|
||||
const { toggleColorScheme } = useMantineColorScheme();
|
||||
|
||||
return {
|
||||
type: "javaScript",
|
||||
onSelect: toggleColorScheme,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
commandKey: "language",
|
||||
icon: IconLanguage,
|
||||
name: tOption("language.label"),
|
||||
useInteraction: interaction.children(languageChildrenOptions),
|
||||
},
|
||||
{
|
||||
commandKey: "newBoard",
|
||||
icon: IconCategoryPlus,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
|
||||
import { globalCommandGroup } from "./global-group";
|
||||
import { preferencesGroup } from "../preferences/groups";
|
||||
|
||||
export const commandMode = {
|
||||
modeKey: "command",
|
||||
character: ">",
|
||||
groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
|
||||
groups: [contextSpecificActionsSearchGroups, preferencesGroup, globalCommandGroup],
|
||||
} satisfies SearchMode;
|
||||
@@ -64,6 +64,36 @@ type MediaRequestChildrenProps = {
|
||||
kind: IntegrationKind;
|
||||
url: string;
|
||||
id: string;
|
||||
permissions?: {
|
||||
hasInteractAccess: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const useMediaRequestSearchInteraction = (
|
||||
integration: MediaRequestChildrenProps["integration"],
|
||||
searchResult: FromIntegrationSearchResult,
|
||||
): inferSearchInteractionDefinition<"link" | "children"> => {
|
||||
const { openSearchInNewTab } = useSettings();
|
||||
|
||||
const type = searchResult.type;
|
||||
if (type === "person") {
|
||||
return {
|
||||
type: "link",
|
||||
href: searchResult.link,
|
||||
newTab: openSearchInNewTab,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "children",
|
||||
...mediaRequestsChildrenOptions({
|
||||
result: {
|
||||
...searchResult,
|
||||
type,
|
||||
},
|
||||
integration,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -71,8 +101,6 @@ export const useFromIntegrationSearchInteraction = (
|
||||
searchEngine: SearchEngine,
|
||||
searchResult: FromIntegrationSearchResult,
|
||||
): inferSearchInteractionDefinition<"link" | "javaScript" | "children"> => {
|
||||
const { openSearchInNewTab } = useSettings();
|
||||
|
||||
if (searchEngine.type !== "fromIntegration") {
|
||||
throw new Error("Invalid search engine type");
|
||||
}
|
||||
@@ -87,25 +115,7 @@ export const useFromIntegrationSearchInteraction = (
|
||||
) &&
|
||||
"type" in searchResult
|
||||
) {
|
||||
const type = searchResult.type;
|
||||
if (type === "person") {
|
||||
return {
|
||||
type: "link",
|
||||
href: searchResult.link,
|
||||
newTab: openSearchInNewTab,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "children",
|
||||
...mediaRequestsChildrenOptions({
|
||||
result: {
|
||||
...searchResult,
|
||||
type,
|
||||
},
|
||||
integration: searchEngine.integration,
|
||||
}),
|
||||
};
|
||||
return useMediaRequestSearchInteraction(searchEngine.integration, searchResult);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -115,13 +125,15 @@ export const useFromIntegrationSearchInteraction = (
|
||||
};
|
||||
};
|
||||
|
||||
const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenProps>({
|
||||
export const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenProps>({
|
||||
useActions() {
|
||||
const { openModal } = useModalAction(RequestMediaModal);
|
||||
return [
|
||||
{
|
||||
key: "request",
|
||||
hide: (option) => option.result.inLibrary,
|
||||
hide: (option) =>
|
||||
(option.result.type === "movie" && option.result.inLibrary) ||
|
||||
option.integration.permissions?.hasInteractAccess === false,
|
||||
Component(option) {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
return (
|
||||
|
||||
@@ -19,12 +19,13 @@ import type { SearchMode } from "../../lib/mode";
|
||||
import { appIntegrationBoardMode } from "../app-integration-board";
|
||||
import { commandMode } from "../command";
|
||||
import { externalMode } from "../external";
|
||||
import { mediaMode } from "../media";
|
||||
import { pageMode } from "../page";
|
||||
import { preferencesGroup } from "../preferences/groups";
|
||||
import { userGroupMode } from "../user-group";
|
||||
|
||||
// We need type to allow it as generic type for Record<string, unknown>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type QuickLinkOption = {
|
||||
key: string;
|
||||
icon: TablerIcon;
|
||||
name: string;
|
||||
path: string;
|
||||
@@ -33,7 +34,7 @@ type QuickLinkOption = {
|
||||
export const useHomeEmptyGroups = () => {
|
||||
const { data: session } = useSession();
|
||||
const tPages = useScopedI18n("search.mode.page.group.page.option");
|
||||
const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, commandMode, pageMode];
|
||||
const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, mediaMode, commandMode, pageMode];
|
||||
|
||||
if (session?.user.permissions.includes("admin")) {
|
||||
visibleSearchModes.unshift(userGroupMode);
|
||||
@@ -41,19 +42,13 @@ export const useHomeEmptyGroups = () => {
|
||||
|
||||
return [
|
||||
createGroup({
|
||||
keyPath: "path",
|
||||
keyPath: "key",
|
||||
title: () => "Quick links",
|
||||
options: (() => {
|
||||
const quickLinks: QuickLinkOption[] = [];
|
||||
if (session?.user) {
|
||||
quickLinks.push({
|
||||
icon: IconSettings,
|
||||
path: `/manage/users/${session.user.id}/general`,
|
||||
name: tPages("preferences.label"),
|
||||
});
|
||||
}
|
||||
|
||||
quickLinks.push({
|
||||
key: "manageBoards",
|
||||
icon: IconLayoutDashboard,
|
||||
path: "/manage/boards",
|
||||
name: tPages("manageBoard.label"),
|
||||
@@ -61,6 +56,7 @@ export const useHomeEmptyGroups = () => {
|
||||
|
||||
if (session?.user.permissions.includes("admin")) {
|
||||
quickLinks.push({
|
||||
key: "manageSettings",
|
||||
icon: IconSettings,
|
||||
path: "/manage/settings",
|
||||
name: tPages("manageSettings.label"),
|
||||
@@ -69,6 +65,7 @@ export const useHomeEmptyGroups = () => {
|
||||
|
||||
if (session) {
|
||||
quickLinks.push({
|
||||
key: "manageSearchEngines",
|
||||
icon: IconSearch,
|
||||
path: "/manage/search-engines",
|
||||
name: tPages("manageSearchEngine.label"),
|
||||
@@ -87,7 +84,7 @@ export const useHomeEmptyGroups = () => {
|
||||
useInteraction: interaction.link(({ path }) => ({ href: path })),
|
||||
}),
|
||||
createGroup({
|
||||
keyPath: "character",
|
||||
keyPath: "modeKey",
|
||||
title: (t) => t("search.mode.help.group.mode.title"),
|
||||
options: visibleSearchModes.map(({ character, modeKey }) => ({ character, modeKey })),
|
||||
Component: ({ modeKey, character }) => {
|
||||
@@ -96,7 +93,7 @@ export const useHomeEmptyGroups = () => {
|
||||
return (
|
||||
<Group px="md" py="xs" w="100%" wrap="nowrap" align="center" justify="space-between">
|
||||
<Text>{t("help")}</Text>
|
||||
<Kbd size="sm">{character}</Kbd>
|
||||
{character && <Kbd size="sm">{character}</Kbd>}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
@@ -138,3 +135,5 @@ export const useHomeEmptyGroups = () => {
|
||||
}),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const useHomeEmptyGroupsWithPreferences = () => [preferencesGroup, ...useHomeEmptyGroups()];
|
||||
@@ -20,6 +20,7 @@ type GroupItem = {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon: TablerIcon | string;
|
||||
disabled?: boolean;
|
||||
useInteraction: (query: string) => inferSearchInteractionDefinition<SearchInteraction>;
|
||||
};
|
||||
|
||||
@@ -37,7 +38,7 @@ export const homeSearchEngineGroup = createGroup<GroupItem>({
|
||||
);
|
||||
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs" opacity={item.disabled ? 0.55 : 1}>
|
||||
{icon}
|
||||
<Stack gap={0}>
|
||||
<Text>{item.name}</Text>
|
||||
@@ -91,9 +92,7 @@ const createDefaultSearchEntries = (
|
||||
query: string,
|
||||
t: TranslationFunction,
|
||||
): GroupItem[] => {
|
||||
if (!session?.user && !defaultSearchEngine) {
|
||||
return [];
|
||||
}
|
||||
if (!session?.user && !defaultSearchEngine) return [];
|
||||
|
||||
if (!defaultSearchEngine) {
|
||||
return [
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { preferencesGroup } from "../preferences/groups";
|
||||
import { contextSpecificSearchGroups } from "./context-specific-group";
|
||||
import { homeSearchEngineGroup } from "./home-search-engine-group";
|
||||
|
||||
export const homeMode = {
|
||||
character: undefined,
|
||||
modeKey: "home",
|
||||
groups: [homeSearchEngineGroup, contextSpecificSearchGroups],
|
||||
groups: [preferencesGroup, homeSearchEngineGroup, contextSpecificSearchGroups],
|
||||
} satisfies SearchMode;
|
||||
@@ -2,6 +2,7 @@ import { appIntegrationBoardMode } from "./app-integration-board";
|
||||
import { commandMode } from "./command";
|
||||
import { externalMode } from "./external";
|
||||
import { homeMode } from "./home";
|
||||
import { mediaMode } from "./media";
|
||||
import { pageMode } from "./page";
|
||||
import { userGroupMode } from "./user-group";
|
||||
|
||||
@@ -11,5 +12,6 @@ export const searchModes = [
|
||||
externalMode,
|
||||
commandMode,
|
||||
pageMode,
|
||||
mediaMode,
|
||||
homeMode,
|
||||
] as const;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { mediaRequestSearchGroup } from "./media-request-search-group";
|
||||
|
||||
export const mediaMode = {
|
||||
modeKey: "media",
|
||||
character: undefined,
|
||||
groups: [mediaRequestSearchGroup],
|
||||
} satisfies SearchMode;
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Badge, Group, Image, Stack, Text } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconMovie, IconSearch } from "@tabler/icons-react";
|
||||
import { keepPreviousData } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import type { MediaAvailability } from "@homarr/integrations/types";
|
||||
import { mediaAvailabilityConfiguration } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { mediaRequestSearchScopeAtom } from "../../spotlight-store";
|
||||
import { useMediaRequestSearchInteraction } from "../external/search-engines-search-group";
|
||||
|
||||
const availabilityLabelKeys = ["available", "partiallyAvailable", "processing", "requested", "pending"] as const;
|
||||
type AvailabilityWithLabel = (typeof availabilityLabelKeys)[number];
|
||||
|
||||
type MediaRequestSearchResult = RouterOutputs["integration"]["searchMediaRequests"][number];
|
||||
|
||||
type MediaRequestSearchOption =
|
||||
| {
|
||||
key: string;
|
||||
kind: "disabled" | "hint";
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
| {
|
||||
key: string;
|
||||
kind: "result";
|
||||
result: MediaRequestSearchResult;
|
||||
};
|
||||
|
||||
export const mediaRequestSearchGroup = createGroup<MediaRequestSearchOption>({
|
||||
keyPath: "key",
|
||||
title: (t) => t("search.mode.media.group.title"),
|
||||
Component(option) {
|
||||
const tMedia = useScopedI18n("search.mode.media");
|
||||
|
||||
if (option.kind !== "result") {
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs" opacity={option.kind === "disabled" ? 0.55 : 1}>
|
||||
{option.kind === "disabled" ? <IconMovie stroke={1.5} /> : <IconSearch stroke={1.5} />}
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{option.label}</Text>
|
||||
<Text size="xs" c="gray.6">
|
||||
{option.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const { result } = option;
|
||||
const hasAvailabilityLabel =
|
||||
result.availability && (availabilityLabelKeys as readonly string[]).includes(result.availability);
|
||||
const badgeLabel = hasAvailabilityLabel
|
||||
? tMedia(`availability.${result.availability as AvailabilityWithLabel}`)
|
||||
: undefined;
|
||||
const badgeColor = result.availability
|
||||
? mediaAvailabilityConfiguration[result.availability as MediaAvailability]?.color
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||
{result.image ? (
|
||||
<Image src={result.image} w={35} h={50} fit="cover" radius="md" />
|
||||
) : (
|
||||
<IconSearch stroke={1.5} size={35} />
|
||||
)}
|
||||
<Stack gap={2} style={{ flex: 1 }}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text>{result.name}</Text>
|
||||
{badgeLabel && (
|
||||
<Badge size="xs" color={badgeColor} variant="light">
|
||||
{badgeLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text c="dimmed" size="sm" lineClamp={2}>
|
||||
{result.text ?? getIntegrationName(result.integration.kind)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction(option) {
|
||||
if (option.kind !== "result") {
|
||||
return {
|
||||
type: "none",
|
||||
};
|
||||
}
|
||||
|
||||
return useMediaRequestSearchInteraction(option.result.integration, option.result);
|
||||
},
|
||||
useQueryOptions(query) {
|
||||
const tMedia = useScopedI18n("search.mode.media");
|
||||
const scope = useAtomValue(mediaRequestSearchScopeAtom);
|
||||
const [debouncedQuery] = useDebouncedValue(query.trim(), 150);
|
||||
const targetsQuery = clientApi.integration.mediaRequestSearchTargets.useQuery();
|
||||
|
||||
const scopedTargets = (targetsQuery.data ?? []).filter((target) =>
|
||||
scope.integrationIds ? scope.integrationIds.includes(target.id) : true,
|
||||
);
|
||||
const hasScopedTargets = scopedTargets.length > 0;
|
||||
const shouldSearch = debouncedQuery.length > 0 && hasScopedTargets;
|
||||
const integrationIds = scope.integrationIds ? scopedTargets.map((target) => target.id) : undefined;
|
||||
|
||||
const resultsQuery = clientApi.integration.searchMediaRequests.useQuery(
|
||||
{ query: debouncedQuery, integrationIds },
|
||||
{
|
||||
enabled: shouldSearch,
|
||||
placeholderData: keepPreviousData,
|
||||
select: (data) => data.slice(0, 10),
|
||||
},
|
||||
);
|
||||
|
||||
const isLoading = targetsQuery.isLoading || (shouldSearch && resultsQuery.isLoading);
|
||||
const isError = targetsQuery.isError || (shouldSearch && resultsQuery.isError);
|
||||
|
||||
if (!targetsQuery.data) {
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasScopedTargets) {
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
data: [
|
||||
{
|
||||
key: "disabled",
|
||||
kind: "disabled",
|
||||
label: tMedia("action.search.label"),
|
||||
description: scope.integrationIds
|
||||
? tMedia("action.search.disabled.scoped")
|
||||
: tMedia("action.search.disabled.noIntegration"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (debouncedQuery.length === 0) {
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
data: [
|
||||
{
|
||||
key: "hint",
|
||||
kind: "hint",
|
||||
label: tMedia("action.search.label"),
|
||||
description: tMedia("action.search.description"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
data: (resultsQuery.data ?? []).map((result) => ({
|
||||
key: `result-${result.integration.id}-${result.type}-${result.id}`,
|
||||
kind: "result",
|
||||
result,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { colorSchemes } from "@homarr/definitions";
|
||||
import { visiblePreferenceDefinitions } from "@homarr/settings";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { preferenceChildrenOptionsByKey, preferenceIcons } from "../../preferences/preference-registry";
|
||||
import { settingsChildrenOptions } from "../command/children/preferences/settings-children";
|
||||
|
||||
type PreferencesGroupOption = { key: "settings"; preferenceKey: null } | { key: string; preferenceKey: string };
|
||||
|
||||
const settingsOption: PreferencesGroupOption = { key: "settings", preferenceKey: null };
|
||||
|
||||
export const preferencesGroup = createGroup<PreferencesGroupOption>({
|
||||
keyPath: "key",
|
||||
title: (t) => t("search.mode.command.group.preferences.title"),
|
||||
useOptions(query) {
|
||||
const t = useI18n();
|
||||
const tColorScheme = useScopedI18n("common.colorScheme.options");
|
||||
const { data: session } = useSession();
|
||||
const definitions = visiblePreferenceDefinitions(Boolean(session?.user));
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
if (q.length === 0) return [settingsOption];
|
||||
|
||||
const matching = definitions.flatMap((def): PreferencesGroupOption[] => {
|
||||
if (!preferenceChildrenOptionsByKey[def.key]) return [];
|
||||
const label = t(`search.mode.command.group.preferences.option.${def.key}.label` as never);
|
||||
const searchValues = [
|
||||
label,
|
||||
...def.aliases,
|
||||
...(def.key === "colorScheme" ? colorSchemes.map((s) => tColorScheme(s)) : []),
|
||||
];
|
||||
if (!searchValues.some((v) => v.toLowerCase().includes(q))) return [];
|
||||
return [{ key: def.key, preferenceKey: def.key }];
|
||||
});
|
||||
|
||||
if (matching.length > 0) return matching;
|
||||
|
||||
const allSearchValues = [
|
||||
t("search.mode.command.group.preferences.title"),
|
||||
...definitions.flatMap((d) => d.aliases),
|
||||
];
|
||||
return allSearchValues.some((v) => v.toLowerCase().includes(q)) ? [settingsOption] : [];
|
||||
},
|
||||
Component: (option) => {
|
||||
const t = useScopedI18n("search.mode.command.group.preferences");
|
||||
const tOption = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
const Icon = option.preferenceKey
|
||||
? preferenceIcons[option.preferenceKey as keyof typeof preferenceIcons]
|
||||
: IconSettings;
|
||||
const label = option.preferenceKey ? tOption(`${option.preferenceKey}.label` as never) : t("title");
|
||||
|
||||
return (
|
||||
<Group px="md" py="sm" wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
filter: () => true,
|
||||
useInteraction: (option) => ({
|
||||
type: "children",
|
||||
...(option.preferenceKey
|
||||
? (preferenceChildrenOptionsByKey[option.preferenceKey as keyof typeof preferenceChildrenOptionsByKey]?.({}) ??
|
||||
settingsChildrenOptions({}))
|
||||
: settingsChildrenOptions({})),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
IconActivity,
|
||||
IconCalendarWeek,
|
||||
IconDeviceDesktop,
|
||||
IconExternalLink,
|
||||
IconHome,
|
||||
IconLanguage,
|
||||
IconLayoutBoard,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconWorldWww,
|
||||
} from "@tabler/icons-react";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { colorSchemes } from "@homarr/definitions";
|
||||
import type { UserPreferenceKey } from "@homarr/settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../lib/children";
|
||||
import {
|
||||
createCheckmarkPreferenceAction,
|
||||
createLoadingPreferenceAction,
|
||||
PreferenceDetailHeader,
|
||||
} from "../modes/command/children/preferences/action-row";
|
||||
import { createSelectablePreferenceActions } from "../modes/command/children/preferences/selectable-actions";
|
||||
import { languageChildrenOptions } from "../modes/command/children/language";
|
||||
import { useUserPreference } from "./use-user-preference";
|
||||
|
||||
dayjs.extend(localeData);
|
||||
|
||||
type ChildrenFactory = ReturnType<typeof createChildrenOptions<Record<string, unknown>>>;
|
||||
|
||||
export const preferenceIcons: Record<UserPreferenceKey, TablerIcon> = {
|
||||
colorScheme: IconDeviceDesktop,
|
||||
locale: IconLanguage,
|
||||
defaultSearchEngineId: IconSearch,
|
||||
openSearchInNewTab: IconExternalLink,
|
||||
ddgBangs: IconWorldWww,
|
||||
firstDayOfWeek: IconCalendarWeek,
|
||||
homeBoardId: IconHome,
|
||||
mobileHomeBoardId: IconLayoutBoard,
|
||||
pingIconsEnabled: IconActivity,
|
||||
fullPreferencesPage: IconSettings,
|
||||
};
|
||||
|
||||
const createStaticChildren = (
|
||||
preferenceKey: UserPreferenceKey,
|
||||
useOptions: () => Array<{ key: string; label: string; value: unknown }>,
|
||||
): ChildrenFactory =>
|
||||
createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: () => {
|
||||
const { value, setValue, isPending } = useUserPreference(preferenceKey);
|
||||
return useOptions().map((opt) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: opt.key,
|
||||
label: opt.label,
|
||||
isSelected: value === opt.value,
|
||||
onSelect: () => setValue(opt.value as never),
|
||||
isPending,
|
||||
}),
|
||||
);
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey={`${preferenceKey}.children.detail.title`} />,
|
||||
});
|
||||
|
||||
const createAsyncSelectableChildren = <TItem,>(
|
||||
preferenceKey: UserPreferenceKey,
|
||||
useQuery: () => { data: TItem[] | undefined; isLoading: boolean },
|
||||
mapItem: (item: TItem) => { key: string; label: string; value: string },
|
||||
noneKey: string,
|
||||
unavailableKey: string,
|
||||
): ChildrenFactory =>
|
||||
createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: (_, query) => {
|
||||
const { value, setValue, isPending } = useUserPreference(preferenceKey);
|
||||
const result = useQuery();
|
||||
const t = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
|
||||
if (result.isLoading) return [createLoadingPreferenceAction()];
|
||||
|
||||
return createSelectablePreferenceActions({
|
||||
query,
|
||||
currentValue: value as string | null,
|
||||
noneLabel: t(noneKey as never),
|
||||
unavailableLabel: t(unavailableKey as never),
|
||||
items: (result.data ?? []).map(mapItem),
|
||||
onSelect: (v) => setValue(v as never),
|
||||
isPending,
|
||||
});
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey={`${preferenceKey}.children.detail.title`} />,
|
||||
});
|
||||
|
||||
const firstDayOfWeekOptions: DayOfWeek[] = [1, 6, 0];
|
||||
|
||||
export const preferenceChildrenOptionsByKey: Partial<Record<UserPreferenceKey, ChildrenFactory>> = {
|
||||
colorScheme: createStaticChildren("colorScheme", () => {
|
||||
const t = useScopedI18n("common.colorScheme.options");
|
||||
return colorSchemes.map((s) => ({ key: s, label: t(s), value: s }));
|
||||
}),
|
||||
locale: languageChildrenOptions,
|
||||
defaultSearchEngineId: createAsyncSelectableChildren(
|
||||
"defaultSearchEngineId",
|
||||
() => clientApi.searchEngine.getSelectable.useQuery(),
|
||||
(e) => ({ key: e.value, label: e.label, value: e.value }),
|
||||
"searchEngine.none",
|
||||
"searchEngine.unavailable",
|
||||
),
|
||||
firstDayOfWeek: createStaticChildren("firstDayOfWeek", () =>
|
||||
firstDayOfWeekOptions.map((day) => ({
|
||||
key: day.toString(),
|
||||
label: dayjs.weekdays(false)[day] ?? day.toString(),
|
||||
value: day,
|
||||
})),
|
||||
),
|
||||
homeBoardId: createAsyncSelectableChildren(
|
||||
"homeBoardId",
|
||||
() => clientApi.board.getAllBoards.useQuery(),
|
||||
(b) => ({ key: b.id, label: b.name, value: b.id }),
|
||||
"board.none",
|
||||
"board.unavailable",
|
||||
),
|
||||
mobileHomeBoardId: createAsyncSelectableChildren(
|
||||
"mobileHomeBoardId",
|
||||
() => clientApi.board.getAllBoards.useQuery(),
|
||||
(b) => ({ key: b.id, label: b.name, value: b.id }),
|
||||
"board.none",
|
||||
"board.unavailable",
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMantineColorScheme } from "@mantine/core";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
import { userPreferenceDefinitions, userPreferenceDefinitionByKey } from "@homarr/settings";
|
||||
import type { UserPreferenceKey } from "@homarr/settings";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
// --- Optimistic store (module-level, shared across all hook instances) ---
|
||||
|
||||
const optimisticRef: { current: Partial<Record<UserPreferenceKey, unknown>> } = { current: {} };
|
||||
let optimisticVersion = 0;
|
||||
const optimisticListeners = new Set<() => void>();
|
||||
|
||||
const notifyListeners = () => {
|
||||
optimisticVersion += 1;
|
||||
optimisticListeners.forEach((fn) => fn());
|
||||
};
|
||||
|
||||
const subscribeOptimistic = (listener: () => void) => {
|
||||
optimisticListeners.add(listener);
|
||||
return () => optimisticListeners.delete(listener);
|
||||
};
|
||||
|
||||
const setOptimistic = (key: UserPreferenceKey, value: unknown) => {
|
||||
optimisticRef.current[key] = value;
|
||||
notifyListeners();
|
||||
};
|
||||
|
||||
const clearOptimistic = (keys: readonly UserPreferenceKey[]) => {
|
||||
let changed = false;
|
||||
for (const key of keys) {
|
||||
changed ||= key in optimisticRef.current;
|
||||
delete optimisticRef.current[key];
|
||||
}
|
||||
if (changed) notifyListeners();
|
||||
};
|
||||
|
||||
const CLIENT_ONLY_GROUPS = new Set(["colorScheme", "locale"]);
|
||||
|
||||
const isDbBacked = (key: UserPreferenceKey) => {
|
||||
const group = userPreferenceDefinitionByKey[key].mutationGroup;
|
||||
return group !== null && !CLIENT_ONLY_GROUPS.has(group);
|
||||
};
|
||||
|
||||
const getGroupKeys = (key: UserPreferenceKey): UserPreferenceKey[] => {
|
||||
const group = userPreferenceDefinitionByKey[key].mutationGroup;
|
||||
if (!group) return [key];
|
||||
return userPreferenceDefinitions.filter((d) => d.mutationGroup === group).map((d) => d.key);
|
||||
};
|
||||
|
||||
// --- Hook ---
|
||||
|
||||
export const useUserPreferences = () => {
|
||||
const router = useRouter();
|
||||
const settings = useSettings();
|
||||
const settingsRef = useRef(settings);
|
||||
settingsRef.current = settings;
|
||||
|
||||
const { data: session } = useSession();
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
const currentLocale = useCurrentLocale();
|
||||
const { changeLocale } = useChangeLocale();
|
||||
const t = useI18n();
|
||||
const userId = session?.user.id;
|
||||
const pendingRef = useRef<Record<string, boolean>>({});
|
||||
const prevUserIdRef = useRef(userId);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevUserIdRef.current === userId) return;
|
||||
prevUserIdRef.current = userId;
|
||||
clearOptimistic(userPreferenceDefinitions.map((d) => d.key));
|
||||
}, [userId]);
|
||||
|
||||
useSyncExternalStore(
|
||||
subscribeOptimistic,
|
||||
() => optimisticVersion,
|
||||
() => optimisticVersion,
|
||||
);
|
||||
|
||||
const refresh = () => router.refresh();
|
||||
|
||||
const searchMutation = clientApi.user.changeSearchPreferences.useMutation({ onSuccess: refresh });
|
||||
const homeBoardsMutation = clientApi.user.changeHomeBoards.useMutation({ onSuccess: refresh });
|
||||
const firstDayMutation = clientApi.user.changeFirstDayOfWeek.useMutation({ onSuccess: refresh });
|
||||
const pingMutation = clientApi.user.changePingIconsEnabled.useMutation({ onSuccess: refresh });
|
||||
|
||||
const getEffective = (key: UserPreferenceKey): unknown =>
|
||||
key in optimisticRef.current ? optimisticRef.current[key] : settingsRef.current[key as keyof SettingsContextProps];
|
||||
|
||||
const pick = (field: UserPreferenceKey, changedKey: UserPreferenceKey, value: unknown) =>
|
||||
field === changedKey ? value : getEffective(field);
|
||||
|
||||
const groupDispatchers: Record<string, (uid: string, key: UserPreferenceKey, value: unknown) => Promise<unknown>> = {
|
||||
searchPreferences: (uid, key, value) =>
|
||||
searchMutation.mutateAsync({
|
||||
userId: uid,
|
||||
defaultSearchEngineId: pick("defaultSearchEngineId", key, value) as string | null,
|
||||
openInNewTab: pick("openSearchInNewTab", key, value) as boolean,
|
||||
ddgBangsEnabled: pick("ddgBangs", key, value) as boolean,
|
||||
}),
|
||||
homeBoards: (uid, key, value) =>
|
||||
homeBoardsMutation.mutateAsync({
|
||||
userId: uid,
|
||||
homeBoardId: pick("homeBoardId", key, value) as string | null,
|
||||
mobileHomeBoardId: pick("mobileHomeBoardId", key, value) as string | null,
|
||||
}),
|
||||
firstDayOfWeek: (uid, _key, value) => firstDayMutation.mutateAsync({ id: uid, firstDayOfWeek: value as DayOfWeek }),
|
||||
pingIconsEnabled: (uid, _key, value) => pingMutation.mutateAsync({ id: uid, pingIconsEnabled: value as boolean }),
|
||||
};
|
||||
|
||||
const groupPending: Record<string, boolean> = {
|
||||
searchPreferences: searchMutation.isPending,
|
||||
homeBoards: homeBoardsMutation.isPending,
|
||||
firstDayOfWeek: firstDayMutation.isPending,
|
||||
pingIconsEnabled: pingMutation.isPending,
|
||||
};
|
||||
|
||||
const persistedValues = useMemo<Record<UserPreferenceKey, unknown>>(() => {
|
||||
const values: Record<string, unknown> = {
|
||||
colorScheme: colorScheme as ColorScheme,
|
||||
locale: currentLocale,
|
||||
fullPreferencesPage: userId ? `/manage/users/${userId}/general` : null,
|
||||
};
|
||||
for (const def of userPreferenceDefinitions) {
|
||||
if (def.key in values) continue;
|
||||
values[def.key] = settings[def.key as keyof SettingsContextProps];
|
||||
}
|
||||
return values as Record<UserPreferenceKey, unknown>;
|
||||
}, [colorScheme, currentLocale, userId, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
const matchedKeys = Object.entries(persistedValues)
|
||||
.filter(([key, value]) => optimisticRef.current[key as UserPreferenceKey] === value)
|
||||
.map(([key]) => key as UserPreferenceKey);
|
||||
clearOptimistic(matchedKeys);
|
||||
}, [persistedValues]);
|
||||
|
||||
const getValue = (key: UserPreferenceKey) =>
|
||||
key in optimisticRef.current ? optimisticRef.current[key] : persistedValues[key];
|
||||
|
||||
const isPreferencePending = (key: UserPreferenceKey): boolean => {
|
||||
const group = userPreferenceDefinitionByKey[key].mutationGroup;
|
||||
if (!group || CLIENT_ONLY_GROUPS.has(group)) return false;
|
||||
return (groupPending[group] ?? false) || (pendingRef.current[group] ?? false);
|
||||
};
|
||||
|
||||
const clientSetters: Partial<Record<UserPreferenceKey, (value: unknown) => void>> = {
|
||||
colorScheme: (value) => setColorScheme(value as ColorScheme),
|
||||
locale: (value) => changeLocale(value as typeof currentLocale),
|
||||
fullPreferencesPage: () => undefined,
|
||||
};
|
||||
|
||||
const setValueAsync = async (key: UserPreferenceKey, value: unknown) => {
|
||||
if (isDbBacked(key) && !userId) return;
|
||||
if (isPreferencePending(key)) return;
|
||||
|
||||
const clientSetter = clientSetters[key];
|
||||
if (clientSetter) {
|
||||
clientSetter(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const group = userPreferenceDefinitionByKey[key].mutationGroup;
|
||||
if (!group) return;
|
||||
const dispatcher = groupDispatchers[group];
|
||||
if (!dispatcher || !userId) return;
|
||||
|
||||
setOptimistic(key, value);
|
||||
|
||||
pendingRef.current[group] = true;
|
||||
try {
|
||||
await dispatcher(userId, key, value);
|
||||
} catch {
|
||||
clearOptimistic(getGroupKeys(key));
|
||||
showErrorNotification({
|
||||
message: t("search.mode.command.group.preferences.notification.error.message"),
|
||||
});
|
||||
} finally {
|
||||
pendingRef.current[group] = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getPreference(preferenceKey: UserPreferenceKey) {
|
||||
return {
|
||||
value: getValue(preferenceKey),
|
||||
setValue: (value: never) => setValueAsync(preferenceKey, value),
|
||||
isPending: isPreferencePending(preferenceKey),
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useUserPreference = (key: UserPreferenceKey) => useUserPreferences().getPreference(key);
|
||||
@@ -3,9 +3,17 @@
|
||||
import { clamp } from "@mantine/hooks";
|
||||
import type { SpotlightStore } from "@mantine/spotlight";
|
||||
import { createSpotlight } from "@mantine/spotlight";
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const [spotlightStore, spotlightActions] = createSpotlight();
|
||||
|
||||
export interface MediaRequestSearchScope {
|
||||
integrationIds?: string[];
|
||||
}
|
||||
|
||||
export const mediaRequestSearchScopeAtom = atom<MediaRequestSearchScope>({});
|
||||
export const mediaRequestSearchEvent = "homarr:spotlight:media-request-search";
|
||||
|
||||
export const setSelectedAction = (index: number, store: SpotlightStore) => {
|
||||
store.updateState((state) => ({ ...state, selected: index }));
|
||||
};
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Contrasenya",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Inclou com a mínim 8 caràcters",
|
||||
"lowercase": "Inclou minúscules",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "密码",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "至少包含 8 个字符",
|
||||
"lowercase": "包含小写字母",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "在新标签页中打开"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "书签",
|
||||
"add": "添加书签"
|
||||
|
||||
@@ -132,12 +132,13 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "crwdns26:0crwdne26:0",
|
||||
"suggestionsTitle": "crwdns4261:0crwdne4261:0",
|
||||
"suggestion": {
|
||||
"length": "crwdns28:0crwdne28:0",
|
||||
"lowercase": "crwdns30:0crwdne30:0",
|
||||
"uppercase": "crwdns32:0crwdne32:0",
|
||||
"number": "crwdns34:0crwdne34:0",
|
||||
"special": "crwdns36:0crwdne36:0"
|
||||
"length": "crwdns4263:0crwdne4263:0",
|
||||
"lowercase": "crwdns4265:0crwdne4265:0",
|
||||
"uppercase": "crwdns4267:0crwdne4267:0",
|
||||
"number": "crwdns4269:0crwdne4269:0",
|
||||
"special": "crwdns4271:0crwdne4271:0"
|
||||
}
|
||||
},
|
||||
"passwordConfirm": {
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "crwdns2686:0crwdne2686:0"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": "crwdns4273:0crwdne4273:0"
|
||||
},
|
||||
"items": {
|
||||
"label": "crwdns764:0crwdne764:0",
|
||||
"add": "crwdns766:0crwdne766:0"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Heslo",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Obsahuje alespoň 8 znaků",
|
||||
"lowercase": "Obsahuje malé písmeno",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Adgangskode",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Inkluderer mindst 8 tegn",
|
||||
"lowercase": "Inkluderer små bogstaver",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Åbn i nyt faneblad"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Bogmærker",
|
||||
"add": "Tilføj bogmærke"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Enthält mindestens 8 Zeichen",
|
||||
"lowercase": "Enthält Kleinbuchstaben",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "In neuem Tab öffnen"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Lesezeichen",
|
||||
"add": "Lesezeichen hinzufügen"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Enthält mindestens 8 Zeichen",
|
||||
"lowercase": "Enthält Kleinbuchstaben",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "In neuem Tab öffnen"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Lesezeichen",
|
||||
"add": "Lesezeichen hinzufügen"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Κωδικός",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "Περιλαμβάνει πεζό γράμμα",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Includes a minimum of 8 characters",
|
||||
"lowercase": "Includes a lower-case letter",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Open in new tab"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Bookmarks",
|
||||
"add": "Add bookmark"
|
||||
@@ -4551,9 +4555,23 @@
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"help": "Search for movies or shows to request",
|
||||
"requestMovie": "",
|
||||
"requestSeries": "",
|
||||
"openIn": ""
|
||||
"openIn": "",
|
||||
"group": {
|
||||
"title": "Movies and shows"
|
||||
},
|
||||
"action": {
|
||||
"search": {
|
||||
"label": "Search for movies or shows",
|
||||
"description": "Start typing to search your media request integrations",
|
||||
"disabled": {
|
||||
"noIntegration": "Add an Overseerr, Jellyseerr, or Seerr integration to enable media search.",
|
||||
"scoped": "This widget does not have an accessible media request integration."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"help": "",
|
||||
@@ -4829,7 +4847,11 @@
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Season",
|
||||
"episodes": "Episodes"
|
||||
"episodes": "Episodes",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"requested": "Requested"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -1303,6 +1303,13 @@
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit item",
|
||||
"tab": {
|
||||
"widget": "Edit widget",
|
||||
"app": "Edit app"
|
||||
},
|
||||
"app": {
|
||||
"propagationNotice": "Changes to this app will be applied everywhere it is used across all dashboards."
|
||||
},
|
||||
"advancedOptions": {
|
||||
"label": "Advanced options",
|
||||
"title": "Advanced item options"
|
||||
@@ -1406,6 +1413,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Open in new tab"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": "Show border"
|
||||
},
|
||||
"items": {
|
||||
"label": "Bookmarks",
|
||||
"add": "Add bookmark"
|
||||
@@ -4548,13 +4558,141 @@
|
||||
"label": "Create a new group"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferences",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select a preference to change",
|
||||
"backHint": "Press Backspace or use the back button to go back",
|
||||
"backAction": "Go back"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"error": {
|
||||
"message": "Unable to update preference"
|
||||
}
|
||||
},
|
||||
"option": {
|
||||
"preferences": {
|
||||
"label": "Open preferences"
|
||||
},
|
||||
"boolean": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"board": {
|
||||
"none": "None",
|
||||
"unavailable": "Currently selected (unavailable)"
|
||||
},
|
||||
"searchEngine": {
|
||||
"none": "None",
|
||||
"unavailable": "Currently selected (unavailable)"
|
||||
},
|
||||
"colorScheme": {
|
||||
"label": "Color scheme",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select your color scheme"
|
||||
}
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"label": "Language",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select your preferred language"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSearchEngineId": {
|
||||
"label": "Default search engine",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select your default search engine"
|
||||
}
|
||||
}
|
||||
},
|
||||
"openSearchInNewTab": {
|
||||
"label": "Open search results in new tab",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Open search results in new tab"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ddgBangs": {
|
||||
"label": "Enable DuckDuckGo bangs",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Enable DuckDuckGo bangs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"firstDayOfWeek": {
|
||||
"label": "First day of the week",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select the first day of the week"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeBoardId": {
|
||||
"label": "Home board",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select your home board"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mobileHomeBoardId": {
|
||||
"label": "Mobile home board",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Select your mobile home board"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pingIconsEnabled": {
|
||||
"label": "Use icons for pings",
|
||||
"children": {
|
||||
"detail": {
|
||||
"title": "Use icons for pings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fullPreferencesPage": {
|
||||
"label": "Open full preferences page"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"help": "Search for movies or shows to request",
|
||||
"requestMovie": "Request movie",
|
||||
"requestSeries": "Request series",
|
||||
"openIn": "Open in {kind}"
|
||||
"openIn": "Open in {kind}",
|
||||
"group": {
|
||||
"title": "Movies and shows"
|
||||
},
|
||||
"action": {
|
||||
"search": {
|
||||
"label": "Search for movies or shows",
|
||||
"description": "Start typing to search your media request integrations",
|
||||
"disabled": {
|
||||
"noIntegration": "Add an Overseerr, Jellyseerr, or Seerr integration to enable media search.",
|
||||
"scoped": "This widget does not have an accessible media request integration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"available": "Available",
|
||||
"partiallyAvailable": "Partial",
|
||||
"processing": "Processing",
|
||||
"requested": "Requested",
|
||||
"pending": "Requested"
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"help": "Use an external search engine",
|
||||
@@ -4830,7 +4968,11 @@
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Season",
|
||||
"episodes": "Episodes"
|
||||
"episodes": "Episodes",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"requested": "Requested"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Contraseña",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Incluye al menos 8 caracteres",
|
||||
"lowercase": "Incluye al menos una letra minúscula",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Abrir en una nueva pestaña"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Marcadores",
|
||||
"add": "Añadir marcador"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Mot de passe",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Contient au moins 8 caractères",
|
||||
"lowercase": "Inclut une lettre minuscule",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Ouvrir dans un nouvel onglet"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Signets",
|
||||
"add": "Ajouter un signet"
|
||||
@@ -2088,7 +2092,7 @@
|
||||
"empty": "",
|
||||
"user": "Utilisateur",
|
||||
"media": "Média",
|
||||
"state": "",
|
||||
"state": "État",
|
||||
"quality": "Qualité",
|
||||
"device": "Appareil",
|
||||
"progress": "Progression",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "סיסמה",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "כולל לפחות 8 תווים",
|
||||
"lowercase": "אפשר אותיות קטנות",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "פתיחה בכרטיסיה חדשה"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "סימניות",
|
||||
"add": "הוסף סימניה"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Lozinka",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "Uključuje mala slova",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Jelszó",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Tartalmazzon legalább 8 karaktert",
|
||||
"lowercase": "Tartalmazzon kisbetűt",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Megnyitás új lapon"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Könyjelzők",
|
||||
"add": "Könyvjelző hozzáadása"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"suggestionsTitle": "Suggerimenti password",
|
||||
"suggestion": {
|
||||
"length": "Include almeno 8 caratteri",
|
||||
"lowercase": "Include lettera minuscola",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Apri in una nuova scheda"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": "Mostra bordo"
|
||||
},
|
||||
"items": {
|
||||
"label": "Segnalibri",
|
||||
"add": "Aggiungi segnalibro"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "パスワード",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "少なくとも 8 文字以上を含む",
|
||||
"lowercase": "小文字を含む",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "新しいタブで開く"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "ブックマーク",
|
||||
"add": "ブックマークに追加"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "비밀번호",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "소문자 포함",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Slaptažodis",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Parole",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "Ietver mazo burtu",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Wachtwoord",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Bevat ten minste 8 tekens",
|
||||
"lowercase": "Inclusief kleine letter",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "In nieuw tabblad openen"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Bladwijzers",
|
||||
"add": "Bladwijzer toevoegen"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Passord",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Inkluderer minst 8 tegn",
|
||||
"lowercase": "Inkluderer liten bokstav",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Åpne i ny fane"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Bokmerke",
|
||||
"add": "Legg til bokmerke"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Hasło",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Zawiera co najmniej 8 znaków",
|
||||
"lowercase": "Zawiera małą literę",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Otwórz w nowej karcie"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Zakładki",
|
||||
"add": "Dodaj zakładkę"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Senha",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Inclua pelo menos 8 caracteres",
|
||||
"lowercase": "Inclui letras minúsculas",
|
||||
@@ -1056,7 +1057,7 @@
|
||||
}
|
||||
},
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Nenhuma mídia enviada"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1088,8 +1089,8 @@
|
||||
"checkLogs": "Verifique os logs para mais detalhes",
|
||||
"tryAgain": "Tente novamente",
|
||||
"loading": "Carregando",
|
||||
"show": "",
|
||||
"hide": ""
|
||||
"show": "Mostrar",
|
||||
"hide": "Esconder"
|
||||
},
|
||||
"here": "aqui",
|
||||
"iconPicker": {
|
||||
@@ -1255,7 +1256,7 @@
|
||||
"label": {
|
||||
"create": "Nova categoria",
|
||||
"changePosition": "Mudar de posição",
|
||||
"settings": ""
|
||||
"settings": "Configurações"
|
||||
}
|
||||
},
|
||||
"openAllInNewTabs": {
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Abrir em nova guia"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Marcadores",
|
||||
"add": "Adicionar marcador"
|
||||
@@ -3459,7 +3463,7 @@
|
||||
"private": "Este quadro é privado"
|
||||
},
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Nenhum painel ainda"
|
||||
},
|
||||
"modal": {
|
||||
"createBoard": {
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Parola",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "Să includă litere mici",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Содержит не менее 8 символов",
|
||||
"lowercase": "Содержит строчную букву",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Открывать в новой вкладке"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Закладки",
|
||||
"add": "Добавить закладку"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Heslo",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Obsahuje aspoň 8 znakov",
|
||||
"lowercase": "Zahŕňa malé písmeno",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Otvoriť na novej karte"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Záložky",
|
||||
"add": "Pridať záložku"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Geslo",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "",
|
||||
"lowercase": "Vključuje male črke",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Lösenord",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Använd minst 8 tecken",
|
||||
"lowercase": "Använd liten bokstav",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Öppna i en ny flik"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Bokmärken",
|
||||
"add": "Addera bokmärke"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Şifre",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "En az 8 karakter içermeli",
|
||||
"lowercase": "Küçük harf içermeli",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Yeni sekmede aç"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Yer İmleri",
|
||||
"add": "Yer imi ekle"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Включає принаймні 8 символів",
|
||||
"lowercase": "Включає малу літеру",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "Відкрити в новій вкладці"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "Закладки",
|
||||
"add": "Додати закладку"
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "Mật khẩu",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "Bao gồm ít nhất 8 kí tự",
|
||||
"lowercase": "Bao gồm chữ thường",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": ""
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "",
|
||||
"add": ""
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
},
|
||||
"password": {
|
||||
"label": "密碼",
|
||||
"suggestionsTitle": "",
|
||||
"suggestion": {
|
||||
"length": "至少包含 8 個字符",
|
||||
"lowercase": "包含小寫字母",
|
||||
@@ -1405,6 +1406,9 @@
|
||||
"openNewTab": {
|
||||
"label": "於新分頁開啟"
|
||||
},
|
||||
"withBorder": {
|
||||
"label": ""
|
||||
},
|
||||
"items": {
|
||||
"label": "書籤",
|
||||
"add": "新增書籤"
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
|
||||
const board = useRequiredBoard();
|
||||
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
||||
select(data) {
|
||||
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
|
||||
hideIcon={options.hideIcon}
|
||||
hideHostname={options.hideHostname}
|
||||
openNewTab={options.openNewTab}
|
||||
withBorder={options.withBorder}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
@@ -67,6 +68,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
|
||||
hideIcon={options.hideIcon}
|
||||
hideHostname={options.hideHostname}
|
||||
openNewTab={options.openNewTab}
|
||||
withBorder={options.withBorder}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
@@ -81,6 +83,7 @@ interface FlexLayoutProps {
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
openNewTab: boolean;
|
||||
withBorder: boolean;
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ const FlexLayout = ({
|
||||
hideIcon,
|
||||
hideHostname,
|
||||
openNewTab,
|
||||
withBorder,
|
||||
hasIconColor,
|
||||
}: FlexLayoutProps) => {
|
||||
const board = useRequiredBoard();
|
||||
@@ -106,7 +110,15 @@ const FlexLayout = ({
|
||||
key={app.id}
|
||||
w="100%"
|
||||
>
|
||||
<Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={4} h="100%">
|
||||
<Card
|
||||
radius={board.itemRadius}
|
||||
className={classes.card}
|
||||
w="100%"
|
||||
display="flex"
|
||||
p={4}
|
||||
h="100%"
|
||||
withBorder={withBorder}
|
||||
>
|
||||
{direction === "row" ? (
|
||||
<VerticalItem
|
||||
app={app}
|
||||
@@ -138,6 +150,7 @@ interface GridLayoutProps {
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
openNewTab: boolean;
|
||||
withBorder: boolean;
|
||||
itemDirection: "horizontal" | "vertical";
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
@@ -148,6 +161,7 @@ const GridLayout = ({
|
||||
hideIcon,
|
||||
hideHostname,
|
||||
openNewTab,
|
||||
withBorder,
|
||||
itemDirection,
|
||||
hasIconColor,
|
||||
}: GridLayoutProps) => {
|
||||
@@ -168,6 +182,7 @@ const GridLayout = ({
|
||||
h="100%"
|
||||
className={combineClasses(classes.card, classes["card-grid"])}
|
||||
radius={board.itemRadius}
|
||||
withBorder={withBorder}
|
||||
p="xs"
|
||||
>
|
||||
{itemDirection === "horizontal" ? (
|
||||
|
||||
@@ -24,6 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
|
||||
hideIcon: factory.switch({ defaultValue: false }),
|
||||
hideHostname: factory.switch({ defaultValue: false }),
|
||||
openNewTab: factory.switch({ defaultValue: true }),
|
||||
withBorder: factory.switch({ defaultValue: false }),
|
||||
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
|
||||
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
|
||||
return (
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
.statItem {
|
||||
padding: 12px;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.statItem {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Image,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { IconSearch, IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { MediaRequestStatus } from "@homarr/integrations/types";
|
||||
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
import classes from "../search-button.module.css";
|
||||
|
||||
export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
@@ -43,7 +58,7 @@ export default function MediaServerWidget({
|
||||
const newData = filteredData.concat(
|
||||
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
|
||||
);
|
||||
return newData.sort((dataA, dataB) => {
|
||||
return newData.toSorted((dataA, dataB) => {
|
||||
if (dataA.status === dataB.status) {
|
||||
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
||||
}
|
||||
@@ -61,25 +76,46 @@ export default function MediaServerWidget({
|
||||
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="mediaRequests-list-scrollArea"
|
||||
scrollbarSize="md"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
||||
{mediaRequests.map((mediaRequest) => (
|
||||
<MediaRequestCard
|
||||
key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
|
||||
request={mediaRequest}
|
||||
isTiny={width <= 256}
|
||||
options={options}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
<Box className={classes.searchRoot}>
|
||||
{!isEditMode && <MediaRequestSearchButton integrationIds={integrationIds} />}
|
||||
<ScrollArea
|
||||
className="mediaRequests-list-scrollArea"
|
||||
scrollbarSize="md"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
||||
{mediaRequests.map((mediaRequest) => (
|
||||
<MediaRequestCard
|
||||
key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
|
||||
request={mediaRequest}
|
||||
isTiny={width <= 256}
|
||||
options={options}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
<ActionIcon
|
||||
className={classes.searchButton}
|
||||
variant="light"
|
||||
size="sm"
|
||||
aria-label={t("action.search.label")}
|
||||
onClick={() => openMediaRequestSearch({ integrationIds })}
|
||||
>
|
||||
<IconSearch size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
interface MediaRequestCardProps {
|
||||
request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequests"][number];
|
||||
isTiny: boolean;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
.searchRoot {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
position: absolute;
|
||||
top: var(--mantine-spacing-xs);
|
||||
right: var(--mantine-spacing-xs);
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.searchRoot:hover .searchButton,
|
||||
.searchRoot:focus-within .searchButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.searchButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar, Card, Grid, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Avatar, Box, Card, Grid, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import type { Icon } from "@tabler/icons-react";
|
||||
import {
|
||||
IconDeviceTv,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IconMovie,
|
||||
IconPlayerPlay,
|
||||
IconReceipt,
|
||||
IconSearch,
|
||||
IconThumbDown,
|
||||
IconThumbUp,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -17,11 +18,13 @@ import combineClasses from "clsx";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { RequestStats } from "@homarr/integrations/types";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
import classes from "./component.module.css";
|
||||
import searchClasses from "../search-button.module.css";
|
||||
|
||||
const OVERSEERR_COLOR = "#ECB000";
|
||||
const JELLYSEERR_COLOR = "#6677CC";
|
||||
@@ -93,83 +96,104 @@ export default function MediaServerWidget({
|
||||
const isTiny = width < 256;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="mediaRequests-stats-layout"
|
||||
h="100%"
|
||||
gap="xs"
|
||||
p="sm"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text className="mediaRequests-stats-stats-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
|
||||
{t("titles.stats.main")}
|
||||
</Text>
|
||||
<Grid className="mediaRequests-stats-stats-grid" gap={4} w="100%">
|
||||
{data.map((stat) => (
|
||||
<Grid.Col
|
||||
className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)}
|
||||
key={stat.name}
|
||||
span={isTiny ? 6 : 3}
|
||||
>
|
||||
<Tooltip label={t(`titles.stats.${stat.name}`)}>
|
||||
<Card p={0} radius={board.itemRadius} className={classes.card}>
|
||||
<Group className="mediaRequests-stats-stat-stack" justify="center" align="center" gap="xs" w="100%">
|
||||
<stat.icon className="mediaRequests-stats-stat-icon" size={16} />
|
||||
<Text className="mediaRequests-stats-stat-value" size="md">
|
||||
{stat.number}
|
||||
<Box className={searchClasses.searchRoot}>
|
||||
{!isEditMode && <MediaRequestSearchButton integrationIds={integrationIds} />}
|
||||
<Stack
|
||||
className="mediaRequests-stats-layout"
|
||||
h="100%"
|
||||
gap="xs"
|
||||
p="sm"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text className="mediaRequests-stats-stats-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
|
||||
{t("titles.stats.main")}
|
||||
</Text>
|
||||
<Grid className="mediaRequests-stats-stats-grid" gap={4} w="100%">
|
||||
{data.map((stat) => (
|
||||
<Grid.Col
|
||||
className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)}
|
||||
key={stat.name}
|
||||
span={isTiny ? 6 : 3}
|
||||
>
|
||||
<Tooltip label={t(`titles.stats.${stat.name}`)}>
|
||||
<Card p={0} radius={board.itemRadius} className={classes.card}>
|
||||
<Group className="mediaRequests-stats-stat-stack" justify="center" align="center" gap="xs" w="100%">
|
||||
<stat.icon className="mediaRequests-stats-stat-icon" size={16} />
|
||||
<Text className="mediaRequests-stats-stat-value" size="md">
|
||||
{stat.number}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text className="mediaRequests-stats-users-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
|
||||
{t("titles.users.main")} ({t("titles.users.requests")})
|
||||
</Text>
|
||||
<Stack className="mediaRequests-stats-users-wrapper" flex={1} w="100%" gap={4} style={{ overflow: "hidden" }}>
|
||||
{requestStats.users.slice(0, 10).map((user) => (
|
||||
<Card
|
||||
component="a"
|
||||
href={user.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={combineClasses(
|
||||
"mediaRequests-stats-users-user-wrapper",
|
||||
`mediaRequests-stats-users-user-${user.id}`,
|
||||
classes.card,
|
||||
)}
|
||||
key={user.id}
|
||||
p="xs"
|
||||
radius={board.itemRadius}
|
||||
>
|
||||
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="sm" justify="space-between">
|
||||
<Group gap={4}>
|
||||
<Tooltip label={user.integration.name}>
|
||||
<Avatar
|
||||
className="mediaRequests-stats-users-user-avatar"
|
||||
size={20}
|
||||
src={user.avatar}
|
||||
bd={`2px solid ${user.integration.kind === "overseerr" ? OVERSEERR_COLOR : JELLYSEERR_COLOR}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text className="mediaRequests-stats-users-user-userName" size="sm">
|
||||
{user.displayName}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text className="mediaRequests-stats-users-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
|
||||
{t("titles.users.main")} ({t("titles.users.requests")})
|
||||
</Text>
|
||||
<Stack className="mediaRequests-stats-users-wrapper" flex={1} w="100%" gap={4} style={{ overflow: "hidden" }}>
|
||||
{requestStats.users.slice(0, 10).map((user) => (
|
||||
<Card
|
||||
component="a"
|
||||
href={user.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={combineClasses(
|
||||
"mediaRequests-stats-users-user-wrapper",
|
||||
`mediaRequests-stats-users-user-${user.id}`,
|
||||
classes.card,
|
||||
)}
|
||||
key={user.id}
|
||||
p="xs"
|
||||
radius={board.itemRadius}
|
||||
>
|
||||
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="sm" justify="space-between">
|
||||
<Group gap={4}>
|
||||
<Tooltip label={user.integration.name}>
|
||||
<Avatar
|
||||
className="mediaRequests-stats-users-user-avatar"
|
||||
size={20}
|
||||
src={user.avatar}
|
||||
bd={`2px solid ${user.integration.kind === "overseerr" ? OVERSEERR_COLOR : JELLYSEERR_COLOR}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text className="mediaRequests-stats-users-user-userName" size="sm">
|
||||
{user.displayName}
|
||||
|
||||
<Text className="mediaRequests-stats-users-user-request-count" size="md" fw={500}>
|
||||
{user.requestCount}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Text className="mediaRequests-stats-users-user-request-count" size="md" fw={500}>
|
||||
{user.requestCount}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
<ActionIcon
|
||||
className={searchClasses.searchButton}
|
||||
variant="light"
|
||||
size="sm"
|
||||
aria-label={t("action.search.label")}
|
||||
onClick={() => openMediaRequestSearch({ integrationIds })}
|
||||
>
|
||||
<IconSearch size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useImperativeHandle, useRef } from "react";
|
||||
import { Alert, Button, Center, Loader, Stack, Text } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { AppFormHandle } from "@homarr/forms-collection";
|
||||
import { AppForm } from "@homarr/forms-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export interface EmbeddedAppEditFormHandle {
|
||||
submitIfDirty: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface EmbeddedAppEditFormProps {
|
||||
appId: string;
|
||||
handleRef: React.Ref<EmbeddedAppEditFormHandle>;
|
||||
}
|
||||
|
||||
export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormProps) => {
|
||||
const t = useI18n();
|
||||
const utils = clientApi.useUtils();
|
||||
const appFormRef = useRef<AppFormHandle>(null);
|
||||
const { data: app, isPending: isLoadingApp, isError, refetch } = clientApi.app.byId.useQuery({ id: appId });
|
||||
|
||||
const { mutateAsync, isPending: isMutating } = clientApi.app.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.app.invalidate();
|
||||
showSuccessNotification({
|
||||
title: t("app.page.edit.notification.success.title"),
|
||||
message: t("app.page.edit.notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("app.page.edit.notification.error.title"),
|
||||
message: t("app.page.edit.notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
handleRef,
|
||||
() => ({
|
||||
submitIfDirty: async () => {
|
||||
if (!appFormRef.current?.isDirty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return appFormRef.current.submit();
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
if (isLoadingApp) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !app) {
|
||||
return (
|
||||
<Stack align="center" gap="md" py="xl">
|
||||
<Text c="dimmed">{t("app.action.select.notFound")}</Text>
|
||||
<Button variant="light" onClick={() => void refetch()}>
|
||||
{t("common.action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Alert icon={<IconInfoCircle size={16} />} color="blue" variant="light">
|
||||
{t("item.edit.app.propagationNotice")}
|
||||
</Alert>
|
||||
<AppForm
|
||||
formRef={appFormRef}
|
||||
hideButtons
|
||||
showBackToOverview={false}
|
||||
buttonLabels={{ submit: "" }}
|
||||
initialValues={app}
|
||||
handleSubmit={async (values) => {
|
||||
await mutateAsync({ id: appId, ...values });
|
||||
}}
|
||||
isPending={isMutating}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
import { useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { Button, Group, Stack, Tabs } from "@mantine/core";
|
||||
import { schemaResolver } from "@mantine/form";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
@@ -20,6 +22,8 @@ import type { OptionsBuilderResult } from "../options";
|
||||
import type { IntegrationSelectOption } from "../widget-integration-select";
|
||||
import { WidgetIntegrationSelect } from "../widget-integration-select";
|
||||
import { WidgetAdvancedOptionsModal } from "./widget-advanced-options-modal";
|
||||
import type { EmbeddedAppEditFormHandle } from "./embedded-app-edit-form";
|
||||
import { EmbeddedAppEditForm } from "./embedded-app-edit-form";
|
||||
|
||||
export interface WidgetEditModalState {
|
||||
options: Record<string, unknown>;
|
||||
@@ -34,13 +38,16 @@ interface ModalProps<TSort extends WidgetKind> {
|
||||
integrationData: IntegrationSelectOption[];
|
||||
integrationSupport: boolean;
|
||||
settings: SettingsContextProps;
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const [advancedOptions, setAdvancedOptions] = useState<BoardItemAdvancedOptions>(innerProps.value.advancedOptions);
|
||||
const appEditRef = useRef<EmbeddedAppEditFormHandle>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Translate the error messages
|
||||
z.config({
|
||||
customError: zodErrorMap(t),
|
||||
});
|
||||
@@ -77,78 +84,128 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
});
|
||||
const { openModal } = useModalAction(WidgetAdvancedOptionsModal);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
innerProps.onSuccessfulEdit({
|
||||
...values,
|
||||
advancedOptions,
|
||||
});
|
||||
actions.closeModal();
|
||||
const canModifyApps = session?.user.permissions.includes("app-modify-all") ?? false;
|
||||
const appId = innerProps.appId;
|
||||
const showAppTab = innerProps.kind === "app" && canModifyApps && Boolean(appId);
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const appSaved = showAppTab ? await (appEditRef.current?.submitIfDirty() ?? true) : true;
|
||||
|
||||
if (!appSaved) {
|
||||
return;
|
||||
}
|
||||
|
||||
innerProps.onSuccessfulEdit({
|
||||
...values,
|
||||
advancedOptions,
|
||||
});
|
||||
actions.closeModal();
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
handleSubmit(event);
|
||||
};
|
||||
|
||||
const widgetFormContent = (
|
||||
<Stack>
|
||||
{innerProps.integrationSupport && (
|
||||
<WidgetIntegrationSelect
|
||||
label={t("item.edit.field.integrations.label")}
|
||||
data={innerProps.integrationData}
|
||||
{...form.getInputProps("integrationIds")}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(options).map(([key, value]) => {
|
||||
const Input = getInputForType(value.type);
|
||||
|
||||
if (
|
||||
!Input ||
|
||||
value.shouldHide?.(
|
||||
form.values.options as never,
|
||||
innerProps.integrationData
|
||||
.filter(({ id }) => form.values.integrationIds.includes(id))
|
||||
.map(({ kind }) => kind),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={key}
|
||||
kind={innerProps.kind}
|
||||
property={key}
|
||||
options={value as never}
|
||||
initialOptions={innerProps.value.options}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<Stack>
|
||||
{innerProps.integrationSupport && (
|
||||
<WidgetIntegrationSelect
|
||||
label={t("item.edit.field.integrations.label")}
|
||||
data={innerProps.integrationData}
|
||||
{...form.getInputProps("integrationIds")}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(options).map(([key, value]) => {
|
||||
const Input = getInputForType(value.type);
|
||||
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
!Input ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
value.shouldHide?.(
|
||||
form.values.options as never,
|
||||
innerProps.integrationData
|
||||
.filter(({ id }) => form.values.integrationIds.includes(id))
|
||||
.map(({ kind }) => kind),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={key}
|
||||
kind={innerProps.kind}
|
||||
property={key}
|
||||
options={value as never}
|
||||
initialOptions={innerProps.value.options}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
advancedOptions,
|
||||
onSuccess(options) {
|
||||
setAdvancedOptions(options);
|
||||
innerProps.onSuccessfulEdit({
|
||||
...innerProps.value,
|
||||
advancedOptions: options,
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("item.edit.advancedOptions.label")}
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
advancedOptions,
|
||||
onSuccess(options) {
|
||||
setAdvancedOptions(options);
|
||||
innerProps.onSuccessfulEdit({
|
||||
...innerProps.value,
|
||||
advancedOptions: options,
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("item.edit.advancedOptions.label")}
|
||||
</Button>
|
||||
{!showAppTab && (
|
||||
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={onFormSubmit}>
|
||||
<FormProvider form={form}>
|
||||
{showAppTab ? (
|
||||
<Stack>
|
||||
<Tabs defaultValue="widget">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="widget">{t("item.edit.tab.widget")}</Tabs.Tab>
|
||||
<Tabs.Tab value="app">{t("item.edit.tab.app")}</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="widget" pt="md">
|
||||
{widgetFormContent}
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="app" pt="md">
|
||||
{appId && <EmbeddedAppEditForm appId={appId} handleRef={appEditRef} />}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Group justify="end">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("common.action.saveChanges")}</Button>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
widgetFormContent
|
||||
)}
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
Generated
+320
-325
@@ -142,53 +142,53 @@ catalogs:
|
||||
specifier: ^12.0.1
|
||||
version: 12.0.1
|
||||
'@tiptap/extension-color':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-highlight':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-image':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-link':
|
||||
specifier: ^3.23.6
|
||||
version: 3.23.6
|
||||
specifier: ^3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-placeholder':
|
||||
specifier: ^3.23.6
|
||||
version: 3.23.6
|
||||
specifier: ^3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-table':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-table-cell':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-table-header':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-table-row':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-task-item':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-task-list':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-text-align':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-text-style':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/extension-underline':
|
||||
specifier: 3.23.6
|
||||
version: 3.23.6
|
||||
specifier: 3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/react':
|
||||
specifier: ^3.23.6
|
||||
version: 3.23.6
|
||||
specifier: ^3.24.0
|
||||
version: 3.24.0
|
||||
'@tiptap/starter-kit':
|
||||
specifier: ^3.23.6
|
||||
version: 3.23.6
|
||||
specifier: ^3.24.0
|
||||
version: 3.24.0
|
||||
'@trpc/client':
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0
|
||||
@@ -756,7 +756,7 @@ importers:
|
||||
version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@mantine/tiptap':
|
||||
specifier: 'catalog:'
|
||||
version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))(@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))(@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@tabler/icons-react':
|
||||
specifier: 'catalog:'
|
||||
version: 3.44.0(react@19.2.6)
|
||||
@@ -2144,6 +2144,9 @@ importers:
|
||||
'@homarr/modals-collection':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../modals-collection
|
||||
'@homarr/notifications':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../notifications
|
||||
'@homarr/settings':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../settings
|
||||
@@ -2396,52 +2399,52 @@ importers:
|
||||
version: 3.44.0(react@19.2.6)
|
||||
'@tiptap/extension-color':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)))
|
||||
version: 3.24.0(@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)))
|
||||
'@tiptap/extension-highlight':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-image':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-link':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-placeholder':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-table':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-table-cell':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-table-header':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-table-row':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-task-item':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-task-list':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-text-align':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-text-style':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-underline':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/react':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
version: 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@tiptap/starter-kit':
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.6
|
||||
version: 3.24.0
|
||||
clsx:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.1
|
||||
@@ -3268,15 +3271,9 @@ packages:
|
||||
'@fastify/proxy-addr@5.1.0':
|
||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||
|
||||
@@ -4932,215 +4929,215 @@ packages:
|
||||
'@testcontainers/redis@12.0.1':
|
||||
resolution: {integrity: sha512-x5iDR6Y2Mc+2rTJYSQlRTtBMCPt77pKxDogpqRwXgJVmWoDrWFpiFcA/dw6IEoNuyxYUg03tbS8/VzCJI5JX6A==}
|
||||
|
||||
'@tiptap/core@3.23.6':
|
||||
resolution: {integrity: sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw==}
|
||||
'@tiptap/core@3.24.0':
|
||||
resolution: {integrity: sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==}
|
||||
peerDependencies:
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-blockquote@3.23.6':
|
||||
resolution: {integrity: sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug==}
|
||||
'@tiptap/extension-blockquote@3.24.0':
|
||||
resolution: {integrity: sha512-DgwEEJ1GbDQcT054ynxoaZGmB9apGeUklPrinq9o6xdLHpdg+bO9HCQzggdB8n21VLLglb8jfAEWsVNwh3eASQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-bold@3.23.6':
|
||||
resolution: {integrity: sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA==}
|
||||
'@tiptap/extension-bold@3.24.0':
|
||||
resolution: {integrity: sha512-CujogYaynasklFKHADUseuvj8X2FnWktTCCo3Hl+nlyRvBTmm5TK2aqiamg3v2P4dBh3O6a70mo8BfRJPuiR1g==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-bubble-menu@3.23.6':
|
||||
resolution: {integrity: sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA==}
|
||||
'@tiptap/extension-bubble-menu@3.24.0':
|
||||
resolution: {integrity: sha512-jRXD+JPu9ayvq78g8hsCxx4q/qUFtrdfIYirRSf5YUseuuUbtfrq83AsGabcygpUTefjJkMQoXNITkh6294Ggw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-bullet-list@3.23.6':
|
||||
resolution: {integrity: sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA==}
|
||||
'@tiptap/extension-bullet-list@3.24.0':
|
||||
resolution: {integrity: sha512-IOpAm5c4XVVVvkOef+V9XYMVpea+3MgBpCQgn83UQRlwO9eIMwmcyxOznu7gQPQVShTEpkt4T6uK+ZN9o8meIA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-code-block@3.23.6':
|
||||
resolution: {integrity: sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ==}
|
||||
'@tiptap/extension-code-block@3.24.0':
|
||||
resolution: {integrity: sha512-NZglw4oHoH6oJ5+HvxxQCYk+wODJmsxzUpRQdsOmje08sekQH+Zt9i4UKimBhg4urpd5r+dKXTslab9a5eQ86w==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-code@3.23.6':
|
||||
resolution: {integrity: sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA==}
|
||||
'@tiptap/extension-code@3.24.0':
|
||||
resolution: {integrity: sha512-MAQtrPRQ+HRmcGotWbksdIGeH1gqayFAdvi4lNGeFT7taHXP1o1XD7CQp7iYIKmg8IU4/MQ+RdetSfuC1A9edQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-color@3.23.6':
|
||||
resolution: {integrity: sha512-2WxYc+X+OGFXbq/Ak0VXODEzfK1l5wz33oTWSuhdSw44HAQM7+SSyiDnMCXDQdqi063ANqMmQmlw5WngjQ6wCA==}
|
||||
'@tiptap/extension-color@3.24.0':
|
||||
resolution: {integrity: sha512-gVv9zfQ9diq/sk+SVJzqadShsOtDK1XPojBZR1zXg1QxedzdwLKuRDxvnlfIc3kFIyaDP4mNrek+anednTbWBA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-text-style': 3.23.6
|
||||
'@tiptap/extension-text-style': 3.24.0
|
||||
|
||||
'@tiptap/extension-document@3.23.6':
|
||||
resolution: {integrity: sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw==}
|
||||
'@tiptap/extension-document@3.24.0':
|
||||
resolution: {integrity: sha512-yxgM3+yXy2XZzEwH43y2Kp8D1BkblxEWLXqo0YCoAKtxyKCcEaT8kdlf70kS7D0+VSzYU4D0iN7VdQIYHcL2mA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-dropcursor@3.23.6':
|
||||
resolution: {integrity: sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA==}
|
||||
'@tiptap/extension-dropcursor@3.24.0':
|
||||
resolution: {integrity: sha512-Dbv1c5LnvG3PT+yEbCNroyOeeUkHq9wcir2pbC7wri7g7d2sCi0+HvKH0MAxLwY3j5NJJSiSyG2ypMaXOAs4sg==}
|
||||
peerDependencies:
|
||||
'@tiptap/extensions': 3.23.6
|
||||
'@tiptap/extensions': 3.24.0
|
||||
|
||||
'@tiptap/extension-floating-menu@3.23.6':
|
||||
resolution: {integrity: sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA==}
|
||||
'@tiptap/extension-floating-menu@3.24.0':
|
||||
resolution: {integrity: sha512-7QEbf3mUzFAkejjQGX9f0L507oMtnOBRwHt2skUTR+9yXgudsN8zaDBSSRHLeMWGk9b7L293ZMA6zCRrZaHrfA==}
|
||||
peerDependencies:
|
||||
'@floating-ui/dom': ^1.0.0
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-gapcursor@3.23.6':
|
||||
resolution: {integrity: sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ==}
|
||||
'@tiptap/extension-gapcursor@3.24.0':
|
||||
resolution: {integrity: sha512-CzCP5/jni5RFwW9jCfBO6auh83GbaioMTpSk6tyR3sd+CbwlBcUdsJFGJkbaRdiSS9dgIyi+6hRbhjpYdHcp+w==}
|
||||
peerDependencies:
|
||||
'@tiptap/extensions': 3.23.6
|
||||
'@tiptap/extensions': 3.24.0
|
||||
|
||||
'@tiptap/extension-hard-break@3.23.6':
|
||||
resolution: {integrity: sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w==}
|
||||
'@tiptap/extension-hard-break@3.24.0':
|
||||
resolution: {integrity: sha512-T/ZEBiHQPMyTqDvXG0tiqBToNeuSemIPmNtdoGSgBN/degVl7VJZqQIrLIvOUHfjf3QkRs7TE/mcqTJsIboO/g==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-heading@3.23.6':
|
||||
resolution: {integrity: sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w==}
|
||||
'@tiptap/extension-heading@3.24.0':
|
||||
resolution: {integrity: sha512-GCSgapIzQPqEGNcVGE0/Pcjg5wITMLYJlrS3GGVw7BPmECJwgexcoOsEwkxtzJnXT/HpFXbvOFW43sM0KeHSjg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-highlight@3.23.6':
|
||||
resolution: {integrity: sha512-GWv6R4iZzVrNeB33qtJDBt/BfVjuc5S/sUF/VDFAtK8h86bg4cEm1tGLEsui4T3+vij/t+P+eoGhkzF6NGogdg==}
|
||||
'@tiptap/extension-highlight@3.24.0':
|
||||
resolution: {integrity: sha512-+Vx6YKSP+lzCBiTbgQOpR2rIOzL8Ns6UnOBtW4O6tYc7TMTexycaG66RWxXMpAD6QkamwLwX3H5TTYviWtU9RQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-horizontal-rule@3.23.6':
|
||||
resolution: {integrity: sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg==}
|
||||
'@tiptap/extension-horizontal-rule@3.24.0':
|
||||
resolution: {integrity: sha512-DFzWJTrb23x+qssLLs85vEyho8ItUGp3RY9XUsVTIAGZn5IsoUw8wMsvIBlH1ux4Ch7gLchtcD6kpTdMdrL9kw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-image@3.23.6':
|
||||
resolution: {integrity: sha512-vvNGxArvD2dW+XvV0KdYovRVUzCy8QVNulc2r5pV7umnG1E6cCmMkiHiif8J2ePJu2KtysAvJQe0iF+UqueGMw==}
|
||||
'@tiptap/extension-image@3.24.0':
|
||||
resolution: {integrity: sha512-mH+bvsX2cPKuZzV7YMQi4FV2YbDP+Kmq36bY+Bwi/x4mYUc8u0cjQxcu8RzLO7GtsgUJPxGMwfkQxmDqXFLZvw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-italic@3.23.6':
|
||||
resolution: {integrity: sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q==}
|
||||
'@tiptap/extension-italic@3.24.0':
|
||||
resolution: {integrity: sha512-mf3cbNlbMPUNj3IyUkIke+o3ZpOUrtVeY5Yqs5IM/VhkUUh/PdIzqw74VuqEAJ0Z4oZ6nNDHeYLrl3Be1j99lQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-link@3.23.6':
|
||||
resolution: {integrity: sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA==}
|
||||
'@tiptap/extension-link@3.24.0':
|
||||
resolution: {integrity: sha512-MwMoNGG2mL5XGFV1tEGunBRglwsIbW+ZOB2QnKiv+Mcbi2JCWMrorndJZBqpVPR5nM+Bef2KnpchEJmYlQLvKQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-list-item@3.23.6':
|
||||
resolution: {integrity: sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA==}
|
||||
'@tiptap/extension-list-item@3.24.0':
|
||||
resolution: {integrity: sha512-zl/U3viJiV9OzkKM37AHIUN1af1TSLrcbHUUoNLkfJ33Nq+NlpaXpCVK0rKRqiLFJf7zk/a5KWG5CrOy9TxjKA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-list-keymap@3.23.6':
|
||||
resolution: {integrity: sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw==}
|
||||
'@tiptap/extension-list-keymap@3.24.0':
|
||||
resolution: {integrity: sha512-69fKcrngYGEKWNn4R5oLwl0YuV3FY4kufEValVcjnihUmqJTE1vx+fwctYoTsOGnIuNGpUIQ7f9YDD/0w34qBw==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-list@3.23.6':
|
||||
resolution: {integrity: sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw==}
|
||||
'@tiptap/extension-list@3.24.0':
|
||||
resolution: {integrity: sha512-GcxDVMMmDGj7OFTBrV7JpVgr5wxlr2vmjwH7U8QxZX7OJI5vrsMYl/U6KRTvUpG8wP+Zmo5jRlLM+BbL+a/W3g==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-ordered-list@3.23.6':
|
||||
resolution: {integrity: sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A==}
|
||||
'@tiptap/extension-ordered-list@3.24.0':
|
||||
resolution: {integrity: sha512-buRa6bmBDw0TztH+rAcusIye14DiLDS+yGheo6GiNCTD7kKJnksXagBdxvip3jhW5sx7gyAKvoBmvGSg1BbsGA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-paragraph@3.23.6':
|
||||
resolution: {integrity: sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg==}
|
||||
'@tiptap/extension-paragraph@3.24.0':
|
||||
resolution: {integrity: sha512-wD06aB6hO7LgcrlhGiw7I64k2tus9kNoICX5R+UecBSB1DVJdzKvXoXL2kPNv4DqYvljHdkIeK/OpuOTQd6MJA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-placeholder@3.23.6':
|
||||
resolution: {integrity: sha512-8I6b2aevF74aLgymKMxbDxSLxWA2y+2dh0zZDeI8sRZ2m6WHHes+Kyuuwkq1HIPcR+ZLpbec74cmf6lcL/yvqQ==}
|
||||
'@tiptap/extension-placeholder@3.24.0':
|
||||
resolution: {integrity: sha512-3jfYYCIuwMADhvZ92vB6c80YiTmgTSFR23JqyLps8qkQtV59Va5CBYpwJtSs1+VrbCVnNxhZBHhVXusZO3uRkA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extensions': 3.23.6
|
||||
'@tiptap/extensions': 3.24.0
|
||||
|
||||
'@tiptap/extension-strike@3.23.6':
|
||||
resolution: {integrity: sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ==}
|
||||
'@tiptap/extension-strike@3.24.0':
|
||||
resolution: {integrity: sha512-sfN1iQs6Fdlorrfe8wipDkTPwu/Egx3s2fkY7TAWusTGFHwlovuRUGFKqCL9dI4N3u6uqUMpEuWmQNgv+aQGjQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-table-cell@3.23.6':
|
||||
resolution: {integrity: sha512-hS9TmmvRlT9/ikT+0ukACS+hmJuii4zQaH47cg3oJkz/Fv7O7tL7GZniKtK6l2OUZGPhY+4SV2RkDB6bD7DXfw==}
|
||||
'@tiptap/extension-table-cell@3.24.0':
|
||||
resolution: {integrity: sha512-jg62iVc1L8tOycSuwZ5i82EGKGuvelCIkpFX7wvHMl9EiPX9fv70enmb4ua9DyLgRtYg/MNrGOdUATRpxWsFRA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-table': 3.23.6
|
||||
'@tiptap/extension-table': 3.24.0
|
||||
|
||||
'@tiptap/extension-table-header@3.23.6':
|
||||
resolution: {integrity: sha512-D6o0a1cJXUU0xWakainBFGPnGHinQkPcdu1YqGd/PoFANY38lnuZt/NW2O/OLfLXu5LXDRfpqF1+dsKww27dUA==}
|
||||
'@tiptap/extension-table-header@3.24.0':
|
||||
resolution: {integrity: sha512-z1TGVoBD5lVX48hpKcrQY5vETB8aCR61Enk5wGl45zNO/sMEMNxYfDKTkMydyOFUWfGqvOPnjDHJqJuT36/19w==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-table': 3.23.6
|
||||
'@tiptap/extension-table': 3.24.0
|
||||
|
||||
'@tiptap/extension-table-row@3.23.6':
|
||||
resolution: {integrity: sha512-OauWVzkyRQg0rKOqM/a3PuKPc1S7YXMb1LRN7Nh8Ytvglvd7GFRTbl1lVqdZRaz4Jzopag4PQnriIZfMPUpxWw==}
|
||||
'@tiptap/extension-table-row@3.24.0':
|
||||
resolution: {integrity: sha512-kASEYMWZF9ZqO6DRGz0LiD11c3LPfzOlFiJQVDrOyLP7xkJpHzIAKH0K+8u4aohOziDLn8nz+8I+3xjRS46oGg==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-table': 3.23.6
|
||||
'@tiptap/extension-table': 3.24.0
|
||||
|
||||
'@tiptap/extension-table@3.23.6':
|
||||
resolution: {integrity: sha512-XbhZXjhsS6AP7ThoZxjAnNs+NiR81YRori25l6E+ORqB7quiPkIXOAi5h4AIpkn/CYIqze6ere11lWsYpDjtaQ==}
|
||||
'@tiptap/extension-table@3.24.0':
|
||||
resolution: {integrity: sha512-lr5elob3uJnB+ltgqPDEeVQmIPRx6JoS0I6z93tOgKsI2mIsaw5ErghteeiCTpExdyax7aWR0fn5pZzLVDQL8A==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-task-item@3.23.6':
|
||||
resolution: {integrity: sha512-PIHW+3KZCtEmtC7z34rfkusbC42HYr7uw1h5xNvrazQwJGWhKo1UmI0Yea5vyJNKPrTLPCMEm/mgMrj8nNAZfA==}
|
||||
'@tiptap/extension-task-item@3.24.0':
|
||||
resolution: {integrity: sha512-2VtX6VOp4SJk5E/VHeSC2Acfbge6h69TUBhF7hFebbiCDyB73xCaPKcuClIyzOlWJgpWz2ZPXwVB+Y3meXw+yw==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-task-list@3.23.6':
|
||||
resolution: {integrity: sha512-XNuYbVV7F0TBpxHqqEFvrxwGbmTq01qJrZA6qwbQopOsw5wOsCy4XQsCkeZ8BGkakORCEWGSMjPyVOtIKq2jGA==}
|
||||
'@tiptap/extension-task-list@3.24.0':
|
||||
resolution: {integrity: sha512-EIbB/WQ7L92WfaY5AF9upH93nIgBHjGt5zwPYoX+ZTpBncebr1AEDF7rc+LHNuSa1g1ToOKbFB88ep02IIXOjA==}
|
||||
peerDependencies:
|
||||
'@tiptap/extension-list': 3.23.6
|
||||
'@tiptap/extension-list': 3.24.0
|
||||
|
||||
'@tiptap/extension-text-align@3.23.6':
|
||||
resolution: {integrity: sha512-pE+gDmvnrSMWHADDnDSzaO7YUi9kOywQkyrqZ9bEPLddZred7ICoJ4NtD2DqLjDSur+HijaJuByResWb78c6FA==}
|
||||
'@tiptap/extension-text-align@3.24.0':
|
||||
resolution: {integrity: sha512-WKFtYXGthtkUc+Cwy2fItSr+9FKwLZjkJVAY1GhkRdcq35qTuVhkb4Q4wR2Rhkb6QRqtlxF1NDuTf2vxiQmfBQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-text-style@3.23.6':
|
||||
resolution: {integrity: sha512-Auasgj469wkQ5ip+Zi2gaRzvqxx9qKG58+1mkT8yPE3QAGnOIg/AaKyQ7pTV77UyL3FHvLnU+KsWCad+qcobww==}
|
||||
'@tiptap/extension-text-style@3.24.0':
|
||||
resolution: {integrity: sha512-1Hy+5tFEAsnoLhZ/eqmza4USvFHwMA8haeAdCGlwTeshBrt+nUKTrEsRHidF60cGsRwlTcuqxSkjT94dULgp5Q==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-text@3.23.6':
|
||||
resolution: {integrity: sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA==}
|
||||
'@tiptap/extension-text@3.24.0':
|
||||
resolution: {integrity: sha512-Im7keLPEihxm3+LyF+drYCoaOY5hlq35lvHAp/el6M8pJ/scts88HrYpdR1Yc4BtpZBIhfHSyWgPaupI4qwdeg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extension-underline@3.23.6':
|
||||
resolution: {integrity: sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw==}
|
||||
'@tiptap/extension-underline@3.24.0':
|
||||
resolution: {integrity: sha512-D4W4X3UMq9dLVIOfPB9+UodQ4eAJ8yDcm8qFWAwq0a15YWH6bnwulCuIdV+U5dEG+yaRxN8haB9GrrID9jmrSA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
|
||||
'@tiptap/extensions@3.23.6':
|
||||
resolution: {integrity: sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig==}
|
||||
'@tiptap/extensions@3.24.0':
|
||||
resolution: {integrity: sha512-z6gRYzy2ucJp07OQ0F2W07NxyhMTxPYH1ia2eGiQkWax1i56oExpjMsDHP8THWlg8Tb7NnbfKpkfh881EsmofA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/pm@3.23.6':
|
||||
resolution: {integrity: sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww==}
|
||||
'@tiptap/pm@3.24.0':
|
||||
resolution: {integrity: sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==}
|
||||
|
||||
'@tiptap/react@3.23.6':
|
||||
resolution: {integrity: sha512-Tw9KZkYqFMk3vaJAEQKqEYIO/iq3cSJe7OUEGBul4k4GaMQeLItLf5EYhUd0GIPXci1WVVPNntKJsHfX25M37w==}
|
||||
'@tiptap/react@3.24.0':
|
||||
resolution: {integrity: sha512-KxnrlQbzOgA02EMsfuGGHtNhfkJQGqVlQttmQctI9DOl/F3gcaRqg+wNTBY1Fof8yDaZ8Z1LL1F0C05W0o3vUw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': 3.23.6
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0
|
||||
'@tiptap/pm': 3.24.0
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
'@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tiptap/starter-kit@3.23.6':
|
||||
resolution: {integrity: sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A==}
|
||||
'@tiptap/starter-kit@3.24.0':
|
||||
resolution: {integrity: sha512-Ef4PCP96vcY2GonXN9J0M8iC6zvxPTmQlL/QZiCwuYqqnH/hNpYIjNSQdTndiDpxRKofa32Sr2HWktgEnL32Bg==}
|
||||
|
||||
'@tree-sitter-grammars/tree-sitter-yaml@0.7.1':
|
||||
resolution: {integrity: sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==}
|
||||
@@ -8313,6 +8310,9 @@ packages:
|
||||
prosemirror-history@1.4.1:
|
||||
resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
|
||||
|
||||
prosemirror-inputrules@1.5.1:
|
||||
resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
|
||||
|
||||
prosemirror-keymap@1.2.2:
|
||||
resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==}
|
||||
|
||||
@@ -10463,21 +10463,10 @@ snapshots:
|
||||
'@fastify/forwarded': 3.0.1
|
||||
ipaddr.js: 2.3.0
|
||||
|
||||
'@floating-ui/core@1.7.3':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.11
|
||||
optional: true
|
||||
|
||||
'@floating-ui/core@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.11
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.11
|
||||
optional: true
|
||||
|
||||
'@floating-ui/dom@1.7.6':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.5
|
||||
@@ -11010,12 +10999,12 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.6
|
||||
|
||||
'@mantine/tiptap@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))(@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
'@mantine/tiptap@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))(@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
'@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@mantine/hooks': 9.2.2(react@19.2.6)
|
||||
'@tiptap/extension-link': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/react': 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@tiptap/extension-link': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/react': 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
@@ -12232,174 +12221,175 @@ snapshots:
|
||||
- bare-buffer
|
||||
- supports-color
|
||||
|
||||
'@tiptap/core@3.23.6(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/core@3.24.0(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-blockquote@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-blockquote@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-bold@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-bold@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-bubble-menu@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
optional: true
|
||||
|
||||
'@tiptap/extension-bullet-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
|
||||
'@tiptap/extension-code-block@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
|
||||
'@tiptap/extension-code@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
|
||||
'@tiptap/extension-color@3.23.6(@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)))':
|
||||
dependencies:
|
||||
'@tiptap/extension-text-style': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
|
||||
'@tiptap/extension-document@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
|
||||
'@tiptap/extension-dropcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
dependencies:
|
||||
'@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
|
||||
'@tiptap/extension-floating-menu@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extension-bubble-menu@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
optional: true
|
||||
|
||||
'@tiptap/extension-gapcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-bullet-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-hard-break@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-code-block@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-heading@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-code@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-highlight@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-color@3.24.0(@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-text-style': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
|
||||
'@tiptap/extension-horizontal-rule@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extension-document@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-image@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-dropcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-italic@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-floating-menu@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
optional: true
|
||||
|
||||
'@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extension-gapcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-hard-break@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-heading@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-highlight@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-horizontal-rule@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-image@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-italic@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
linkifyjs: 4.3.3
|
||||
|
||||
'@tiptap/extension-list-item@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-list-item@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-list-keymap@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-list-keymap@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-ordered-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-ordered-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-paragraph@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-paragraph@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-placeholder@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-placeholder@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-strike@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-strike@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-table-cell@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-table-cell@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-table-header@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-table-header@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-table-row@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-table-row@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/extension-task-item@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-task-item@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-task-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-task-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-text-align@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-text-align@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-text@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-text@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extension-underline@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
|
||||
'@tiptap/extension-underline@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
|
||||
'@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
|
||||
'@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tiptap/pm@3.23.6':
|
||||
'@tiptap/pm@3.24.0':
|
||||
dependencies:
|
||||
prosemirror-changeset: 2.3.0
|
||||
prosemirror-commands: 1.6.2
|
||||
prosemirror-dropcursor: 1.8.1
|
||||
prosemirror-gapcursor: 1.3.2
|
||||
prosemirror-history: 1.4.1
|
||||
prosemirror-inputrules: 1.5.1
|
||||
prosemirror-keymap: 1.2.2
|
||||
prosemirror-model: 1.24.1
|
||||
prosemirror-schema-list: 1.5.1
|
||||
@@ -12408,10 +12398,10 @@ snapshots:
|
||||
prosemirror-transform: 1.10.2
|
||||
prosemirror-view: 1.41.4
|
||||
|
||||
'@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
'@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
'@types/react': 19.2.15
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.15)
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
@@ -12420,37 +12410,37 @@ snapshots:
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
use-sync-external-store: 1.6.0(react@19.2.6)
|
||||
optionalDependencies:
|
||||
'@tiptap/extension-bubble-menu': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-floating-menu': 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-bubble-menu': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-floating-menu': 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
transitivePeerDependencies:
|
||||
- '@floating-ui/dom'
|
||||
|
||||
'@tiptap/starter-kit@3.23.6':
|
||||
'@tiptap/starter-kit@3.24.0':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-blockquote': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-bold': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-bullet-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-code': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-code-block': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-document': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-dropcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-gapcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-hard-break': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-heading': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-horizontal-rule': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-italic': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-link': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/extension-list-item': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-list-keymap': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-ordered-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-paragraph': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-strike': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-text': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extension-underline': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
|
||||
'@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
|
||||
'@tiptap/pm': 3.23.6
|
||||
'@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-blockquote': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-bold': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-bullet-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-code': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-code-block': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-document': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-dropcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-gapcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-hard-break': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-heading': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-horizontal-rule': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-italic': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-link': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/extension-list-item': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-list-keymap': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-ordered-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-paragraph': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-strike': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-text': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extension-underline': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
|
||||
'@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
|
||||
'@tiptap/pm': 3.24.0
|
||||
|
||||
'@tree-sitter-grammars/tree-sitter-yaml@0.7.1(tree-sitter@0.22.4)':
|
||||
dependencies:
|
||||
@@ -15748,6 +15738,11 @@ snapshots:
|
||||
prosemirror-view: 1.41.4
|
||||
rope-sequence: 1.3.4
|
||||
|
||||
prosemirror-inputrules@1.5.1:
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.3
|
||||
prosemirror-transform: 1.10.2
|
||||
|
||||
prosemirror-keymap@1.2.2:
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.3
|
||||
|
||||
+16
-16
@@ -49,22 +49,22 @@ catalog:
|
||||
"@testcontainers/mysql": ^12.0.1
|
||||
"@testcontainers/postgresql": ^12.0.1
|
||||
"@testcontainers/redis": ^12.0.1
|
||||
"@tiptap/extension-color": 3.23.6
|
||||
"@tiptap/extension-highlight": 3.23.6
|
||||
"@tiptap/extension-image": 3.23.6
|
||||
"@tiptap/extension-link": ^3.23.6
|
||||
"@tiptap/extension-placeholder": ^3.23.6
|
||||
"@tiptap/extension-table": 3.23.6
|
||||
"@tiptap/extension-table-cell": 3.23.6
|
||||
"@tiptap/extension-table-header": 3.23.6
|
||||
"@tiptap/extension-table-row": 3.23.6
|
||||
"@tiptap/extension-task-item": 3.23.6
|
||||
"@tiptap/extension-task-list": 3.23.6
|
||||
"@tiptap/extension-text-align": 3.23.6
|
||||
"@tiptap/extension-text-style": 3.23.6
|
||||
"@tiptap/extension-underline": 3.23.6
|
||||
"@tiptap/react": ^3.23.6
|
||||
"@tiptap/starter-kit": ^3.23.6
|
||||
"@tiptap/extension-color": 3.24.0
|
||||
"@tiptap/extension-highlight": 3.24.0
|
||||
"@tiptap/extension-image": 3.24.0
|
||||
"@tiptap/extension-link": ^3.24.0
|
||||
"@tiptap/extension-placeholder": ^3.24.0
|
||||
"@tiptap/extension-table": 3.24.0
|
||||
"@tiptap/extension-table-cell": 3.24.0
|
||||
"@tiptap/extension-table-header": 3.24.0
|
||||
"@tiptap/extension-table-row": 3.24.0
|
||||
"@tiptap/extension-task-item": 3.24.0
|
||||
"@tiptap/extension-task-list": 3.24.0
|
||||
"@tiptap/extension-text-align": 3.24.0
|
||||
"@tiptap/extension-text-style": 3.24.0
|
||||
"@tiptap/extension-underline": 3.24.0
|
||||
"@tiptap/react": ^3.24.0
|
||||
"@tiptap/starter-kit": ^3.24.0
|
||||
"@trpc/client": ^11.17.0
|
||||
"@trpc/next": ^11.17.0
|
||||
"@trpc/react-query": ^11.17.0
|
||||
|
||||
Reference in new issue
Block a user