From 19d9f33454caf2d264f2783e6fbddff39987b95f Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 18 Nov 2024 20:26:03 +0100 Subject: [PATCH] feat(dashboard): ignore messages page, remove ignore channel page --- .../Api/GuildsController.ChannelsRoles.cs | 118 +++++-- .../Api/GuildsController.KeyRoles.cs | 61 ++++ Catalogger.Backend/Api/GuildsController.cs | 8 +- Catalogger.Frontend/package.json | 4 +- Catalogger.Frontend/src/lib/api.ts | 12 +- .../lib/components/RemovableListItem.svelte | 12 + .../src/lib/components/RoleListItem.svelte | 18 + .../src/routes/dash/[guildId]/+layout.svelte | 22 +- .../src/routes/dash/[guildId]/+page.svelte | 3 +- .../routes/dash/[guildId]/RoleSelect.svelte | 18 + .../[guildId]/ignored-channels/+page.svelte | 114 ------- .../[guildId]/ignored-messages/+page.svelte | 320 ++++++++++++++++++ .../+page.ts | 0 .../dash/[guildId]/ignored-users/+page.svelte | 119 ------- .../routes/dash/[guildId]/import/+page.svelte | 8 +- .../dash/[guildId]/key-roles/+page.svelte | 14 +- .../dash/[guildId]/redirects/+page.svelte | 25 +- Catalogger.Frontend/yarn.lock | 14 +- 18 files changed, 575 insertions(+), 315 deletions(-) create mode 100644 Catalogger.Backend/Api/GuildsController.KeyRoles.cs create mode 100644 Catalogger.Frontend/src/lib/components/RemovableListItem.svelte create mode 100644 Catalogger.Frontend/src/lib/components/RoleListItem.svelte create mode 100644 Catalogger.Frontend/src/routes/dash/[guildId]/RoleSelect.svelte delete mode 100644 Catalogger.Frontend/src/routes/dash/[guildId]/ignored-channels/+page.svelte create mode 100644 Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.svelte rename Catalogger.Frontend/src/routes/dash/[guildId]/{ignored-users => ignored-messages}/+page.ts (100%) delete mode 100644 Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.svelte diff --git a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs b/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs index 7fc43ac..b3a6928 100644 --- a/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs +++ b/Catalogger.Backend/Api/GuildsController.ChannelsRoles.cs @@ -13,18 +13,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using System.Net; -using Catalogger.Backend.Api.Middleware; using Microsoft.AspNetCore.Mvc; -using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; namespace Catalogger.Backend.Api; public partial class GuildsController { - [HttpPut("ignored-channels/{channelId}")] - public async Task AddIgnoredChannelAsync(string id, ulong channelId) + [HttpPut("ignored-messages/channels/{channelId}")] + public async Task AddIgnoredMessageChannelAsync(string id, ulong channelId) { var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); @@ -53,8 +50,8 @@ public partial class GuildsController return NoContent(); } - [HttpDelete("ignored-channels/{channelId}")] - public async Task RemoveIgnoredChannelAsync(string id, ulong channelId) + [HttpDelete("ignored-messages/channels/{channelId}")] + public async Task RemoveIgnoredMessageChannelAsync(string id, ulong channelId) { var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); @@ -65,40 +62,109 @@ public partial class GuildsController return NoContent(); } - [HttpPut("key-roles/{roleId}")] - public async Task AddKeyRoleAsync(string id, ulong roleId) + [HttpPut("ignored-messages/roles/{roleId}")] + public async Task AddIgnoredMessageRoleAsync(string id, ulong roleId) { var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); + if (guildConfig.Messages.IgnoredRoles.Contains(roleId)) + return NoContent(); + if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId)) - throw new ApiError(HttpStatusCode.BadRequest, ErrorCode.BadRequest, "Role not found"); + return NoContent(); - if (guildConfig.KeyRoles.Contains(roleId)) - throw new ApiError( - HttpStatusCode.BadRequest, - ErrorCode.BadRequest, - "Role is already a key role" - ); + guildConfig.Messages.IgnoredRoles.Add(roleId); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); - await guildRepository.AddKeyRoleAsync(guildId, DiscordSnowflake.New(roleId)); return NoContent(); } - [HttpDelete("key-roles/{roleId}")] - public async Task RemoveKeyRoleAsync(string id, ulong roleId) + [HttpDelete("ignored-messages/roles/{roleId}")] + public async Task RemoveIgnoredMessageRoleAsync(string id, ulong roleId) { var (guildId, _) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); - if (!guildConfig.KeyRoles.Contains(roleId)) - throw new ApiError( - HttpStatusCode.BadRequest, - ErrorCode.BadRequest, - "Role is already not a key role" - ); + guildConfig.Messages.IgnoredRoles.Remove(roleId); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return NoContent(); + } + + [HttpPut("ignored-entities/channels/{channelId}")] + public async Task AddIgnoredEntityChannelAsync(string id, ulong channelId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.IgnoredChannels.Contains(channelId)) + return NoContent(); + + var channel = channelCache + .GuildChannels(guildId) + .FirstOrDefault(c => + c.ID.Value == channelId + && c.Type + is ChannelType.GuildText + or ChannelType.GuildCategory + or ChannelType.GuildAnnouncement + or ChannelType.GuildForum + or ChannelType.GuildMedia + or ChannelType.GuildVoice + ); + if (channel == null) + return NoContent(); + + guildConfig.IgnoredChannels = [.. guildConfig.IgnoredChannels, channelId]; + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return NoContent(); + } + + [HttpDelete("ignored-entities/channels/{channelId}")] + public async Task RemoveIgnoredEntityChannelAsync(string id, ulong channelId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + var channels = guildConfig.IgnoredChannels.ToList(); + channels.Remove(channelId); + guildConfig.IgnoredChannels = channels.ToArray(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return NoContent(); + } + + [HttpPut("ignored-entities/roles/{roleId}")] + public async Task AddIgnoredEntityRoleAsync(string id, ulong roleId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (guildConfig.Messages.IgnoredRoles.Contains(roleId)) + return NoContent(); + + if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId)) + return NoContent(); + + guildConfig.IgnoredRoles = [.. guildConfig.IgnoredRoles, roleId]; + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); + + return NoContent(); + } + + [HttpDelete("ignored-entities/roles/{roleId}")] + public async Task RemoveIgnoredEntityRoleAsync(string id, ulong roleId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + var roles = guildConfig.IgnoredRoles.ToList(); + roles.Remove(roleId); + guildConfig.IgnoredRoles = roles.ToArray(); + await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig); - await guildRepository.RemoveKeyRoleAsync(guildId, DiscordSnowflake.New(roleId)); return NoContent(); } } diff --git a/Catalogger.Backend/Api/GuildsController.KeyRoles.cs b/Catalogger.Backend/Api/GuildsController.KeyRoles.cs new file mode 100644 index 0000000..2580042 --- /dev/null +++ b/Catalogger.Backend/Api/GuildsController.KeyRoles.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Net; +using Catalogger.Backend.Api.Middleware; +using Microsoft.AspNetCore.Mvc; +using Remora.Discord.API; + +namespace Catalogger.Backend.Api; + +public partial class GuildsController +{ + [HttpPut("key-roles/{roleId}")] + public async Task AddKeyRoleAsync(string id, ulong roleId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (roleCache.GuildRoles(guildId).All(r => r.ID.Value != roleId)) + throw new ApiError(HttpStatusCode.BadRequest, ErrorCode.BadRequest, "Role not found"); + + if (guildConfig.KeyRoles.Contains(roleId)) + throw new ApiError( + HttpStatusCode.BadRequest, + ErrorCode.BadRequest, + "Role is already a key role" + ); + + await guildRepository.AddKeyRoleAsync(guildId, DiscordSnowflake.New(roleId)); + return NoContent(); + } + + [HttpDelete("key-roles/{roleId}")] + public async Task RemoveKeyRoleAsync(string id, ulong roleId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await guildRepository.GetAsync(guildId); + + if (!guildConfig.KeyRoles.Contains(roleId)) + throw new ApiError( + HttpStatusCode.BadRequest, + ErrorCode.BadRequest, + "Role is already not a key role" + ); + + await guildRepository.RemoveKeyRoleAsync(guildId, DiscordSnowflake.New(roleId)); + return NoContent(); + } +} diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs index dfd6479..5eb29f3 100644 --- a/Catalogger.Backend/Api/GuildsController.cs +++ b/Catalogger.Backend/Api/GuildsController.cs @@ -111,6 +111,9 @@ public partial class GuildsController( categories, channelsWithoutCategories, roles, + guildConfig.IgnoredChannels, + guildConfig.IgnoredRoles, + guildConfig.Messages, guildConfig.Channels, guildConfig.KeyRoles ) @@ -137,7 +140,10 @@ public partial class GuildsController( IEnumerable Categories, IEnumerable ChannelsWithoutCategory, IEnumerable Roles, - Database.Models.Guild.ChannelConfig Config, + ulong[] IgnoredChannels, + ulong[] IgnoredRoles, + Database.Models.Guild.MessageConfig Messages, + Database.Models.Guild.ChannelConfig Channels, ulong[] KeyRoles ); diff --git a/Catalogger.Frontend/package.json b/Catalogger.Frontend/package.json index bfb736a..d1142b3 100644 --- a/Catalogger.Frontend/package.json +++ b/Catalogger.Frontend/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@sveltejs/adapter-static": "^3.0.5", "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "3", + "@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltestrap/sveltestrap": "^6.2.7", "@types/eslint": "^9.6.0", "@types/file-saver": "^2.0.7", @@ -32,7 +32,7 @@ "sass": "^1.80.1", "snarkdown": "^2.0.0", "svelecte": "^4.3.1", - "svelte": "4", + "svelte": "^4.2.7", "svelte-check": "^4.0.0", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", diff --git a/Catalogger.Frontend/src/lib/api.ts b/Catalogger.Frontend/src/lib/api.ts index a52aa7c..05c6d67 100644 --- a/Catalogger.Frontend/src/lib/api.ts +++ b/Catalogger.Frontend/src/lib/api.ts @@ -78,7 +78,10 @@ export type FullGuild = { categories: GuildCategory[]; channels_without_category: GuildChannel[]; roles: GuildRole[]; - config: GuildConfig; + ignored_channels: string[]; + ignored_roles: string[]; + messages: MessageConfig; + channels: ChannelConfig; key_roles: string[]; }; @@ -114,14 +117,15 @@ export type ApiError = { message: string; }; -export type GuildConfig = GuildChannelConfig & { +export type MessageConfig = { ignored_channels: string[]; ignored_users: string[]; + ignored_roles: string[]; ignored_users_per_channel: Record; - redirects: Record; }; -export type GuildChannelConfig = { +export type ChannelConfig = { + redirects: Record; guild_update: string; guild_emojis_update: string; guild_role_create: string; diff --git a/Catalogger.Frontend/src/lib/components/RemovableListItem.svelte b/Catalogger.Frontend/src/lib/components/RemovableListItem.svelte new file mode 100644 index 0000000..3c63c68 --- /dev/null +++ b/Catalogger.Frontend/src/lib/components/RemovableListItem.svelte @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Catalogger.Frontend/src/lib/components/RoleListItem.svelte b/Catalogger.Frontend/src/lib/components/RoleListItem.svelte new file mode 100644 index 0000000..eed5c13 --- /dev/null +++ b/Catalogger.Frontend/src/lib/components/RoleListItem.svelte @@ -0,0 +1,18 @@ + + + + {#if typeof role === "string"} + (unknown role {role}) + {:else} + + {role.name} + {/if} + diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.svelte index 6ec80b5..c3ddeaf 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.svelte +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.svelte @@ -2,7 +2,7 @@ import { Button, Nav, NavItem, NavLink } from "@sveltestrap/sveltestrap"; import type { LayoutData } from "./$types"; import { page } from "$app/stores"; - import apiFetch, { type ApiError, type GuildConfig } from "$lib/api"; + import apiFetch, { type ApiError, type ChannelConfig } from "$lib/api"; import { addToast } from "$lib/toast"; export let data: LayoutData; @@ -10,16 +10,16 @@ // This only saves log channels. All other pages are lists and are saved immediately upon adding/removing an entry. const save = async () => { try { - const resp = await apiFetch( + const resp = await apiFetch( "PATCH", `/api/guilds/${data.guild.id}`, - data.guild.config, + data.guild.channels, ); - data.guild.config = resp; + data.guild.channels = resp; addToast({ header: "Saved log channels.", - body: "Successfully edited log channels and ignored channels.", + body: "Successfully edited log channels.", }); } catch (e) { addToast({ @@ -51,16 +51,10 @@ Redirects - Ignored channels - - - Ignored users + Ignored messages import { Label } from "@sveltestrap/sveltestrap"; - import type { GuildChannelConfig } from "$lib/api"; import type { PageData } from "./$types"; import ChannelSelect from "./ChannelSelect.svelte"; export let data: PageData; - $: channels = data.guild.config as GuildChannelConfig; + $: channels = data.guild.channels;

Log channels

diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/RoleSelect.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/RoleSelect.svelte new file mode 100644 index 0000000..3dfc5c9 --- /dev/null +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/RoleSelect.svelte @@ -0,0 +1,18 @@ + + + diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-channels/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-channels/+page.svelte deleted file mode 100644 index 7764287..0000000 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-channels/+page.svelte +++ /dev/null @@ -1,114 +0,0 @@ - - -

Ignored channels

- -

- Messages from ignored channels will not be logged. Changes to ignored channels - will also not be logged, but note that ignored channels being deleted - (or new channels being created in an ignored category) will still be logged. -

- -
- - -
- -
- -
- -
-

Currently ignored channels

-
- - - {#each ignored as id} - - {channelName(id)} - - - {/each} - diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.svelte new file mode 100644 index 0000000..8704e8a --- /dev/null +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.svelte @@ -0,0 +1,320 @@ + + +

Ignored messages

+ +

Here you can select which channels and roles to ignore messages from.

+ +

How it works

+ +

Messages will be ignored if:

+ +
    +
  • They are posted in a channel that is listed here
  • +
  • + They are posted in a channel in a category that is listed here +
  • +
  • They are posted in a thread that's in a channel that is listed here
  • +
  • + They are posted by a person with at least one role that is listed here +
  • +
  • They are posted by a user that is listed here
  • +
+ +

+ Additionally, messages from ignored users are also ignored. +

+ +

Channels

+ +
+ +
+ +
+ +
+ + + {#each ignoredChannels as id} + removeChannelIgnore(id)} + buttonText="Stop ignoring" + > + {channelName(id)} + + {/each} + + +

Roles

+ +
+ +
+ +
+ +
+ + + {#each ignoredRoles as id} + removeRoleIgnore(id)} + buttonText="Stop ignoring" + /> + {/each} + + +

Users

+ +
+ + {#if userToIgnore && !userIdRegex.test(userToIgnore)} +

+ If you're not ignoring a member of your server, you need to give a + user ID, not their username. +

+ {/if} +
+ +
+ +
+ + + {#each data.users as user (user.id)} + removeUserIgnore(user.id)} + buttonText="Stop ignoring" + > + {user.tag} (ID: {user.id}) + + {/each} + diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.ts b/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.ts similarity index 100% rename from Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.ts rename to Catalogger.Frontend/src/routes/dash/[guildId]/ignored-messages/+page.ts diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.svelte deleted file mode 100644 index 544d73b..0000000 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/ignored-users/+page.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - -

Ignored users

- -

Messages from ignored users will not be logged.

- -
- - - {#if toIgnore && !idRegex.test(toIgnore)} -

- If you're not ignoring a member of your server, you need to give a - user ID, not their username. -

- {/if} -
- -
- -
- -

Currently ignored users

- - - {#each data.users as user (user.id)} - - {user.tag} (ID: {user.id}) - - - {/each} - diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/import/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/import/+page.svelte index 7a8fccb..4c32d95 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/import/+page.svelte +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/import/+page.svelte @@ -1,5 +1,5 @@

Key roles

@@ -95,14 +93,8 @@

Current key roles

- {#each keyRoles as r (r.id)} - - - - {r.name} - - - + {#each keyRoles as role (role.id)} + removeRole(role.id)} /> {/each} diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/redirects/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/redirects/+page.svelte index bfff7cd..8e77ec5 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/redirects/+page.svelte +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/redirects/+page.svelte @@ -11,10 +11,11 @@ import { fastFetch } from "$lib/api"; import { addToast } from "$lib/toast"; import { makeFullOptions } from "$lib/util"; + import RemovableListItem from "$lib/components/RemovableListItem.svelte"; export let data: PageData; - $: redirects = data.guild.config.redirects; + $: redirects = data.guild.channels.redirects; $: allChannels = [ ...data.guild.channels_without_category.map((c) => ({ @@ -51,7 +52,7 @@ target, }); - data.guild.config.redirects[source] = target; + data.guild.channels.redirects[source] = target; addToast({ body: "Successfully added redirect." }); } catch (e) { @@ -67,8 +68,8 @@ try { await fastFetch("DELETE", `/api/guilds/${data.guild.id}/redirects/${id}`); - delete data.guild.config.redirects[id]; - data.guild.config.redirects = data.guild.config.redirects; + delete data.guild.channels.redirects[id]; + data.guild.channels.redirects = data.guild.channels.redirects; addToast({ body: "Successfully removed redirect." }); } catch (e) { @@ -108,16 +109,12 @@ {#each Object.keys(redirects) as redirectSource} - - {channelName(redirectSource)} ➜ {channelName( - redirects[redirectSource], - )} - - + removeRedirect(redirectSource)} + buttonText="Remove redirect" + > + {channelName(redirectSource)} ➜ {channelName(redirects[redirectSource])} + {/each} {:else} diff --git a/Catalogger.Frontend/yarn.lock b/Catalogger.Frontend/yarn.lock index 4ede98d..ded308d 100644 --- a/Catalogger.Frontend/yarn.lock +++ b/Catalogger.Frontend/yarn.lock @@ -696,8 +696,8 @@ __metadata: linkType: hard "@sveltejs/kit@npm:^2.0.0": - version: 2.8.0 - resolution: "@sveltejs/kit@npm:2.8.0" + version: 2.8.1 + resolution: "@sveltejs/kit@npm:2.8.1" dependencies: "@types/cookie": "npm:^0.6.0" cookie: "npm:^0.6.0" @@ -717,7 +717,7 @@ __metadata: vite: ^5.0.3 bin: svelte-kit: svelte-kit.js - checksum: 10c0/f4fdad81bf5f8f645eed21ffd7015b14c76481b4df054f7dc16ab6bb6a32dc0c9fd28790a7da3e3e3a0b9860e6405cbd082df8cc689694f09bef30659c47cc23 + checksum: 10c0/531cf8fdff0e039f51ad3e9aa288acea5fd9eb03b3189fda68dc45e7ac12a288f6a1792aace9c9fec3979475b5e487eb3af4b6160ad23c5bea1f503b89da5cd2 languageName: node linkType: hard @@ -734,7 +734,7 @@ __metadata: languageName: node linkType: hard -"@sveltejs/vite-plugin-svelte@npm:3": +"@sveltejs/vite-plugin-svelte@npm:^3.0.0": version: 3.1.2 resolution: "@sveltejs/vite-plugin-svelte@npm:3.1.2" dependencies: @@ -1117,7 +1117,7 @@ __metadata: dependencies: "@sveltejs/adapter-static": "npm:^3.0.5" "@sveltejs/kit": "npm:^2.0.0" - "@sveltejs/vite-plugin-svelte": "npm:3" + "@sveltejs/vite-plugin-svelte": "npm:^3.0.0" "@sveltestrap/sveltestrap": "npm:^6.2.7" "@types/eslint": "npm:^9.6.0" "@types/file-saver": "npm:^2.0.7" @@ -1135,7 +1135,7 @@ __metadata: sass: "npm:^1.80.1" snarkdown: "npm:^2.0.0" svelecte: "npm:^4.3.1" - svelte: "npm:4" + svelte: "npm:^4.2.7" svelte-check: "npm:^4.0.0" typescript: "npm:^5.0.0" typescript-eslint: "npm:^8.0.0" @@ -3124,7 +3124,7 @@ __metadata: languageName: node linkType: hard -"svelte@npm:4": +"svelte@npm:^4.2.7": version: 4.2.19 resolution: "svelte@npm:4.2.19" dependencies: