diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js index 50d67e3ace1..03bc4d5bdac 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js @@ -88,12 +88,14 @@ describe('Verify Accessibility Support in different sections in Settings and Pro }); it('MM-T1465_1 Verify Label & Tab behavior in section links', () => { - // * Verify aria-label and tab support in section of Account settings modal + // * Verify tab selection and keyboard navigation in Account settings modal cy.uiOpenProfileModal('Profile Settings'); cy.findByRole('tab', {name: 'profile settings'}).should('be.visible').focus().should('be.focused'); ['profile settings', 'security'].forEach((text) => { - // * Verify aria-label on each tab and it supports navigating to the next tab with arrow keys - cy.focused().should('have.attr', 'aria-label', text).type('{downarrow}'); + // * Verify each tab is correctly selected and supports navigating to the next tab with arrow keys + cy.findByRole('tab', {name: text}). + should('have.attr', 'aria-selected', 'true'). + type('{downarrow}'); }); cy.uiClose(); @@ -101,8 +103,10 @@ describe('Verify Accessibility Support in different sections in Settings and Pro cy.uiOpenSettingsModal(); cy.findByRole('tab', {name: 'notifications'}).should('be.visible').focus().should('be.focused'); ['notifications', 'display', 'sidebar', 'advanced'].forEach((text) => { - // * Verify aria-label on each tab and it supports navigating to the next tab with arrow keys - cy.focused().should('have.attr', 'aria-label', text).type('{downarrow}'); + // * Verify each tab is correctly selected and supports navigating to the next tab with arrow keys + cy.findByRole('tab', {name: text}). + should('have.attr', 'aria-selected', 'true'). + type('{downarrow}'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js index cac966dd603..1dec7d5059c 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js @@ -29,30 +29,39 @@ describe('Channel Settings - Channel Header', () => { // # Visit the newly created channel cy.visit(`/${testTeam.name}/channels/${channel.name}`); - // # Click on the channel name in the channel header to open the channel menu options - cy.get(`[aria-label="${channel.display_name.split('-').join(' ').toLowerCase()} channel menu"]`).click(); - // # Select the "Edit Channel Header" option from the dropdown - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - cy.findByText('Edit Channel Header').click(); + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // * Verify modal is open + cy.get('.ChannelSettingsModal').should('be.visible'); + cy.get('#genericModalLabel').should('contain', 'Channel Settings'); // # Type something in the header edit box - cy.get('textarea[placeholder="Edit the Channel Header..."]').clear().type('This is the new header content'); + cy.get('#channel_settings_header_textbox').clear().type('This is the new header content'); - // * Verify the "Preview" button exists - cy.findByText('Preview').should('be.visible'); + // * Verify the "Preview" button exists and is not active + cy.get('#channel_settings_header_textbox'). + parents('.AdvancedTextbox'). + find('#PreviewInputTextButton'). + should('not.have.class', 'active'); // * Verify that before hitting the preview button, the style on the textbox is `display: block` - cy.get('textarea[placeholder="Edit the Channel Header..."]').should('have.css', 'display', 'block'); + cy.get('#channel_settings_header_textbox').should('have.css', 'display', 'block'); // # Click the "Preview" button - cy.findByText('Preview').click(); - - // * Verify the "Preview" button label has changed to "Edit" - cy.findByText('Edit').should('be.visible'); + cy.get('#channel_settings_header_textbox'). + parents('.AdvancedTextbox'). + find('#PreviewInputTextButton').click(); // * Verify that the display is now none on the textbox element - cy.get('textarea[placeholder="Edit the Channel Header..."]').should('have.css', 'display', 'none'); + cy.get('#channel_settings_header_textbox').should('have.css', 'display', 'none'); + + // * Verify the "Preview" button has class active + cy.get('#channel_settings_header_textbox'). + parents('.AdvancedTextbox'). + find('#PreviewInputTextButton'). + should('have.class', 'active'); }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_modal_spec.ts new file mode 100644 index 00000000000..1c8340c4a83 --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_modal_spec.ts @@ -0,0 +1,361 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// *************************************************************** +// - [#] indicates a test step (e.g. # Go to a page) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element ID when selecting an element. Create one if none. +// *************************************************************** + +// Stage: @prod +// Group: @channels @channel @channel_settings + +import {Team} from '@mattermost/types/teams'; +import {Channel} from '@mattermost/types/channels'; +import {UserProfile} from '@mattermost/types/users'; + +describe('Channel Settings Modal', () => { + let testTeam: Team; + let testUser: UserProfile; + let testChannel: Channel; + let originalTestChannel: Channel; + + before(() => { + // Setup test data + cy.apiInitSetup().then(({team, user}) => { + testTeam = team; + testUser = user; + + // Create a test channel + cy.apiCreateChannel(testTeam.id, 'test-channel', 'Test Channel').then(({channel}) => { + testChannel = channel; + originalTestChannel = {...channel}; + cy.apiAddUserToChannel(channel.id, user.id); + }); + + cy.apiLogin(testUser); + }); + }); + + beforeEach(() => { + // Visit the channel before each test + cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); + }); + + // 2DO: update tests ids once QA has defined the test plan and zephyr tests + it('MM-T1: Can open and close the channel settings modal', () => { + // # Open channel settings modal from channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // * Verify modal is open + cy.get('.ChannelSettingsModal').should('be.visible'); + cy.get('#genericModalLabel').should('contain', 'Channel Settings'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + + // * Verify modal is closed + cy.get('.ChannelSettingsModal').should('not.exist'); + }); + + it('MM-T2: Can navigate between tabs', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // * Verify Info tab is active by default + cy.get('#infoButton').should('have.class', 'active'); + cy.get('.ChannelSettingsModal__infoTab').should('be.visible'); + + // # Click on Archive tab + cy.get('#archiveButton').click(); + + // * Verify Archive tab is active + cy.get('#archiveButton').should('have.class', 'active'); + cy.get('.ChannelSettingsModal__archiveTab').should('be.visible'); + + // # Click back on Info tab + cy.get('#infoButton').click(); + + // * Verify Info tab is active again + cy.get('#infoButton').should('have.class', 'active'); + cy.get('.ChannelSettingsModal__infoTab').should('be.visible'); + }); + + it('MM-T3: Can edit channel name and URL', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Get original channel name, url and alias it + cy.get('#input_channel-settings-name').invoke('val').as('originalName'); + cy.get('.url-input-label').invoke('val').as('originalUrl'); + + // # Edit channel name + cy.get('#input_channel-settings-name').clear().type('Updated Channel Name'); + + // * Verify URL is updated automatically + cy.get('.url-input-label').should('contain', 'updated-channel-name'); + + // # Click Save + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify changes are saved + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + + // * Verify channel header shows updated name + cy.get('#channelHeaderTitle').should('contain', 'Updated Channel Name'); + + // # Open channel settings modal to restore original values + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // * Verify the channel name is reset to the original value + cy.get('#input_channel-settings-name').clear().type(originalTestChannel.display_name); + + // # Try to change URL to match the original url + cy.get('.url-input-button').click(); + cy.get('.url-input-container input').clear().type(originalTestChannel.name); + cy.get('.url-input-container button.url-input-button').click(); + + // # Save changes + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify changes are saved + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + + // * Verify channel header shows updated name + cy.get('#channelHeaderTitle').should('contain', originalTestChannel.display_name); + }); + + it('MM-T4: Shows error for invalid channel name', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Set empty channel name + cy.get('#input_channel-settings-name').clear(); + + // # Try Save changes + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify error is shown + cy.get('.Input_fieldset').should('have.class', 'Input_fieldset___error'); + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + }); + + it('MM-T6: Can edit channel purpose and header', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Edit channel purpose + cy.get('#channel_settings_purpose_textbox').clear().type('This is a test purpose'); + + // # Edit channel header + cy.get('#channel_settings_header_textbox').clear().type('This is a test header'); + + // # Save changes + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify changes are saved + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + + // * Verify channel header shows updated header + cy.get('#channelHeaderDescription').should('contain', 'This is a test header'); + }); + + it('MM-T7: Shows error when purpose exceeds character limit', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Enter purpose that exceeds character limit (250 characters) + const longPurpose = 'a'.repeat(260); + cy.get('#channel_settings_purpose_textbox').clear().type(longPurpose); + + // * Verify error is shown + cy.findAllByTestId('channel_settings_purpose_textbox').should('have.class', 'textarea--has-errors'); + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + }); + + it('MM-T8: Shows error when header exceeds character limit', () => { + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Enter header that exceeds character limit (1024 characters) + const longHeader = 'a'.repeat(1050); + cy.findByTestId('channel_settings_header_textbox').clear().type(longHeader); + + // * Verify error is shown + cy.findByTestId('channel_settings_header_textbox').should('have.class', 'textarea--has-errors'); + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + }); + + it('MM-T9: Can archive a channel and redirect to previous visited channel', () => { + // # Create a new channel for this test + cy.apiCreateChannel(testTeam.id, 'first-channel', 'First Channel').then(({channel: channel1}) => { + cy.apiCreateChannel(testTeam.id, 'second-channel', 'Second Channel').then(({channel}) => { + // # visit town square + cy.visit(`/${testTeam.name}/channels/${channel1.name}`); + + // # visit just created channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Click on Archive tab + cy.get('#archiveButton').click(); + + // # Click Archive button + cy.get('#channelSettingsArchiveChannelButton').click(); + + // * Verify confirmation modal appears + cy.get('#archiveChannelConfirmModal').should('be.visible'); + + // # Confirm archive + cy.findByRole('button', {name: 'Confirm'}).click(); + + // * Verify redirect to Town Square + cy.url().should('include', channel1.name); + + // * Verify channel is no longer in sidebar + cy.get('.SidebarChannel').contains('Archive Test').should('not.exist'); + }); + }); + }); + + it('MM-T10: Warns when switching tabs with unsaved changes', () => { + // # Create a new channel for this test + cy.apiCreateChannel(testTeam.id, 'unsaved-test', 'Unsaved Test').then(({channel}) => { + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Make a change to the channel name + cy.get('#input_channel-settings-name').clear().type('Changed Name'); + + // # Try to switch to Archive tab + cy.get('#archiveButton').click(); + + // * Verify we're still on the Info tab due to unsaved changes + cy.get('#infoButton').should('have.class', 'active'); + + // * Verify save changes panel shows error + cy.get('.SaveChangesPanel').should('have.class', 'error'); + }); + }); + + // MM-T11: Can reset changes without saving + it('MM-T11: Can reset changes without saving', () => { + // # Create a new channel for this test + cy.apiCreateChannel(testTeam.id, 'reset-test', 'Reset Test').then(({channel}) => { + // # Visit the channel page using the channel name returned from the API + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Get original channel name and alias it + cy.get('#input_channel-settings-name').invoke('val').as('originalName'); + + // # Change the channel name + cy.get('#input_channel-settings-name').clear().type('Temporary Name'); + + // # Click the Reset button in the SaveChangesPanel + cy.get('.SaveChangesPanel button').contains('Reset').click(); + + // * Verify the channel name is reset to the original value + cy.get('@originalName').then((originalName) => { + cy.get('#input_channel-settings-name').should('have.value', originalName); + }); + + // * Verify the SaveChangesPanel is no longer visible + cy.get('.SaveChangesPanel').should('not.exist'); + }); + }); + + // MM-T12: Can preview purpose and header with markdown + it('MM-T12: Can preview purpose and header with markdown', () => { + // # Create a new channel for this test + cy.apiCreateChannel(testTeam.id, 'markdown-test', 'Markdown Test').then(({channel}) => { + // # Visit the channel page using the channel name returned from the API + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Add markdown to purpose + cy.get('#channel_settings_purpose_textbox').clear().type('This is **bold** and _italic_ text'); + + // # Click preview button for purpose (assumes preview button is inside the AdvancedTextbox container) + cy.get('#channel_settings_purpose_textbox'). + parents('.AdvancedTextbox'). + find('#PreviewInputTextButton'). + click(); + + // * Verify that markdown is rendered in preview: check for bold and italic formatting + cy.get('.textbox-preview-area'). + should('contain', 'This is'). + find('strong'). + should('contain', 'bold'); + cy.get('.textbox-preview-area'). + find('em'). + should('contain', 'italic'); + + // # Add markdown to header + cy.get('#channel_settings_header_textbox').clear().type('Visit [Mattermost](https://mattermost.com)'); + + // # Click preview button for header + cy.get('#channel_settings_header_textbox'). + parents('.AdvancedTextbox'). + find('#PreviewInputTextButton'). + click(); + + // * Verify that markdown is rendered in preview: check for a link with text "Mattermost" + cy.get('.textbox-preview-area'). + should('contain', 'Visit'). + find('a'). + should('contain', 'Mattermost'); + }); + }); + + it('MM-T13: Validates URL when editing channel name', () => { + // # Create two channels to test URL conflict error + cy.apiCreateChannel(testTeam.id, 'first-channel', 'First Channel').then(({channel: channel1}) => { + cy.apiCreateChannel(testTeam.id, 'second-channel', 'Second Channel').then(({channel}) => { + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + + // # Try to change URL to match the first channel + cy.get('.url-input-button').click(); + cy.get('.url-input-container input').clear().type(channel1.name); + cy.get('.url-input-container button.url-input-button').click(); + + // * Verify error is shown + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + }); + }); + }); +}); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts index e39f4b3ba15..961595e7fdb 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts @@ -37,22 +37,23 @@ describe('Channel Settings', () => { // # Go to test channel cy.visit(`/${testTeam.name}/channels/${channel.name}`); - // # Go to channel dropdown > Rename channel - cy.get('#channelHeaderTitle').click(); - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - cy.findByText('Rename Channel').click(); + // # Go to channel dropdown > Channel Settings Modal + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); // # Try to enter existing URL and save - cy.get('#channel_name').clear().type('town-square'); - cy.get('#save-button').click(); + cy.get('#input_channel-settings-name').clear().type('town-square'); + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); // # Error is displayed and URL is unchanged - cy.get('.has-error').should('be.visible').and('contain', 'A channel with that name already exists on the same team.'); + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + cy.get('.url-input-error').should('be.visible').and('contain.text', 'A channel with that name already exists on the same team.'); + cy.url().should('include', `/${testTeam.name}/channels/${channel.name}`); // # Enter a new URL and save - cy.get('#channel_name').clear().type('another-town-square'); - cy.get('#save-button').click(); + cy.get('#input_channel-settings-name').clear().type('another-town-square'); + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); // * URL is updated and no errors are displayed cy.url().should('include', `/${testTeam.name}/channels/another-town-square`); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts deleted file mode 100644 index 032d7844485..00000000000 --- a/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Team} from '@mattermost/types/teams'; -import {UserProfile} from '@mattermost/types/users'; - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @channels @channel - -describe('Channels', () => { - let testUser: UserProfile; - let testTeam: Team; - - before(() => { - cy.apiInitSetup().then(({team, user}) => { - testUser = user; - testTeam = team; - }); - }); - - it('MM-T3348 - Convert to private channel should only be shown to users with permission', () => { - // # Reset permissions to default - resetPermissionsToDefault(); - - // # Enable convert public channels to private for all users - enablePermission('all_users-public_channel-convert_public_channel_to_private-checkbox'); - saveConfig(); - - // # Login as a regular user - cy.apiLogin(testUser); - - // # Create new test channel - cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { - // # Go to test channel - cy.visit(`/${testTeam.name}/channels/${channel.name}`); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should be visible and confirm - cy.get('#channelConvertToPrivate').should('be.visible').click(); - cy.findByTestId('convertChannelConfirm').should('be.visible').click(); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should no longer be visible - cy.get('#channelConvertToPrivate').should('not.exist'); - }); - - // # Reset permissions to default - resetPermissionsToDefault(); - - // # Login as a regular user - cy.apiLogin(testUser); - - // # Create new test channel - cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { - // # Go to test channel - cy.visit(`/${testTeam.name}/channels/${channel.name}`); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should no longer be visible - cy.get('#channelConvertToPrivate').should('not.exist'); - }); - - // # Reset permissions to default - resetPermissionsToDefault(); - - // # Remove permission from team admins - removePermission('team_admin-public_channel-convert_public_channel_to_private-checkbox'); - saveConfig(); - - // # Promote user to team admin - cy.apiUpdateTeamMemberSchemeRole(testTeam.id, testUser.id, {scheme_admin: true, scheme_user: true}); - - // # Login as the now team admin user - cy.apiLogin(testUser); - - // # Create new test channel - cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { - // # Go to test channel - cy.visit(`/${testTeam.name}/channels/${channel.name}`); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should not be visible - cy.get('#channelConvertToPrivate').should('not.exist'); - }); - - // # Reset permissions to default - resetPermissionsToDefault(); - - // # Login as the team admin user - cy.apiLogin(testUser); - - // # Create new test channel - cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { - // # Go to test channel - cy.visit(`/${testTeam.name}/channels/${channel.name}`); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should be visible and confirm - cy.get('#channelConvertToPrivate').should('be.visible').click(); - cy.findByTestId('convertChannelConfirm').should('be.visible').click(); - - // # Click the channel header dropdown - cy.get('#channelHeaderTitle').click(); - - // open ChannelSettings submenu - cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover'); - - // * Channel convert to private should no longer be visible - cy.get('#channelConvertToPrivate').should('not.exist'); - }); - }); -}); - -const saveConfig = () => { - cy.get('#saveSetting').click(); - cy.waitUntil(() => cy.get('#saveSetting').then((el) => { - return el[0].innerText === 'Save'; - })); -}; - -const enablePermission = (permissionCheckBoxTestId) => { - cy.findByTestId(permissionCheckBoxTestId).then((el) => { - if (!el.hasClass('checked')) { - el.click(); - } - }); -}; - -const removePermission = (permissionCheckBoxTestId) => { - cy.findByTestId(permissionCheckBoxTestId).then((el) => { - if (el.hasClass('checked')) { - el.click(); - } - }); -}; - -const resetPermissionsToDefault = () => { - // # Login as sysadmin and navigate to system scheme page - cy.apiAdminLogin(); - cy.visit('/admin_console/user_management/permissions/system_scheme'); - - // # Click reset to defaults and confirm - cy.findByTestId('resetPermissionsToDefault').click(); - cy.get('#confirmModalButton').click(); - - // # Save - saveConfig(); -}; diff --git a/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_type_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_type_spec.ts new file mode 100644 index 00000000000..3ebca9d33b1 --- /dev/null +++ b/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_type_spec.ts @@ -0,0 +1,567 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; +import {Channel} from '@mattermost/types/channels'; +import {getAdminAccount} from '../../../support/env'; + +// *************************************************************** +// - [#] indicates a test step (e.g. # Go to a page) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element ID when selecting an element. Create one if none. +// *************************************************************** + +// Stage: @prod +// Group: @channels @channel + +describe('Channel Type Conversion (Public to Private Only)', () => { + let testUser: UserProfile; + let testTeam: Team; + const admin = getAdminAccount(); + + // Helper functions for permission management + const saveConfig = () => { + cy.waitUntil(() => + cy.get('#saveSetting').then((button) => { + // Check if the button text is exactly "Save" + if (button.text().trim() === 'Save') { + button.click(); + return true; // signals waitUntil that it succeeded + } + return false; + }), + ); + }; + + const enablePermission = (permissionCheckBoxTestId: string) => { + cy.findByTestId(permissionCheckBoxTestId).then((el) => { + if (!el.hasClass('checked')) { + el.click(); + } + }); + }; + + const removePermission = (permissionCheckBoxTestId: string) => { + cy.findByTestId(permissionCheckBoxTestId).then((el) => { + if (el.hasClass('checked')) { + el.click(); + } + }); + }; + + const promoteToChannelAdmin = (userId, channelId, admin) => { + cy.externalRequest({ + user: admin, + method: 'put', + path: `channels/${channelId}/members/${userId}/schemeRoles`, + data: { + scheme_user: true, + scheme_admin: true, + }, + }); + }; + + const resetPermissionsToDefault = () => { + // # Login as sysadmin and navigate to system scheme page + cy.apiAdminLogin(); + cy.visit('/admin_console/user_management/permissions/system_scheme'); + + // # Click reset to defaults and confirm + cy.findByTestId('resetPermissionsToDefault').click(); + cy.get('#confirmModalButton').click(); + + // # Save + saveConfig(); + cy.wait(1000); + cy.visit('/admin_console/user_management/permissions/system_scheme'); + }; + + // Helper functions for permission setup + const setupPermissions = (config: { + resetToDefault?: boolean; + publicToPrivate?: boolean; + removeFromTeamAdmin?: boolean; + }) => { + if (config.resetToDefault) { + resetPermissionsToDefault(); + } + + // Public to private conversion permissions + if (config.publicToPrivate) { + enablePermission('all_users-public_channel-convert_public_channel_to_private-checkbox'); + } else if (config.publicToPrivate === false) { + removePermission('all_users-public_channel-convert_public_channel_to_private-checkbox'); + } + + // Remove public to private conversion from team admin if specified + if (config.removeFromTeamAdmin) { + removePermission('team_admin-public_channel-convert_public_channel_to_private-checkbox'); + } + + cy.wait(1000); + + saveConfig(); + }; + + // Helper functions for channel management + const createAndVisitPublicChannel = (teamName: string, channelId: string, displayName: string): Cypress.Chainable => { + return cy.apiCreateChannel(testTeam.id, channelId, displayName).then(({channel}) => { + cy.apiAddUserToChannel(channel.id, testUser.id); + cy.visit(`/${teamName}/channels/${channel.name}`); + return cy.wrap(channel); + }); + }; + + const createAndVisitPrivateChannel = (teamName: string, channelId: string, displayName: string): Cypress.Chainable => { + return cy.apiCreateChannel(testTeam.id, channelId, displayName, 'P').then(({channel}) => { + cy.apiAddUserToChannel(channel.id, testUser.id); + cy.visit(`/${teamName}/channels/${channel.name}`); + return cy.wrap(channel); + }); + }; + + const visitChannel = (teamName: string, channelName: string) => { + cy.visit(`/${teamName}/channels/${channelName}`); + }; + + // Helper functions for UI interaction + const openChannelSettingsModal = () => { + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); + cy.get('.ChannelSettingsModal').should('be.visible'); + }; + + const closeChannelSettingsModal = () => { + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + cy.get('.ChannelSettingsModal').should('not.exist'); + }; + + // Helper functions for UI interaction + const saveChannelSettings = () => { + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + }; + + const verifySettingsSaved = () => { + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + }; + + // Helper functions for channel conversion + const convertChannelToPrivate = () => { + // Select private channel type + cy.get('#public-private-selector-button-P').click(); + cy.get('#public-private-selector-button-P').should('have.class', 'selected'); + + // Save changes - this will trigger the confirmation modal + saveChannelSettings(); + + // Handle the confirmation modal that appears when converting from public to private + cy.get('#confirmModal').should('be.visible'); + cy.get('#confirmModalButton').click(); + + // Verify settings were saved + verifySettingsSaved(); + }; + + // Function kept for potential future use but not used in current tests + // since private to public conversion is no longer allowed + // const convertChannelToPublic = () => { + // cy.get('#public-private-selector-button-O').click(); + // cy.get('#public-private-selector-button-O').should('have.class', 'selected'); + // }; + + // Helper functions for verification + const verifyChannelIsPrivate = (channelName: string) => { + cy.get('.SidebarChannel').contains(channelName).parent().find('.icon-lock-outline').should('exist'); + }; + + // Function kept for potential future use but not used in current tests + // since private to public conversion is no longer allowed + // const verifyChannelIsPublic = (channelName: string) => { + // cy.get('.SidebarChannel').contains(channelName).parent().find('.icon-lock-outline').should('not.exist'); + // }; + + const verifyConversionOptionDisabled = (toPrivate = true) => { + if (toPrivate) { + // Wait for the UI to fully load and stabilize + cy.wait(500); + + // Check if the button is disabled - it might have a disabled attribute or a disabled class + cy.get('#public-private-selector-button-P').then(($el) => { + const isDisabled = $el.hasClass('disabled') || $el.prop('disabled') === true || $el.attr('aria-disabled') === 'true'; + expect(isDisabled).to.be.true; + }); + } else { + // Wait for the UI to fully load and stabilize + cy.wait(500); + + // Check if the button is disabled - it might have a disabled attribute or a disabled class + cy.get('#public-private-selector-button-O').then(($el) => { + const isDisabled = $el.hasClass('disabled') || $el.prop('disabled') === true || $el.attr('aria-disabled') === 'true'; + expect(isDisabled).to.be.true; + }); + } + }; + + const verifyConversionOptionEnabled = (toPrivate = true) => { + if (toPrivate) { + // Wait for the UI to fully load and stabilize + cy.wait(500); + + // Check if the button is enabled - it should not have disabled attributes or classes + cy.get('#public-private-selector-button-P').then(($el) => { + const isDisabled = $el.hasClass('disabled') || $el.prop('disabled') === true || $el.attr('aria-disabled') === 'true'; + expect(isDisabled).to.be.false; + }); + } else { + // Wait for the UI to fully load and stabilize + cy.wait(500); + + // Check if the button is enabled - it should not have disabled attributes or classes + cy.get('#public-private-selector-button-O').then(($el) => { + const isDisabled = $el.hasClass('disabled') || $el.prop('disabled') === true || $el.attr('aria-disabled') === 'true'; + expect(isDisabled).to.be.false; + }); + } + }; + + // Helper function to make user a channel admin + const makeUserChannelAdmin = (channelId: string, userId: string) => { + cy.apiAddUserToChannel(channelId, userId); + promoteToChannelAdmin(userId, channelId, admin); + }; + + // Helper function to make user a team admin + const makeUserTeamAdmin = (teamId: string, userId: string) => { + cy.apiUpdateTeamMemberSchemeRole(teamId, userId, {scheme_admin: true, scheme_user: true}); + }; + + before(() => { + cy.apiInitSetup().then(({team, user}) => { + testUser = user; + testTeam = team; + }); + }); + + beforeEach(() => { + // Reset to a clean state before each test + cy.apiLogin(testUser); + }); + + describe('Basic Conversion Functionality', () => { + it('MM-T3348-1 - Can convert a public channel to private', () => { + // # Setup permissions + setupPermissions({resetToDefault: true, publicToPrivate: true}); + + // # Create and visit a public channel + createAndVisitPublicChannel(testTeam.name, 'public-to-private', 'Public To Private').then((channel) => { + // # Open channel settings modal + openChannelSettingsModal(); + + // # Convert to private + convertChannelToPrivate(); + + // # Close the modal + closeChannelSettingsModal(); + + // * Verify channel is now private + verifyChannelIsPrivate(channel.display_name); + }); + }); + + it('MM-T3348-5 - Cannot convert a private channel back to public', () => { + // # Setup permissions + setupPermissions({resetToDefault: true, publicToPrivate: true}); + + // # Create and visit a public channel + createAndVisitPublicChannel(testTeam.name, 'private-stays-private', 'Private Stays Private').then((channel) => { + // # Open channel settings modal + openChannelSettingsModal(); + + // # Convert to private + convertChannelToPrivate(); + + // # Close the modal + closeChannelSettingsModal(); + + // * Verify channel is now private + verifyChannelIsPrivate(channel.display_name); + + // # Open channel settings modal again + openChannelSettingsModal(); + + // * Verify conversion option to public is disabled + verifyConversionOptionDisabled(false); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + }); + + describe('Role-Based Channel Type Conversion', () => { + it('MM-T3350-1 - System admin cannot convert a private channel to public', () => { + // # Reset permissions to default + resetPermissionsToDefault(); + + // # Login as system admin + cy.apiAdminLogin(); + + // # Create and visit a private channel + createAndVisitPrivateChannel(testTeam.name, 'sysadmin-private-stays-private', 'SysAdmin Private Channel').then(() => { + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option to public is disabled + verifyConversionOptionDisabled(false); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + + it('MM-T3350-2 - Channel admin cannot convert a private channel to public', () => { + // # Reset permissions to default + setupPermissions({resetToDefault: true}); + + // # Login as regular user + cy.apiLogin(testUser); + + // # Create and visit a private channel + createAndVisitPrivateChannel(testTeam.name, 'channel-admin-private', 'Channel Admin Private').then((channel) => { + // # Make test user a channel admin + makeUserChannelAdmin(channel.id, testUser.id); + + // # Visit the channel again after role change + visitChannel(testTeam.name, channel.name); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option to public is disabled + verifyConversionOptionDisabled(false); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + + it('MM-T3350-3 - Team admin cannot convert a private channel to public', () => { + // # Reset permissions to default + setupPermissions({resetToDefault: true}); + + // # Make test user a team admin + makeUserTeamAdmin(testTeam.id, testUser.id); + + // # Login as team admin + cy.apiLogin(testUser); + + // # Create and visit a private channel + createAndVisitPrivateChannel(testTeam.name, 'team-admin-private', 'Team Admin Private').then(() => { + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option to public is disabled + verifyConversionOptionDisabled(false); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + + it('MM-T3350-4 - Regular user cannot convert a private channel to public', () => { + // # Reset permissions to default + resetPermissionsToDefault(); + + // # Create a new regular user specifically for this test + cy.apiCreateUser().then(({user: regularUser}) => { + // # Add user to the team + cy.apiAddUserToTeam(testTeam.id, regularUser.id); + + // # Have admin create a private channel + cy.apiAdminLogin(); + cy.apiCreateChannel(testTeam.id, 'admin-created-private', 'Admin Created Private', 'P').then(({channel}) => { + // # Add the regular user to the channel (as a regular member, not admin) + cy.apiAddUserToChannel(channel.id, regularUser.id); + + // # Login as the regular user + cy.apiLogin(regularUser); + + // # Visit the channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is disabled + verifyConversionOptionDisabled(false); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + }); + }); + + describe('Permission-Based Tests', () => { + it('MM-T3348-2 - Regular user without permission cannot convert public to private', () => { + // # Setup permissions - remove public to private conversion permission + setupPermissions({resetToDefault: true, publicToPrivate: false}); + + // # Create a new regular user specifically for this test + cy.apiCreateUser().then(({user: regularUser}) => { + // # Add user to the team + cy.apiAddUserToTeam(testTeam.id, regularUser.id); + + // # Have admin create a public channel + cy.apiAdminLogin(); + cy.apiCreateChannel(testTeam.id, 'admin-created-public', 'Admin Created Public').then(({channel}) => { + // # Add the regular user to the channel (as a regular member, not admin) + cy.apiAddUserToChannel(channel.id, regularUser.id); + + // # Login as the regular user + cy.apiLogin(regularUser); + + // # Visit the channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is disabled + verifyConversionOptionDisabled(true); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + }); + + it('MM-T3348-3 - Team admin can convert public to private by default', () => { + // # Setup permissions - reset to default + setupPermissions({resetToDefault: true}); + + // # Make test user a team admin + makeUserTeamAdmin(testTeam.id, testUser.id); + + // # Create and visit a public channel + createAndVisitPublicChannel(testTeam.name, 'team-admin-convert', 'Team Admin Convert').then((channel) => { + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is enabled + verifyConversionOptionEnabled(true); + + // # Convert to private + convertChannelToPrivate(); + + // # Close the modal + closeChannelSettingsModal(); + + // * Verify channel is now private + verifyChannelIsPrivate(channel.display_name); + }); + }); + + it('MM-T3348-4 - Team admin cannot convert when permission is removed', () => { + // # Setup permissions - remove permission from team admin + setupPermissions({ + resetToDefault: true, + removeFromTeamAdmin: true, + }); + + // # Make test user a team admin + makeUserTeamAdmin(testTeam.id, testUser.id); + + // # Have admin create a public channel + cy.apiAdminLogin(); + cy.apiCreateChannel(testTeam.id, 'admin-created-team-admin-no-perm', 'Admin Created Team Admin No Permission').then(({channel}) => { + // # Add the team admin user to the channel (as a regular member, not channel admin) + cy.apiAddUserToChannel(channel.id, testUser.id); + + // # Login as the team admin user + cy.apiLogin(testUser); + + // # Visit the channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is disabled + verifyConversionOptionDisabled(true); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + }); + + describe('Channel Admin Tests', () => { + it('MM-T3349-1 - Channel admin can convert public to private when permission is enabled', () => { + // # Setup permissions - enable public to private conversion permission + setupPermissions({resetToDefault: true, publicToPrivate: true}); + + // # Create and visit a public channel + createAndVisitPublicChannel(testTeam.name, 'channel-admin-pub', 'Channel Admin Public').then((channel) => { + // # Make test user a channel admin + makeUserChannelAdmin(channel.id, testUser.id); + + // # Visit the channel again after role change + visitChannel(testTeam.name, channel.name); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is enabled + verifyConversionOptionEnabled(true); + + // # Convert to private + convertChannelToPrivate(); + + // # Close the modal + closeChannelSettingsModal(); + + // * Verify channel is now private + verifyChannelIsPrivate(channel.display_name); + }); + }); + + it('MM-T3349-2 - Channel admin cannot convert public to private when permission is removed', () => { + // Generate a unique channel name with timestamp to avoid conflicts + const timestamp = Date.now(); + const channelId = `channel-admin-no-perm-${timestamp}`; + const displayName = `Channel Admin No Permission ${timestamp}`; + + // # Setup permissions - remove public to private conversion permission + setupPermissions({resetToDefault: true, publicToPrivate: false, removeFromTeamAdmin: true}); + + // # Create and visit a public channel + createAndVisitPublicChannel(testTeam.name, channelId, displayName).then((channel) => { + // # Make test user a channel admin + makeUserChannelAdmin(channel.id, testUser.id); + + // # Log back in as the test user after making channel admin + cy.apiLogin(testUser); + + // # Wait for login to complete + cy.wait(500); + + // # Visit the channel again after role change + visitChannel(testTeam.name, channel.name); + + // # Wait for channel to load + cy.wait(500); + + // # Open channel settings modal + openChannelSettingsModal(); + + // * Verify conversion option is disabled + verifyConversionOptionDisabled(true); + + // # Close the modal + closeChannelSettingsModal(); + }); + }); + }); +}); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts index 5792f8d3122..b3e334607d3 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts @@ -58,19 +58,7 @@ describe('Leave and Archive channel actions display as destructive', () => { cy.get('#channelMembers').should('be.visible'); // * Channel Settings menu option should be visible - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - - // * Edit Channel Header menu option should be visible - cy.get('#channelEditHeader').should('be.visible'); - - // * Edit Channel Purpose menu option should be visible - cy.get('#channelEditPurpose').should('be.visible'); - - // * Rename Channel menu option should be visible - cy.get('#channelRename').should('be.visible'); - - // * Channel Settings close menu option - cy.findByText('Channel Settings').should('be.visible').trigger('mouseout'); + cy.findByText('Channel Settings').should('be.visible'); // * Archive Channel menu option should be visible and have a background-color (destructive) cy.get('#channelArchiveChannel').should('be.visible').focus().should('have.css', 'background-color', 'rgb(210, 75, 78)'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts index 8e70ffe0dd7..dcb6b858b7c 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts @@ -14,15 +14,13 @@ import * as TIMEOUTS from '../../../fixtures/timeouts'; import {getRandomId} from '../../../utils'; describe('Channel routing', () => { - let testTeam: Cypress.Team; - let testUser: Cypress.UserProfile; - let testChannel: Cypress.Channel; + let testTeam: any; + let testUser: any; before(() => { - cy.apiInitSetup().then(({team, user, channel}) => { + cy.apiInitSetup().then(({team, user}) => { testTeam = team; testUser = user; - testChannel = channel; // # Login as test user and go to town square cy.apiLogin(testUser); @@ -34,24 +32,25 @@ describe('Channel routing', () => { // # Create new test channel cy.uiCreateChannel({name: 'Test__Channel'}); - // # Click on channel menu and press rename channel - cy.get('#channelHeaderTitle').click(); + // # Open channel settings modal from channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); - // * Channel Settings menu option should be visible - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - cy.findByText('Rename Channel').click(); + // # Assert if the channel settings modal is present + cy.get('.ChannelSettingsModal').should('be.visible'); - // # Assert if the rename modal present - cy.get('[aria-labelledby="renameChannelModalLabel"').should('be.visible').within(() => { - // # type the two 26 character strings with 2 underscores between them and click on save - cy.get('#channel_name').clear().type('uzsfmtmniifsjgesce4u7yznyh__uzsfmtmniifsjgesce5u7yznyh', {force: true}).wait(TIMEOUTS.HALF_SEC); - cy.get('#save-button').should('be.visible').click(); + // # Click on the URL input button to edit the URL + cy.get('.url-input-button').should('be.visible').click({force: true}); - // # Assert the error occurred with the appropriate message - cy.get('.input__help').should('have.class', 'error'); - cy.get('.input__help').should('have.text', 'User IDs are not allowed in channel URLs.'); - cy.findByText('Cancel').click(); - }); + // # Type the two 26 character strings with 2 underscores between them + cy.get('.url-input-container input').clear().type('uzsfmtmniifsjgesce4u7yznyh__uzsfmtmniifsjgesce5u7yznyh', {force: true}).wait(TIMEOUTS.HALF_SEC); + + // # Assert the error occurred with the appropriate message + cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above'); + cy.get('.url-input-error').should('contain', 'User IDs are not allowed in channel URLs.'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); }); it('MM-T884_2 Creating new channel validates against two user IDs being used as channel name', () => { @@ -97,42 +96,34 @@ describe('Channel routing', () => { it('MM-T883 Channel URL validation for spaces between characters', () => { const firstWord = getRandomId(26); const secondWord = getRandomId(26); + const channelName = 'test-channel-' + getRandomId(8); - // # In a test channel, click the "v" to the right of the channel name in the header - cy.findByText(`${testChannel.display_name}`).click(); - cy.get('#channelHeaderTitle').click(); + // # Create a new test channel and navigate to it + cy.apiCreateChannel(testTeam.id, channelName, 'Test Channel for URL Validation').then(({channel}) => { + cy.apiAddUserToChannel(channel.id, testUser.id); + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + }); - // * Channel Settings menu option should be visible - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - - // # Select "Rename Channel" - cy.findByText('Rename Channel').click(); + // # Open channel settings modal from channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); + cy.findByText('Channel Settings').click(); // # Change the channel name to {26 alphanumeric characters}[insert 2 spaces]{26 alphanumeric characters} // i.e. a total of 54 characters separated by 2 spaces - cy.get('#display_name').clear().type(`${firstWord}${Cypress._.repeat(' ', 2)}${secondWord}`); + cy.get('#input_channel-settings-name').clear().type(`${firstWord}${Cypress._.repeat(' ', 2)}${secondWord}`); - // # Hit Save - cy.findByText('Save').click(); + // # Save changes + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify changes are saved + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); // * The channel name should be updated to the characters you typed with only 1 space between the characters (extra spaces are trimmed) cy.get('#channelHeaderTitle').contains(`${firstWord} ${secondWord}`); - // # In a test channel, click the "v" to the right of the channel name in the header - cy.get('#channelHeaderTitle').click(); - - // * Channel Settings menu option should be visible - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - - // # Select "Rename Channel" - cy.findByText('Rename Channel').click(); - - // # Change the URL to {26 alphanumeric characters}--{26 alphanumeric characters} - cy.get('#channel_name').clear().type(`${firstWord}${Cypress._.repeat('-', 2)}${secondWord}`); - - // # Hit Save - cy.findByText('Save').click(); - // * The channel URL should be updated to the characters you typed, separated by 2 dashes cy.url().should('equal', `${Cypress.config('baseUrl')}/${testTeam.name}/channels/${firstWord}${Cypress._.repeat('-', 2)}${secondWord}`); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts index c3c971cd957..ab24ecc204d 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts @@ -202,22 +202,33 @@ describe('Team Permissions', () => { // # Visit the `Off-Topic` channel in the new team cy.visit(`/${team.name}/channels/off-topic`); - // # Open channel header menu - cy.uiOpenChannelMenu().wait(TIMEOUTS.HALF_SEC); + // # Open channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); - // * Verify dropdown opens - cy.get('#channelHeaderDropdownMenu').should('be.visible'); - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); + // * Verify Channel Settings option is visible + cy.findByText('Channel Settings').should('be.visible'); - // * Verify `Edit Channel Header` menu item is visible - cy.get('#channelEditHeader').should('be.visible'); + // # Click on Channel Settings + cy.findByText('Channel Settings').click(); - // * Verify `Edit Channel Purpose` menu item is visible - cy.get('#channelEditPurpose').should('be.visible'); + // * Verify Channel Settings modal opens + cy.get('.ChannelSettingsModal').should('be.visible'); - // * Verify `Rename Channel` menu item is visible - cy.get('#channelRename').should('be.visible'); + // * Verify user can edit channel name + cy.get('#input_channel-settings-name').should('be.visible').and('not.be.disabled'); + + // * Verify user can edit channel URL + cy.get('.url-input-button').should('be.visible').and('not.be.disabled'); + + // * Verify user can edit channel purpose + cy.get('#channel_settings_purpose_textbox').should('be.visible').and('not.be.disabled'); + + // * Verify user can edit channel header + cy.get('#channel_settings_header_textbox').should('be.visible').and('not.be.disabled'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + cy.get('.ChannelSettingsModal').should('not.exist'); }); }); }); - diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js index cebccc88a33..b20341801c3 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js @@ -114,13 +114,15 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag cy.uiOpenProfileModal('Profile Settings'); verifyEmojiPickerNotOpen(); - ['Edit Channel Header', 'Rename Channel'].forEach((modal) => { - // # Click on the channel name in the channel header to open the channel menu options - cy.get('#channelHeaderTitle').click(); + // # Open Channel Settings Modal + cy.uiOpenChannelMenu('Channel Settings'); + verifyEmojiPickerNotOpen(); - // # Select the "Edit Channel Header" option from the dropdown - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - cy.findByText(modal).click(); + ['Channel Purpose', 'Channel Header'].forEach((modal) => { + // # Open channel menu and click View Info + cy.uiOpenChannelMenu('View Info'); + + cy.findByText(modal).parent().findAllByRole('button', {name: 'Edit'}).click(); doReactToLastMessageShortcut(); @@ -129,6 +131,9 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag // # Close the modal pressEscapeKey(); + + // # Close channel info + cy.uiOpenChannelMenu('Close Info'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js index b1c6bd98660..719d73d0f88 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js @@ -53,7 +53,7 @@ describe('Onboarding', () => { // # Enable any user with an account on the server to join the team cy.get('input.mm-modal-generic-section-item__input-checkbox').last().should('be.visible').click(); - cy.findAllByTestId('mm-save-changes-panel__save-btn').should('be.visible').click(); + cy.get('[data-testid="SaveChangesPanel__save-btn"]').should('be.visible').click(); // # Close the modal cy.findByLabelText('Close').should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js index 4400dcafa35..2c4970095d2 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js @@ -102,11 +102,19 @@ describe('channels > run', {testIsolation: true}, () => { cy.startPlaybookRunWithSlashCommand('Playbook', playbookRunName); cy.verifyPlaybookRunActive(testTeam.id, playbookRunName); - // # Open the channel header - cy.get('#channelHeaderTitle').click(); - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); + // # Open channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); + + // # Click on Channel Settings + cy.findByText('Channel Settings').should('be.visible').click(); + + // * Verify Channel Settings modal opens + cy.get('.ChannelSettingsModal').should('be.visible'); // * Verify the ability to edit the channel header exists - cy.get('#channelEditHeader').should('exist'); + cy.get('#channel_settings_header_textbox').should('be.visible').and('not.be.disabled'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); }); }); diff --git a/e2e-tests/cypress/tests/support/ui_commands.ts b/e2e-tests/cypress/tests/support/ui_commands.ts index 5a6cce74cd9..ae64c32c67d 100644 --- a/e2e-tests/cypress/tests/support/ui_commands.ts +++ b/e2e-tests/cypress/tests/support/ui_commands.ts @@ -462,20 +462,35 @@ function getCurrentChannelId(): ChainableT { Cypress.Commands.add('getCurrentChannelId', getCurrentChannelId); function updateChannelHeader(text: string) { - cy.get('#channelHeaderTitle'). - should('be.visible'). - click(); - cy.get('#channelHeaderDropdownMenu'). - should('be.visible'); + // # Open channel header dropdown + cy.get('#channelHeaderDropdownButton').click(); - // * Channel Settings menu option should be visible - cy.findByText('Channel Settings').should('be.visible').trigger('mouseover'); - cy.findByText('Edit Channel Header').click(); - cy.get('#edit_textbox'). + // # Click on Channel Settings + cy.findByText('Channel Settings').should('be.visible').click(); + + // * Verify Channel Settings modal opens + cy.get('.ChannelSettingsModal').should('be.visible'); + + // # Edit channel header in the modal + cy.get('#channel_settings_header_textbox'). + should('be.visible'). clear(). - type(text). - type('{enter}'). - wait(TIMEOUTS.HALF_SEC); + type(text); + + // # Save changes + cy.get('[data-testid="SaveChangesPanel__save-btn"]').click(); + + // * Verify changes are saved + cy.get('.SaveChangesPanel').should('contain', 'Settings saved'); + + // # Close the modal + cy.get('.GenericModal .modal-header button[aria-label="Close"]').click(); + + // * Verify modal is closed + cy.get('.ChannelSettingsModal').should('not.exist'); + + // Wait for UI to stabilize + cy.wait(TIMEOUTS.HALF_SEC); } Cypress.Commands.add('updateChannelHeader', updateChannelHeader); diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts b/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts index ec54a14d176..0109e0abb0b 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts @@ -1,18 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Locator, expect} from '@playwright/test'; +import {Locator, expect, Page} from '@playwright/test'; import ChannelsHeader from './header'; import ChannelsPostCreate from './post_create'; import ChannelsPostEdit from './post_edit'; import ChannelsPost from './post'; -import {duration} from '@/util'; +import {duration, hexToRgb} from '@/util'; import {waitUntil} from '@/test_action'; export default class ChannelsCenterView { readonly container: Locator; + readonly page: Page; readonly header; readonly postCreate; @@ -27,9 +28,12 @@ export default class ChannelsCenterView { readonly scheduledDraftSeeAllLink; readonly postEdit; readonly editedPostIcon; + readonly channelBanner; - constructor(container: Locator) { + constructor(container: Locator, page: Page) { this.container = container; + this.page = page; + this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")'; this.scheduledDraftDMChannelLocatorString = 'div.ScheduledPostIndicator span a'; this.header = new ChannelsHeader(this.container.locator('.channel-header')); @@ -43,6 +47,7 @@ export default class ChannelsCenterView { this.scheduledDraftDMChannelLocator = container.locator(this.scheduledDraftDMChannelLocatorString); this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")'); this.editedPostIcon = (postID: string) => container.locator(`#postEdited_${postID}`); + this.channelBanner = container.getByTestId('channel_banner_container'); } async toBeVisible() { @@ -156,4 +161,45 @@ export default class ChannelsCenterView { await this.editedPostIcon(postID).click(); } } + + async assertChannelBanner(text: string, backgroundColor: string) { + await expect(this.channelBanner).toBeVisible(); + + const actualText = await this.channelBanner.textContent(); + expect(actualText).toBe(text); + + const actualBackgroundColor = await this.channelBanner.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('background-color'); + }); + + expect(actualBackgroundColor).toBe(hexToRgb(backgroundColor)); + } + + async assertChannelBannerNotVisible() { + await expect(this.channelBanner).not.toBeVisible(); + } + + async assertChannelBannerHasBoldText(text: string) { + const boldText = await this.channelBanner.locator('strong'); + expect(boldText).toBeVisible(); + + const actualText = await boldText.textContent(); + expect(actualText).toBe(text); + } + + async assertChannelBannerHasItalicText(text: string) { + const italicText = await this.channelBanner.locator('em'); + expect(italicText).toBeVisible(); + + const actualText = await italicText.textContent(); + expect(actualText).toBe(text); + } + + async assertChannelBannerHasStrikethroughText(text: string) { + const strikethroughText = await this.channelBanner.locator('del'); + expect(strikethroughText).toBeVisible(); + + const actualText = await strikethroughText.textContent(); + expect(actualText).toBe(text); + } } diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/header.ts b/e2e-tests/playwright/lib/src/ui/components/channels/header.ts index e590d2a8d15..0ffdb5434fe 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/header.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/header.ts @@ -6,11 +6,20 @@ import {Locator, expect} from '@playwright/test'; export default class ChannelsHeader { readonly container: Locator; + readonly channelMenuDropdown; + constructor(container: Locator) { this.container = container; + + this.channelMenuDropdown = container.locator('[aria-controls="channelHeaderDropdownMenu"]'); } async toBeVisible() { await expect(this.container).toBeVisible(); } + + async openChannelMenu() { + await this.channelMenuDropdown.isVisible(); + await this.channelMenuDropdown.click(); + } } diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/settings/configuration_settings.ts b/e2e-tests/playwright/lib/src/ui/components/channels/settings/configuration_settings.ts new file mode 100644 index 00000000000..dcfcb22c154 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/channels/settings/configuration_settings.ts @@ -0,0 +1,52 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +export default class ConfigurationSettings { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async save() { + const saveButton = this.container.getByTestId('SaveChangesPanel__save-btn'); + await expect(saveButton).toBeVisible(); + await saveButton.click(); + } + + async enableChannelBanner() { + const toggleButton = await this.container.getByTestId('channelBannerToggle-button'); + const classes = await toggleButton.getAttribute('class'); + if (!classes?.includes('active')) { + await toggleButton.click(); + } + } + + async disableChannelBanner() { + const toggleButton = await this.container.getByTestId('channelBannerToggle-button'); + const classes = await toggleButton.getAttribute('class'); + if (classes?.includes('active')) { + await toggleButton.click(); + } + } + + async setChannelBannerText(text: string) { + const textBox = await this.container.getByTestId('channel_banner_banner_text_textbox'); + await expect(textBox).toBeVisible(); + await textBox.fill(text); + } + + async setChannelBannerTextColor(color: string) { + const colorInput = await this.container.locator( + '#channel_banner_banner_background_color_picker-inputColorValue', + ); + await expect(colorInput).toBeVisible(); + await colorInput.fill(color); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts b/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts index 7e8c282cc91..28ba99d2340 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/settings/settings_modal.ts @@ -6,11 +6,16 @@ import {Locator, expect} from '@playwright/test'; import DisplaySettings from './display_settings'; import NotificationsSettings from './notification_settings'; +import ConfigurationSettings from '@/ui/components/channels/settings/configuration_settings'; + export default class SettingsModal { readonly container: Locator; readonly notificationsSettingsTab; + readonly configurationSettingsTab; + readonly notificationsSettings; + readonly configurationSettings; readonly displaySettingsTab; readonly displaySettings; @@ -19,7 +24,12 @@ export default class SettingsModal { this.container = container; this.notificationsSettingsTab = container.locator('#notificationsButton'); + this.configurationSettingsTab = container.locator('#configurationButton'); + this.notificationsSettings = new NotificationsSettings(container.locator('#notificationsSettings')); + this.configurationSettings = new ConfigurationSettings( + container.locator('.ChannelSettingsModal__configurationTab'), + ); this.displaySettingsTab = container.locator('#displayButton'); this.displaySettings = new DisplaySettings(container.locator('#displaySettings')); @@ -52,4 +62,19 @@ export default class SettingsModal { await expect(this.container).not.toBeVisible(); } + + async openConfigurationTab(): Promise { + await expect(this.configurationSettingsTab).toBeVisible(); + await this.configurationSettingsTab.click(); + + await this.configurationSettings.toBeVisible(); + + return this.configurationSettings; + } + + async close() { + const closeButton = this.container.locator('button.close'); + await expect(closeButton).toBeVisible(); + await closeButton.click(); + } } diff --git a/e2e-tests/playwright/lib/src/ui/pages/channels.ts b/e2e-tests/playwright/lib/src/ui/pages/channels.ts index 14bf9c6ca92..a30a768d8cb 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/channels.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/channels.ts @@ -4,6 +4,7 @@ import {expect, Page} from '@playwright/test'; import {ChannelsPost, components} from '@/ui/components'; +import SettingsModal from '@/ui/components/channels/settings/settings_modal'; export default class ChannelsPage { readonly channels = 'Channels'; @@ -38,7 +39,7 @@ export default class ChannelsPage { // The main areas of the app this.globalHeader = new components.GlobalHeader(this, page.locator('#global-header')); this.searchPopover = new components.SearchPopover(page.locator('#searchPopover')); - this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view')); + this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'), page); this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer')); this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); @@ -64,6 +65,8 @@ export default class ChannelsPage { // Posts this.postContainer = page.locator('div.post-message__text'); + + page.locator('#channelHeaderDropdownMenu'); } async toBeVisible() { @@ -95,6 +98,28 @@ export default class ChannelsPage { await this.centerView.postMessage(message, files); } + async openChannelSettings(): Promise { + await this.centerView.header.openChannelMenu(); + await this.page.locator('#channelSettings[role="menuitem"]').click(); + await this.settingsModal.toBeVisible(); + + return this.settingsModal; + } + + async newChannel(name: string, channelType: string) { + await this.page.locator('#browseOrAddChannelMenuButton').click(); + await this.page.locator('#createNewChannelMenuItem').click(); + await this.page.locator('#input_new-channel-modal-name').fill(name); + + if (channelType === 'P') { + await this.page.locator('#public-private-selector-button-P').click(); + } else { + await this.page.locator('#public-private-selector-button-O').click(); + } + + await this.page.getByText('Create channel').click(); + } + async openUserAccountMenu() { await this.userAccountMenuButton.click(); await expect(this.userAccountMenu.container).toBeVisible(); diff --git a/e2e-tests/playwright/lib/src/util.ts b/e2e-tests/playwright/lib/src/util.ts index 9a9ffdb7780..9c23dfe345f 100644 --- a/e2e-tests/playwright/lib/src/util.ts +++ b/e2e-tests/playwright/lib/src/util.ts @@ -46,3 +46,16 @@ export const defaultTeam = {name: 'ad-1', displayName: 'eligendi', type: 'O'}; export const illegalRe = /[/?<>\\:*|":&();]/g; export const simpleEmailRe = /\S+@\S+\.\S+/; + +export function hexToRgb(hex: string): string { + // Remove the # if present + hex = hex.replace(/^#/, ''); + + // Parse the hex values + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + // Return the RGB string + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/e2e-tests/playwright/package-lock.json b/e2e-tests/playwright/package-lock.json index 044c84531b3..8b0525bc4b5 100644 --- a/e2e-tests/playwright/package-lock.json +++ b/e2e-tests/playwright/package-lock.json @@ -47,6 +47,1300 @@ } } }, + "../../webapp/platform/client/node_modules/@jest/console": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/core": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/@jest/environment": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/fake-timers": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/globals": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/reporters": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/@jest/source-map": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/test-result": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/transform": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@jest/types": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "../../webapp/platform/client/node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "../../webapp/platform/client/node_modules/@tootallnate/once": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "../../webapp/platform/client/node_modules/@types/yargs": { + "version": "16.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "../../webapp/platform/client/node_modules/acorn-globals": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "../../webapp/platform/client/node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../../webapp/platform/client/node_modules/acorn-walk": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "../../webapp/platform/client/node_modules/babel-jest": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "../../webapp/platform/client/node_modules/babel-plugin-jest-hoist": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/babel-preset-jest": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "../../webapp/platform/client/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../webapp/platform/client/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../webapp/platform/client/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/cssom": { + "version": "0.4.4", + "dev": true, + "license": "MIT" + }, + "../../webapp/platform/client/node_modules/data-urls": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/diff-sequences": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/domexception": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/emittery": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "../../webapp/platform/client/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "../../webapp/platform/client/node_modules/expect": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../webapp/platform/client/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/http-proxy-agent": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "../../webapp/platform/client/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../webapp/platform/client/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "../../webapp/platform/client/node_modules/jest": { + "version": "27.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.1.0", + "import-local": "^3.0.2", + "jest-cli": "^27.1.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/jest-changed-files": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-circus": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-cli": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/jest-config": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/jest-diff": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-docblock": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-each": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-environment-node": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-get-type": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-haste-map": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "../../webapp/platform/client/node_modules/jest-leak-detector": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-message-util": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-mock": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-regex-util": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-resolve": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-runner": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-runtime": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-snapshot": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-util": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-validate": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-watcher": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "../../webapp/platform/client/node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "../../webapp/platform/client/node_modules/jsdom": { + "version": "16.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/jsdom/node_modules/form-data": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "../../webapp/platform/client/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "../../webapp/platform/client/node_modules/resolve.exports": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/saxes": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../webapp/platform/client/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "../../webapp/platform/client/node_modules/tr46": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../webapp/platform/client/node_modules/v8-to-istanbul": { + "version": "8.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "../../webapp/platform/client/node_modules/v8-to-istanbul/node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "../../webapp/platform/client/node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/webidl-conversions": { + "version": "6.1.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "../../webapp/platform/client/node_modules/whatwg-encoding": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "../../webapp/platform/client/node_modules/whatwg-mimetype": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "../../webapp/platform/client/node_modules/whatwg-url": { + "version": "8.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "../../webapp/platform/client/node_modules/write-file-atomic": { + "version": "3.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "../../webapp/platform/client/node_modules/ws": { + "version": "7.5.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "../../webapp/platform/client/node_modules/xml-name-validator": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0" + }, "../../webapp/platform/types": { "name": "@mattermost/types", "version": "10.6.0", @@ -94,8 +1388,6 @@ }, "node_modules/@axe-core/playwright": { "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.1.tgz", - "integrity": "sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA==", "license": "MPL-2.0", "dependencies": { "axe-core": "~4.10.2" @@ -108,7 +1400,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -122,7 +1413,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -132,7 +1422,6 @@ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.0.2", @@ -144,7 +1433,6 @@ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -155,16 +1443,13 @@ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.4.1", "dev": true, "license": "MIT", "dependencies": { @@ -182,8 +1467,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -195,7 +1478,6 @@ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -210,7 +1492,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -221,7 +1502,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -234,7 +1514,6 @@ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -244,7 +1523,6 @@ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -257,7 +1535,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -276,23 +1553,43 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -305,7 +1602,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", "integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -315,7 +1611,6 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -325,7 +1620,6 @@ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" @@ -336,8 +1630,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -346,8 +1638,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -360,8 +1650,6 @@ }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -374,8 +1662,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -391,7 +1677,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -417,7 +1702,6 @@ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@emnapi/core": "^1.4.0", @@ -427,8 +1711,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -440,8 +1722,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -449,8 +1729,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -464,7 +1742,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.30.10.tgz", "integrity": "sha512-fKASLI1Qj38v64Vb6VktRsW2MZnxQ5JBDGPPk+sP/bSiTZ0D0GC5pz2s+tQaGD7wReYNy9JKzSujrkJqFiBbSg==", - "license": "MIT", "dependencies": { "@percy/cli-app": "1.30.10", "@percy/cli-build": "1.30.10", @@ -487,7 +1764,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.30.10.tgz", "integrity": "sha512-XL1vW4A2C74DOgXx6kilxUxGVtAgQDik+J1Gyr0SF324cpW6fIDB55ktLSvbv8z4qRqUIhEEWyOAWGpngwv8og==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10", "@percy/cli-exec": "1.30.10" @@ -500,7 +1776,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.30.10.tgz", "integrity": "sha512-mMj1asBNW8oYavMuMtg3TU72+UoCg/8eoKshZ8jb4i9Tg/nWG1q9wfbS9/lSdenKPjQuqzfpQ8TB/Q/UI2cajA==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10" }, @@ -512,7 +1787,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.30.10.tgz", "integrity": "sha512-xvTZBTpjQMxihEVI3bEjIfBRjZ5momxFeFgLUFQUhQZXPtNp3o+vWFI1CCltjkK2JAXK/q883ozoeiuKgoacWg==", - "license": "MIT", "dependencies": { "@percy/config": "1.30.10", "@percy/core": "1.30.10", @@ -529,7 +1803,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.30.10.tgz", "integrity": "sha512-8xb4WhC67qiX6lRmpKnJhhvJiYVvTC4bQ9/BZYUpk6r3Ftq8ViOe0sySVj3Ms0vF1IwPn2t0E1osqKOvtKumFg==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10" }, @@ -541,7 +1814,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.30.10.tgz", "integrity": "sha512-NV2KqC15Y9e0PyCqQrt01QuOT7sIXhs3yrovuVvbutqNL2+Ol25eHGoNJG+kMo9jSJM36ZWmD9am4eX6L2VBaQ==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10", "@percy/logger": "1.30.10", @@ -556,7 +1828,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.30.10.tgz", "integrity": "sha512-xlrJj9VvoWLuWA0wjmY2cvvtOvzzPzGuHET14zUOhM7leBifGI8Gz6wNGtXKblKAtJUg+0dYPBZd0vNO6ZVIeA==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10", "yaml": "^2.0.0" @@ -569,7 +1840,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.30.10.tgz", "integrity": "sha512-6+aYDC3eZk+P7PpxftKfu/sSXyb0QpN4Rdtynsy5jt9RobrEL7q7YyZIbeksZm/WtgYszIa8yYQP3i5LUvfZdQ==", - "license": "MIT", "dependencies": { "@percy/cli-command": "1.30.10", "fast-glob": "^3.2.11", @@ -583,7 +1853,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.30.10.tgz", "integrity": "sha512-eIyuiBgiv5e+x8B14bOKkl38cYjTud3OYjN4Uo5uqo/raBaFlI24aoOMtSyq0TVKBNgPVEgmNAgQxj+P9fHFtA==", - "license": "MIT", "dependencies": { "@percy/env": "1.30.10", "@percy/logger": "1.30.10", @@ -598,7 +1867,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.30.10.tgz", "integrity": "sha512-ixzxZ+rwHUCSOxFpAEEsqDfJVnb5mIYfnayGjsaIe1hnMvo/t/K9/npu6JhBGHR/kpaummJQEPCRxEuclN93Ew==", - "license": "MIT", "dependencies": { "@percy/logger": "1.30.10", "ajv": "^8.6.2", @@ -609,34 +1877,11 @@ "node": ">=14" } }, - "node_modules/@percy/config/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@percy/config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/@percy/core": { "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.30.10.tgz", "integrity": "sha512-6oZkiOdjy3YqFpZHVX9ZCHJKxslVEE9cgfqXI09Zba4iFKpG9PJ0pHFc/uee89G0uJj82sqIMu8Jgm833NLyMQ==", "hasInstallScript": true, - "license": "MIT", "dependencies": { "@percy/client": "1.30.10", "@percy/config": "1.30.10", @@ -664,7 +1909,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -673,7 +1917,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -684,14 +1927,12 @@ "node_modules/@percy/dom": { "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.30.10.tgz", - "integrity": "sha512-EJUHmrh6UE8YD3MZ1Hnrc2sTVAlQt2xTC0wWSBDVz2h1/IUvHw5yE7TQHhp615IYOmurI3k8AKjmdI7b70uD7Q==", - "license": "MIT" + "integrity": "sha512-EJUHmrh6UE8YD3MZ1Hnrc2sTVAlQt2xTC0wWSBDVz2h1/IUvHw5yE7TQHhp615IYOmurI3k8AKjmdI7b70uD7Q==" }, "node_modules/@percy/env": { "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.30.10.tgz", "integrity": "sha512-kPJsACurTY9/5TZH8xDMDMaz2Yas9SLr0DqlCqIaqBWxikDzdWXUiDWwAIab0Dik5oAFbQAXC7f4V+0MBlZWag==", - "license": "MIT", "dependencies": { "@percy/logger": "1.30.10" }, @@ -703,7 +1944,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.30.10.tgz", "integrity": "sha512-ABSzY/WVI/ePXac73Q6qEUKqUZ1+NJswzbZy/I/fgiBWmkzf4hKIrlD9RQZYmkjLWVESbjKPhZTmH3bntO084Q==", - "license": "MIT", "engines": { "node": ">=14" } @@ -712,7 +1952,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/monitoring/-/monitoring-1.30.10.tgz", "integrity": "sha512-Iaa6nx1GFc92uZdYHM1EJfyn4aIjm/DWPOp6RbF4eSFZhlpqhvqBQgXvgDLylNNrKmdaXsFIE56BjpJZhamtsQ==", - "license": "MIT", "dependencies": { "@percy/config": "1.30.10", "@percy/logger": "1.30.10", @@ -727,7 +1966,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.0.8.tgz", "integrity": "sha512-v70IKPVy15mDxis5+of2S62AJHdeG99BlA08oj/XpK68CVyNUSE6I8I54EUm3PbEeKxdZLWTdhX+8I69UkxPjA==", - "license": "MIT", "engines": { "node": ">=14" }, @@ -739,7 +1977,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.30.10.tgz", "integrity": "sha512-EOFm6XDbXIpo1YjF+JWxNCW5TB0ZaqjQfHLtOCmffhHi2T0MCXSAHdNxeTUyADyySzWjD4bKba/PbZwwTVE8Zw==", - "license": "MIT", "engines": { "node": ">=14" } @@ -748,7 +1985,6 @@ "version": "1.30.10", "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.30.10.tgz", "integrity": "sha512-dTxSa0Dy7SZrXcjzy+kvWFIxBb3n/YAPJWMMX063ZMeAOINx5MhZ+JizOijW0QXXhy/UJAmW+2Q3VB32kj391Q==", - "license": "MIT", "dependencies": { "@percy/config": "1.30.10", "@percy/sdk-utils": "1.30.10" @@ -761,7 +1997,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", - "license": "Apache-2.0", "dependencies": { "playwright": "1.52.0" }, @@ -777,7 +2012,6 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz", "integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" @@ -804,7 +2038,6 @@ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", @@ -827,7 +2060,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -843,7 +2075,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -857,7 +2088,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -871,7 +2101,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -885,7 +2114,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -899,7 +2127,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -913,7 +2140,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -927,7 +2153,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -941,7 +2166,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -955,7 +2179,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -969,7 +2192,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -983,7 +2205,6 @@ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -997,7 +2218,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1011,7 +2231,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1025,7 +2244,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1039,7 +2257,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1053,7 +2270,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1067,7 +2283,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1081,7 +2296,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1095,7 +2309,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1109,7 +2322,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1119,21 +2331,18 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1143,15 +2352,13 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/fs-extra": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1161,7 +2368,6 @@ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, - "license": "MIT", "dependencies": { "@types/minimatch": "*", "@types/node": "*" @@ -1171,36 +2377,31 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/mime-types": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/node": { "version": "22.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "devOptional": true, - "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -1210,7 +2411,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "dev": true, - "license": "MIT", "dependencies": { "csstype": "^3.0.2" } @@ -1219,7 +2419,6 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -1230,7 +2429,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.30.1", @@ -1255,18 +2453,76 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", - "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", + "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", + "@typescript-eslint/scope-manager": "8.31.0", + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/typescript-estree": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "engines": { @@ -1281,15 +2537,42 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz", + "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz", + "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "@typescript-eslint/visitor-keys": "8.31.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1304,7 +2587,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.30.1", "@typescript-eslint/utils": "8.30.1", @@ -1324,11 +2606,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", - "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz", + "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==", "dev": true, - "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1342,7 +2624,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.30.1", "@typescript-eslint/visitor-keys": "8.30.1", @@ -1364,12 +2645,53 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.30.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.30.1", @@ -1388,12 +2710,41 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { "version": "8.30.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" @@ -1406,12 +2757,42 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz", + "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.31.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1420,182 +2801,169 @@ } }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.6.1.tgz", - "integrity": "sha512-wbOgzEUDjfEmziD0TKMdDoRsCa0zHhtBWcrllJr7iZGPvSfrU7m5VGlpbO3McCi1LLsv7FFvUWej5nFQ+Emigw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.6.3.tgz", + "integrity": "sha512-+BbDAtwT4AVUyGIfC6SimaA6Mi/tEJCf5OYV5XQg7WIOW0vyD15aVgDLvsQscIZxgz42xB6DDqR7Kv6NBQJrEg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.6.1.tgz", - "integrity": "sha512-d5hh78dlTaoFXZTQuDLUxxmV/tS3etw11HCm1a1q5/nUrfgLBUkZLn4u7Pg/jN4ois6aMCabcbt5DZkf4dIx1g==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.6.3.tgz", + "integrity": "sha512-q6qMXI8wT0u0GUns/L26kYHdX2du4yEhwxrXjPj/egvysI8XqcTyjnbWQm3NSJPw0Un2wvKPh0WuoTSJEZgbqw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.6.1.tgz", - "integrity": "sha512-PEV8ICqDKe8ujbxO9FL62/MqNNN2BvahNgtkG5Z49BNNBGtogvzkbgf5GeyrIIt1b3ky1w7IllVRAyqIUeuFEg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.6.3.tgz", + "integrity": "sha512-/7xs7QNNW17VZrFBf+2C95G72rA5c0YGtR18pvWrzM2tVPLrTsKnLl32hi3CG7F6cwwYRy7h61BIkMHh7qaZkw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.6.1.tgz", - "integrity": "sha512-MGBBPrWH0nKMkUvAJ8Qs3Fe2ObHY+t1TVylJdMFY620qvFcu7d0bc89O0tJuZtkzLAx0sUSHQNYQcczhVHn2wQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.6.3.tgz", + "integrity": "sha512-2xv5cUQCt+eYuq5tPF4AHStpzE8i8qdYnhitpvDv9vxzOZ5a0sdzgA8WHYgFe15dP469YOSivenMMdpuRcgE9Q==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.6.1.tgz", - "integrity": "sha512-hEmYRcRhde66Pluis2epKWoow2qbeb5PWhX+s/VaqNbfxKIotV9EI88K/9jKH8s2Mwa8Xy/bfWLfDZzfMNtr6Q==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.6.3.tgz", + "integrity": "sha512-4KaZxKIeFt/jAOD/zuBOLb5yyZk/XG9FKf5IXpDP21NcYxeus/os6w+NCK7wjSJKbOpHZhwfkAYLkfujkAOFkw==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.6.1.tgz", - "integrity": "sha512-ffY3KJvGXsPw+dYr7MDJcSpfJDw2sc7Y9A+Lz+xk89CX+cEzBt/sxbCY4n8Ew6xNC8jKRoE5+1ELVRxbpc5ozw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.6.3.tgz", + "integrity": "sha512-dJoZsZoWwvfS+khk0jkX6KnLL1T2vbRfsxinOR3PghpRKmMTnasEVAxmrXLQFNKqVKZV/mU7gHzWhiBMhbq3bw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.6.1.tgz", - "integrity": "sha512-dyH9+OAzA3klgwkVzMxz5jaYTNqcYPfp8YSSTugF8lC2M3pTrToPrbG+kPEAds5ejBgtkGGpHJ0ONRf1JblLoA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.6.3.tgz", + "integrity": "sha512-2Y6JcAY9e557rD6O53Zmeblrfu48vQfl5CrrKjt0/2J1Op/pKX3WI8TOh0gs5T4qX9uJDqdte11SNUssckdfUA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.6.1.tgz", - "integrity": "sha512-8y2ayqhBL0Y7KiuE1ZvuTwv/DmkjCRZQoSE2gvid3SkxRjJ6qJ3EfG/Yv8O9dktv3n6z++Q7ZtAUlKRsiN1wpQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.6.3.tgz", + "integrity": "sha512-kvcEe+j0De/DEfTNkte2xtmwSL4/GMesArcqmSgRqoOaGknUYY3whJ/3GygYKNMe82vvao4PaQkBlCrxhi88wQ==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.6.1.tgz", - "integrity": "sha512-GZQe8ADu2Y88IVgbob1e8RpVH5MlMWjbDC5X/USa6UZnWQn7sKrO7XdM/9HQHOir+jeu7tJjTBf3tyrq7qtKcA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.6.3.tgz", + "integrity": "sha512-fruY8swKre2H0J96h8HE+kN3iUnDR3VDd2wxBn4BxDw+5g7GOHBz5x1533l9mqAqHI4b2dMBECI4RtQdMOiBeQ==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.6.1.tgz", - "integrity": "sha512-AkWpaZul4Q0bW1IJdfS6vxSoC0Gsjf/PTF1rgCCtDV7b8V25iGazCK02X/wLxcEmIh9uYq2UfasLal0DfKdPZw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.6.3.tgz", + "integrity": "sha512-1w0eaSxm9e69TEj9eArZDPQ7mL2VL6Bb4AXeLOdQoe5SNQpZaL6RlwGm7ss9xErwC7c9Hvob/ZZF7i8xYT55zg==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.6.1.tgz", - "integrity": "sha512-aZlTp6kjbKFFOiDYwknTxB8YUvWLT+hwMbif3cAlvF/c1jtwNLKPGFP/iKx7HkYpRSJYbHf/N0Ns5HkdgeUM9A==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.6.3.tgz", + "integrity": "sha512-ymUqs8AQyHTQQ50aN7EcMV47gKh5yKg8a0+SWSuDZEl6eGEOKn590D/iMDydS5KoWbMTy6/pBipS4vsPUEjYVw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.6.1.tgz", - "integrity": "sha512-A9P2H+/LKHtuUfk/REkWrrawWXx2Z5atIHuU1I5Sv8uOj+NirmoCOPS8H+nfZyemsX4vzSe4id/KDYSQGsnPrA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.6.3.tgz", + "integrity": "sha512-LSfz1cguLZD+c00aTVbtrqX1x1sIR38M2lLYW3CZTGfippkg56Hf8kejHPA8H26OwB71c9/W78BCbgcdnEW+jQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.6.1.tgz", - "integrity": "sha512-Y9Sz1GXo/2z43KiMh4MfP0rTknsFNOsTjly068QhGJYO6qjIsvpbf4vhDnMFTDlBz8bDsibG1ggZ6BRjEzjmiA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.6.3.tgz", + "integrity": "sha512-gehKZDmNDS2QTxefwPBLi0RJgOQ0dIoD/osCcNboDb3+ZKcbSMBaF3+4R5vj+XdV0QBdZg3vXwdwZswfEkQOcA==", "cpu": [ "wasm32" ], "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" @@ -1605,42 +2973,39 @@ } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.6.1.tgz", - "integrity": "sha512-Z2gYsbEsv0eyD/wx8uDnGBmo7n9z1oAJnjpdovq3XkdAjKoIVNCRRlbrFQG0HkVuqBAxrJnWFNECfGebLrz7mA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.6.3.tgz", + "integrity": "sha512-CzTmpDxwkoYl69stmlJzcVWITQEC6Vs8ASMZMEMbFO+q1Dw0GtpRjAA6X76zGcLOADDwzugx1vpT6YXarrhpTA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.6.1.tgz", - "integrity": "sha512-bpGw2JV9NN1zKt/jXpOB+U9AdqdcPdqA2tF8Or6axNoOl3gBtSaooEYx17NpQra33Wx/d7VX8jWv+3LX1dggJA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.6.3.tgz", + "integrity": "sha512-j+n1gWkfu4Q/octUHXU1p1IOrh+B27vpA7ec81RB6nXCml5u7F0B7SrCZU+HqajxjVqgEQEYOcRCb1yzfwfsWw==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.6.1.tgz", - "integrity": "sha512-uNnVmvDLZBDQ4sLFNTugTtzUH9LEoHXG46HdWaih+pK4knwi+wcz+nd0fQC92n/dH4PwPc5T8XArvlCKpfU6vQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.6.3.tgz", + "integrity": "sha512-n33drkd84G5Mu2BkUGawZXmm+IFPuRv7GpODfwEBs/CzZq2+BIZyAZmb03H9IgNbd7xaohZbtZ4/9Gb0xo5ssw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1651,7 +3016,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1664,7 +3028,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1673,48 +3036,27 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -1722,7 +3064,6 @@ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -1739,7 +3080,6 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1760,7 +3100,6 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1770,7 +3109,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1792,7 +3130,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1811,7 +3148,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1830,7 +3166,6 @@ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -1851,7 +3186,6 @@ "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -1864,7 +3198,6 @@ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1873,7 +3206,6 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.27.tgz", "integrity": "sha512-2l7YlsHXxjmq4o3nSpvH4eav21+ZlUYOflj9TQsPBtzt99v+1Xsy8RvU5+x/OdkFTej40ws3AaKMgT/Ssj+LhQ==", - "license": "MIT", "engines": { "node": ">= 0.14.0", "npm": ">= 1.0.0" @@ -1888,7 +3220,6 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1903,30 +3234,24 @@ "version": "4.10.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "license": "MPL-2.0", "engines": { "node": ">=4" } }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { @@ -1935,8 +3260,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -1949,7 +3272,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", "engines": { "node": "*" } @@ -1959,7 +3281,6 @@ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -1978,7 +3299,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1992,7 +3312,6 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2006,8 +3325,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { "node": ">=6" @@ -2015,8 +3332,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -2030,10 +3345,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/color-convert": { + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2043,31 +3370,44 @@ "node": ">=7.0.0" } }, - "node_modules/color-name": { + "node_modules/chalk/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -2079,7 +3419,6 @@ "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -2103,8 +3442,6 @@ }, "node_modules/cross-env": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" @@ -2121,8 +3458,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2137,14 +3472,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", "engines": { "node": ">= 14" } @@ -2154,7 +3487,6 @@ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2172,7 +3504,6 @@ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2190,7 +3521,6 @@ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -2205,14 +3535,10 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.3.7", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2228,15 +3554,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2247,7 +3569,6 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2265,7 +3586,6 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2282,7 +3602,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -2297,7 +3616,6 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2310,7 +3628,6 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2322,7 +3639,6 @@ "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2335,7 +3651,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2349,7 +3664,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -2358,7 +3672,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -2368,7 +3681,6 @@ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -2434,7 +3746,6 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2444,7 +3755,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2454,7 +3764,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2467,7 +3776,6 @@ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2483,7 +3791,6 @@ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2496,7 +3803,6 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -2511,8 +3817,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -2526,7 +3830,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -2548,7 +3851,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.0.tgz", "integrity": "sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2609,7 +3911,6 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -2621,7 +3922,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2631,7 +3931,6 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.3.tgz", "integrity": "sha512-mBgGvAG+3NGx2yk8w/qiBDOrQNwNe0LfxNzimnj0B7lqElJUV12X+1wf81oERAKpPVl506z53Xi1sns4/pvdTg==", "dev": true, - "license": "ISC", "dependencies": { "debug": "^4.4.0", "get-tsconfig": "^4.10.0", @@ -2660,12 +3959,28 @@ } } }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2683,7 +3998,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2693,7 +4007,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", "dev": true, - "license": "MIT", "peerDependencies": { "eslint": ">=7.7.0" } @@ -2703,7 +4016,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, - "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -2737,7 +4049,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2748,7 +4059,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2758,7 +4068,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2771,7 +4080,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -2781,7 +4089,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2795,8 +4102,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2806,10 +4111,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2819,8 +4137,6 @@ }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2830,10 +4146,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2848,7 +4167,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -2866,7 +4184,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2878,7 +4195,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2889,8 +4205,6 @@ }, "node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2905,7 +4219,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2915,8 +4228,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2926,13 +4237,10 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -2942,7 +4250,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -2960,14 +4267,10 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2982,8 +4285,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2994,15 +4295,11 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, @@ -3019,13 +4316,10 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.18.0", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -3035,15 +4329,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3055,8 +4346,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3067,8 +4356,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -3084,8 +4371,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -3097,9 +4382,7 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.2", "dev": true, "license": "ISC" }, @@ -3108,7 +4391,6 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -3124,7 +4406,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -3137,15 +4418,13 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3159,7 +4438,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3169,7 +4447,6 @@ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3190,7 +4467,6 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3200,7 +4476,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3225,7 +4500,6 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3238,7 +4512,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -3254,7 +4527,6 @@ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3272,7 +4544,6 @@ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -3284,7 +4555,6 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -3299,7 +4569,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3317,8 +4586,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -3332,7 +4599,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3342,7 +4608,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3355,7 +4620,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -3368,7 +4632,6 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3385,7 +4648,6 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", "dev": true, - "license": "MIT", "dependencies": { "@types/glob": "^7.1.1", "array-union": "^2.1.0", @@ -3405,7 +4667,6 @@ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3417,13 +4678,10 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, @@ -3432,7 +4690,6 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3440,22 +4697,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3468,7 +4714,6 @@ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -3484,7 +4729,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3497,7 +4741,6 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3513,7 +4756,6 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3525,7 +4767,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -3538,7 +4779,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -3549,8 +4789,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3561,7 +4799,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", "dependencies": { "queue": "6.0.2" }, @@ -3573,9 +4810,7 @@ } }, "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "version": "3.3.0", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3590,8 +4825,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -3603,7 +4836,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3612,15 +4844,13 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -3634,7 +4864,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -3648,7 +4877,6 @@ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3664,15 +4892,13 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, - "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -3692,7 +4918,6 @@ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -3708,7 +4933,6 @@ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3725,7 +4949,6 @@ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, - "license": "MIT", "dependencies": { "semver": "^7.7.1" } @@ -3735,7 +4958,6 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3748,7 +4970,6 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -3764,7 +4985,6 @@ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -3782,7 +5002,6 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -3796,8 +5015,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3808,7 +5025,6 @@ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3824,7 +5040,6 @@ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -3840,8 +5055,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3855,7 +5068,6 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3865,8 +5077,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -3877,7 +5087,6 @@ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3894,7 +5103,6 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3904,7 +5112,6 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -3923,7 +5130,6 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3936,7 +5142,6 @@ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3952,7 +5157,6 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3969,7 +5173,6 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -3987,7 +5190,6 @@ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -4003,7 +5205,6 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4016,7 +5217,6 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4032,7 +5232,6 @@ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -4048,25 +5247,19 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4078,33 +5271,25 @@ "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, @@ -4113,7 +5298,6 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -4126,15 +5310,12 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -4143,8 +5324,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4158,13 +5337,10 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -4179,8 +5355,6 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, @@ -4189,15 +5363,12 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -4205,8 +5376,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -4220,7 +5389,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4229,7 +5397,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, @@ -4239,8 +5406,6 @@ }, "node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -4258,15 +5423,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/napi-postinstall": { @@ -4274,7 +5436,6 @@ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.1.5.tgz", "integrity": "sha512-HI5bHONOUYqV+FJvueOSgjRxHTLB25a3xIv59ugAxFe7xRNbW96hyYbMbsKzl+QvFV9mN/SrtHwiU+vYhMwA7Q==", "dev": true, - "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" }, @@ -4287,8 +5448,6 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -4296,7 +5455,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -4306,7 +5464,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4319,7 +5476,6 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4329,7 +5485,6 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4350,7 +5505,6 @@ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4369,7 +5523,6 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4384,7 +5537,6 @@ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4402,15 +5554,12 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -4430,7 +5579,6 @@ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, - "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -4445,8 +5593,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4461,8 +5607,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -4479,7 +5623,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -4498,7 +5641,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -4510,13 +5652,10 @@ "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -4529,7 +5668,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -4545,8 +5683,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -4557,15 +5693,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -4575,20 +5708,17 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", "engines": { "node": ">=8" } @@ -4596,19 +5726,15 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -4621,7 +5747,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "license": "Apache-2.0", "dependencies": { "playwright-core": "1.52.0" }, @@ -4639,7 +5764,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -4652,15 +5776,12 @@ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -4672,7 +5793,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -4687,7 +5807,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -4695,8 +5814,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -4707,15 +5824,12 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", "dependencies": { "inherits": "~2.0.3" } }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -4737,7 +5851,6 @@ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4760,7 +5873,6 @@ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4780,7 +5892,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4790,7 +5901,6 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, - "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -4808,8 +5918,6 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { "node": ">=4" @@ -4820,15 +5928,12 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "version": "1.0.4", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -4840,7 +5945,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4856,7 +5960,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.7" }, @@ -4896,7 +5999,6 @@ "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", "dev": true, - "license": "MIT", "dependencies": { "@types/fs-extra": "^8.0.1", "colorette": "^1.1.0", @@ -4910,8 +6012,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -4936,7 +6036,6 @@ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -4968,15 +6067,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -4993,7 +6090,6 @@ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5011,7 +6107,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5024,7 +6119,6 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5042,7 +6136,6 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5058,7 +6151,6 @@ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -5070,8 +6162,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5082,8 +6172,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -5094,7 +6182,6 @@ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -5114,7 +6201,6 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -5131,7 +6217,6 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5150,7 +6235,6 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5170,7 +6254,6 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -5179,7 +6262,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -5189,7 +6271,6 @@ "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", - "license": "MIT", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -5203,7 +6284,6 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -5217,7 +6297,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -5226,22 +6305,19 @@ "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5263,7 +6339,6 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5282,7 +6357,6 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5300,7 +6374,6 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -5310,7 +6383,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -5318,25 +6390,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5348,7 +6406,6 @@ "version": "5.25.11", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.25.11.tgz", "integrity": "sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==", - "license": "MIT", "os": [ "darwin", "linux", @@ -5375,7 +6432,6 @@ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, - "license": "MIT", "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" @@ -5392,7 +6448,6 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, - "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5407,7 +6462,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -5417,8 +6471,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -5432,7 +6484,6 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -5445,7 +6496,6 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -5456,13 +6506,10 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -5477,7 +6524,6 @@ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -5492,7 +6538,6 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -5512,7 +6557,6 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -5534,7 +6578,6 @@ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -5555,7 +6598,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5569,7 +6611,6 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -5587,26 +6628,23 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, - "license": "MIT" + "devOptional": true }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/unrs-resolver": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.6.1.tgz", - "integrity": "sha512-PLDI7BRVaI1C0x8mXr8leLPIOPPF1wCRFyKIswJAPJG3LdMxWNiAVvlTvmff5DSezapWFLagk18NF2cCNhe8Fg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.6.3.tgz", + "integrity": "sha512-mYNIMmxlDcaepmUTNrBu2tEB/bRkLBUeAhke8XOnXYqSu/9dUk4cdFiJG1N4d5Q7Fii+9MpgavkxJpnXPqNhHw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "napi-postinstall": "^0.1.1" }, @@ -5614,28 +6652,26 @@ "url": "https://github.com/sponsors/JounQin" }, "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.6.1", - "@unrs/resolver-binding-darwin-x64": "1.6.1", - "@unrs/resolver-binding-freebsd-x64": "1.6.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.6.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.6.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.6.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.6.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.6.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.6.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.6.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.6.1", - "@unrs/resolver-binding-linux-x64-musl": "1.6.1", - "@unrs/resolver-binding-wasm32-wasi": "1.6.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.6.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.6.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.6.1" + "@unrs/resolver-binding-darwin-arm64": "1.6.3", + "@unrs/resolver-binding-darwin-x64": "1.6.3", + "@unrs/resolver-binding-freebsd-x64": "1.6.3", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.6.3", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.6.3", + "@unrs/resolver-binding-linux-arm64-gnu": "1.6.3", + "@unrs/resolver-binding-linux-arm64-musl": "1.6.3", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.6.3", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.6.3", + "@unrs/resolver-binding-linux-s390x-gnu": "1.6.3", + "@unrs/resolver-binding-linux-x64-gnu": "1.6.3", + "@unrs/resolver-binding-linux-x64-musl": "1.6.3", + "@unrs/resolver-binding-wasm32-wasi": "1.6.3", + "@unrs/resolver-binding-win32-arm64-msvc": "1.6.3", + "@unrs/resolver-binding-win32-ia32-msvc": "1.6.3", + "@unrs/resolver-binding-win32-x64-msvc": "1.6.3" } }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5650,15 +6686,12 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5675,7 +6708,6 @@ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -5695,7 +6727,6 @@ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -5723,7 +6754,6 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, - "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -5742,7 +6772,6 @@ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -5761,8 +6790,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -5772,14 +6799,12 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -5800,7 +6825,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -5812,7 +6836,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -5820,8 +6843,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -5835,7 +6856,6 @@ "version": "3.24.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts b/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts new file mode 100644 index 00000000000..7bdcad635f8 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; +import {getRandomId} from 'utils/utils'; + +test('Should show channel banner when configured', async ({pw}) => { + const {adminUser, adminClient} = await pw.initSetup(); + const license = await adminClient.getClientLicenseOld(); + test.skip(license.SkuShortName !== 'premium', 'Skipping test - server does not have Premium license'); + + const {channelsPage} = await pw.testBrowser.login(adminUser); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + await channelsPage.newChannel(getRandomId(), 'O'); + + let settingsModal = await channelsPage.openChannelSettings(); + let configurationTab = await settingsModal.openConfigurationTab(); + + await configurationTab.enableChannelBanner(); + await configurationTab.setChannelBannerText('Example channel banner text'); + await configurationTab.setChannelBannerTextColor('#77DD88'); + + await configurationTab.save(); + await settingsModal.closeModal(); + + await channelsPage.centerView.assertChannelBanner('Example channel banner text', '#77DD88'); + + // Now we'll disable the channel banner + settingsModal = await channelsPage.openChannelSettings(); + configurationTab = await settingsModal.openConfigurationTab(); + await configurationTab.disableChannelBanner(); + + await configurationTab.save(); + await settingsModal.closeModal(); + + await channelsPage.centerView.assertChannelBannerNotVisible(); + + // re-enabling channel banner should already have + // the previously configured text and color + settingsModal = await channelsPage.openChannelSettings(); + configurationTab = await settingsModal.openConfigurationTab(); + await configurationTab.enableChannelBanner(); + + await configurationTab.save(); + await settingsModal.closeModal(); + + await channelsPage.centerView.assertChannelBanner('Example channel banner text', '#77DD88'); +}); + +test('Should render markdown', async ({pw}) => { + const {adminUser, adminClient} = await pw.initSetup(); + const license = await adminClient.getClientLicenseOld(); + test.skip(license.SkuShortName !== 'premium', 'Skipping test - server does not have Premium license'); + + const {channelsPage} = await pw.testBrowser.login(adminUser); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + await channelsPage.newChannel(getRandomId(), 'O'); + + const settingsModal = await channelsPage.openChannelSettings(); + const configurationTab = await settingsModal.openConfigurationTab(); + + await configurationTab.enableChannelBanner(); + await configurationTab.setChannelBannerText('**bold** *italic* ~~strikethrough~~'); + await configurationTab.setChannelBannerTextColor('#77DD88'); + + await configurationTab.save(); + await settingsModal.closeModal(); + + await channelsPage.centerView.assertChannelBannerHasBoldText('bold'); + await channelsPage.centerView.assertChannelBannerHasItalicText('italic'); + await channelsPage.centerView.assertChannelBannerHasStrikethroughText('strikethrough'); +}); diff --git a/e2e-tests/playwright/utils/utils.ts b/e2e-tests/playwright/utils/utils.ts new file mode 100644 index 00000000000..806b851b815 --- /dev/null +++ b/e2e-tests/playwright/utils/utils.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {v4 as uuidv4} from 'uuid'; + +export function getRandomId(length = 7): string { + const MAX_SUBSTRING_INDEX = 27; + + return uuidv4() + .replace(/-/g, '') + .substring(MAX_SUBSTRING_INDEX - length, MAX_SUBSTRING_INDEX); +} diff --git a/server/channels/api4/channel.go b/server/channels/api4/channel.go index 16f91dc272b..521e60d17bb 100644 --- a/server/channels/api4/channel.go +++ b/server/channels/api4/channel.go @@ -2461,7 +2461,7 @@ func convertGroupMessageToChannel(c *Context, w http.ResponseWriter, r *http.Req func canEditChannelBanner(license *model.License, originalChannel *model.Channel) *model.AppError { if !model.MinimumPremiumLicense(license) { - return model.NewAppError("", "license_error.feature_unavailable", nil, "feature is not available for the current license", http.StatusForbidden) + return model.NewAppError("", "license_error.feature_unavailable.specific", map[string]any{"Feature": "Channel Banner"}, "feature is not available for the current license", http.StatusForbidden) } if originalChannel.Type != model.ChannelTypeOpen && originalChannel.Type != model.ChannelTypePrivate { diff --git a/server/channels/api4/channel_test.go b/server/channels/api4/channel_test.go index 8dbfdc10628..548453daeb4 100644 --- a/server/channels/api4/channel_test.go +++ b/server/channels/api4/channel_test.go @@ -5747,7 +5747,7 @@ func TestCanEditChannelBanner(t *testing.T) { err := canEditChannelBanner(nil, channel) require.NotNil(t, err) - assert.Equal(t, "license_error.feature_unavailable", err.Id) + assert.Equal(t, "license_error.feature_unavailable.specific", err.Id) assert.Equal(t, http.StatusForbidden, err.StatusCode) }) @@ -5760,7 +5760,7 @@ func TestCanEditChannelBanner(t *testing.T) { err := canEditChannelBanner(license, channel) require.NotNil(t, err) - assert.Equal(t, "license_error.feature_unavailable", err.Id) + assert.Equal(t, "license_error.feature_unavailable.specific", err.Id) assert.Equal(t, http.StatusForbidden, err.StatusCode) }) diff --git a/server/i18n/en.json b/server/i18n/en.json index 50137a384c7..983eca27889 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -8472,6 +8472,10 @@ "id": "license_error.feature_unavailable", "translation": "Feature is not available for the current license" }, + { + "id": "license_error.feature_unavailable.specific", + "translation": "{{.Feature}} feature is not available for the current license" + }, { "id": "manaultesting.manual_test.parse.app_error", "translation": "Unable to parse URL." diff --git a/server/public/model/channel.go b/server/public/model/channel.go index a362ed6b5c1..3693e3697d0 100644 --- a/server/public/model/channel.go +++ b/server/public/model/channel.go @@ -139,6 +139,7 @@ type ChannelPatch struct { Header *string `json:"header"` Purpose *string `json:"purpose"` GroupConstrained *bool `json:"group_constrained"` + Type ChannelType `json:"type"` BannerInfo *ChannelBannerInfo `json:"banner_info"` } diff --git a/webapp/channels/src/actions/views/channel.ts b/webapp/channels/src/actions/views/channel.ts index d6e8f79ce27..48826608813 100644 --- a/webapp/channels/src/actions/views/channel.ts +++ b/webapp/channels/src/actions/views/channel.ts @@ -32,6 +32,7 @@ import { isManuallyUnread, getCurrentChannelId, } from 'mattermost-redux/selectors/entities/channels'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getMostRecentPostIdInChannel, getPost} from 'mattermost-redux/selectors/entities/posts'; import { getCurrentRelativeTeamUrl, @@ -49,7 +50,7 @@ import {openDirectChannelToUserId} from 'actions/channel_actions'; import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions'; import {closeRightHandSide} from 'actions/views/rhs'; import {markThreadAsRead} from 'actions/views/threads'; -import {getLastViewedChannelName} from 'selectors/local_storage'; +import {getLastViewedChannelName, getPenultimateViewedChannelName} from 'selectors/local_storage'; import {getSelectedPost, getSelectedPostId} from 'selectors/rhs'; import {getLastPostsApiTimeForChannel} from 'selectors/views/channel'; import {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads'; @@ -59,6 +60,7 @@ import LocalStorageStore from 'stores/local_storage_store'; import {getHistory} from 'utils/browser_history'; import {isArchivedChannel} from 'utils/channel_utils'; import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants'; +import {stopTryNotificationRing} from 'utils/notification_sounds'; import type {ActionFuncAsync, ThunkActionFunc} from 'types/store'; @@ -524,18 +526,42 @@ export function updateToastStatus(status: boolean) { export function deleteChannel(channelId: string): ActionFuncAsync { return async (dispatch, getState) => { + // Get state before deletion + const state = getState(); + const channel = getChannel(state, channelId); + + // Validate channel ID + if (!channel || channel.id.length !== Constants.CHANNEL_ID_LENGTH) { + return {data: false}; + } + + const canViewArchivedChannels = getConfig(state).ExperimentalViewArchivedChannels === 'true'; + const currentTeamDetails = getCurrentTeam(state); + const penultimateViewedChannelName = getPenultimateViewedChannelName(state) || + getRedirectChannelNameForTeam(state, getCurrentTeamId(state)); + + // Handle redirection before deletion if needed + if (!canViewArchivedChannels && penultimateViewedChannelName && currentTeamDetails) { + getHistory().push('/' + currentTeamDetails.name + '/channels/' + penultimateViewedChannelName); + } + + // Call the delete channel action const res = await dispatch(deleteChannelRedux(channelId)); if (res.error) { return {data: false}; } - const state = getState(); - const selectedPost = getSelectedPost(state); - const selectedPostId = getSelectedPostId(state); + // Handle RHS state + const updatedState = getState(); + const selectedPost = getSelectedPost(updatedState); + const selectedPostId = getSelectedPostId(updatedState); if (selectedPostId && !selectedPost.exists) { dispatch(closeRightHandSide()); } + // Stop notification sounds + stopTryNotificationRing(); + return {data: true}; }; } diff --git a/webapp/channels/src/actions/views/textbox.js b/webapp/channels/src/actions/views/textbox.js index 49ac36a0f6b..a014d79075b 100644 --- a/webapp/channels/src/actions/views/textbox.js +++ b/webapp/channels/src/actions/views/textbox.js @@ -23,3 +23,17 @@ export function setShowPreviewOnEditChannelHeaderModal(showPreview) { showPreview, }; } + +export function setShowPreviewOnChannelSettingsHeaderModal(showPreview) { + return { + type: ActionTypes.SET_SHOW_PREVIEW_ON_CHANNEL_SETTINGS_HEADER_MODAL, + showPreview, + }; +} + +export function setShowPreviewOnChannelSettingsPurposeModal(showPreview) { + return { + type: ActionTypes.SET_SHOW_PREVIEW_ON_CHANNEL_SETTINGS_PURPOSE_MODAL, + showPreview, + }; +} diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx index eec928f3b3c..daa9e29f621 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx @@ -13,7 +13,7 @@ import type {FileUpload} from 'components/file_upload/file_upload'; import type Textbox from 'components/textbox/textbox'; import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; -import {renderWithContext, userEvent, screen} from 'tests/react_testing_utils'; +import {renderWithContext, userEvent, screen, act} from 'tests/react_testing_utils'; import Constants, {Locations, StoragePrefixes} from 'utils/constants'; import {TestHelper} from 'utils/test_helper'; @@ -184,7 +184,7 @@ const baseProps = { describe('components/avanced_text_editor/advanced_text_editor', () => { describe('keyDown behavior', () => { - it('ESC should blur the input', () => { + it('ESC should blur the input', async () => { renderWithContext( { }), ); const textbox = screen.getByTestId('post_textbox'); - userEvent.type(textbox, 'something{esc}'); + + await act(async () => { + userEvent.type(textbox, 'something{esc}'); + }); + expect(textbox).not.toHaveFocus(); expect(mockedUpdateDraft).not.toHaveBeenCalled(); }); - it('ESC should blur the input and reset draft when in editing mode', () => { + it('ESC should blur the input and reset draft when in editing mode', async () => { jest.useFakeTimers(); const props = { ...baseProps, @@ -226,7 +230,9 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { }), ); const textbox = screen.getByTestId('edit_textbox'); - userEvent.type(textbox, 'something{esc}'); + await act(async () => { + userEvent.type(textbox, 'something{esc}'); + }); expect(textbox).not.toHaveFocus(); // save is called with a short delayed after pressing escape key @@ -236,7 +242,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { }); }); - it('should set the textbox value to an existing draft on mount and when changing channels', () => { + it('should set the textbox value to an existing draft on mount and when changing channels', async () => { const {rerender} = renderWithContext( { expect(screen.getByPlaceholderText('Write to Test Channel')).toHaveValue('original draft'); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(screen.getByPlaceholderText('Write to Other Channel')).toHaveValue('a different draft'); }); - it('should save a new draft when changing channels', () => { + it('should save a new draft when changing channels', async () => { const {rerender} = renderWithContext( { initialState, ); - userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text'); + await act(async () => { + userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text'); + }); expect(mockedUpdateDraft).not.toHaveBeenCalled(); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(mockedUpdateDraft).toHaveBeenCalled(); expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({ @@ -296,7 +308,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { }); }); - it('MM-60541 should not save an unmodified draft when changing channels', () => { + it('MM-60541 should not save an unmodified draft when changing channels', async () => { const {rerender} = renderWithContext( { expect(mockedUpdateDraft).not.toHaveBeenCalled(); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(mockedUpdateDraft).not.toHaveBeenCalled(); }); - it('should save an updated draft when changing channels', () => { + it('should save an updated draft when changing channels', async () => { const {rerender} = renderWithContext( { }), ); - userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text'); + await act(async () => { + userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text'); + }); expect(mockedUpdateDraft).not.toHaveBeenCalled(); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(mockedUpdateDraft).toHaveBeenCalled(); expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({ @@ -362,7 +380,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { }); }); - it('should deleted a deleted draft when changing channels', () => { + it('should deleted a deleted draft when changing channels', async () => { const {rerender} = renderWithContext( { }), ); - userEvent.clear(screen.getByPlaceholderText('Write to Test Channel')); + await act(async () => { + userEvent.clear(screen.getByPlaceholderText('Write to Test Channel')); + }); expect(mockedRemoveDraft).not.toHaveBeenCalled(); expect(mockedUpdateDraft).not.toHaveBeenCalled(); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(mockedRemoveDraft).toHaveBeenCalled(); expect(mockedUpdateDraft).not.toHaveBeenCalled(); }); - it('MM-60541 should not attempt to delete a non-existent draft when changing channels', () => { + it('MM-60541 should not attempt to delete a non-existent draft when changing channels', async () => { const {rerender} = renderWithContext( { expect(mockedRemoveDraft).not.toHaveBeenCalled(); expect(mockedUpdateDraft).not.toHaveBeenCalled(); - rerender( - , - ); + await act(async () => { + rerender( + , + ); + }); expect(mockedRemoveDraft).not.toHaveBeenCalled(); expect(mockedUpdateDraft).not.toHaveBeenCalled(); diff --git a/webapp/channels/src/components/channel_banner/channel_banner.test.tsx b/webapp/channels/src/components/channel_banner/channel_banner.test.tsx index db9a5bb7302..b35449c30c5 100644 --- a/webapp/channels/src/components/channel_banner/channel_banner.test.tsx +++ b/webapp/channels/src/components/channel_banner/channel_banner.test.tsx @@ -10,7 +10,7 @@ import {renderWithContext} from 'tests/react_testing_utils'; import {LicenseSkus, Constants} from 'utils/constants'; import {TestHelper} from 'utils/test_helper'; -import ChannelBanner from './index'; +import ChannelBanner from './channel_banner'; describe('components/channel_banner', () => { const channel1 = TestHelper.getChannelMock({ diff --git a/webapp/channels/src/components/channel_banner/index.tsx b/webapp/channels/src/components/channel_banner/channel_banner.tsx similarity index 81% rename from webapp/channels/src/components/channel_banner/index.tsx rename to webapp/channels/src/components/channel_banner/channel_banner.tsx index f4b9542171b..9b94f7b150e 100644 --- a/webapp/channels/src/components/channel_banner/index.tsx +++ b/webapp/channels/src/components/channel_banner/channel_banner.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo, useRef} from 'react'; import {useIntl} from 'react-intl'; import {useSelector} from 'react-redux'; @@ -30,6 +30,19 @@ type Props = { export default function ChannelBanner({channelId}: Props) { const channelBannerInfo = useSelector((state: GlobalState) => getChannelBanner(state, channelId)); const showChannelBanner = useSelector((state: GlobalState) => selectShowChannelBanner(state, channelId)); + const textContainerRef = useRef(null); + const [tooltipNeeded, setTooltipNeeded] = React.useState(false); + + useEffect(() => { + if (!textContainerRef.current) { + return; + } + + const isOverflowingHorizontally = textContainerRef.current.offsetWidth < textContainerRef.current.scrollWidth; + const isOverflowingVertically = textContainerRef.current.offsetHeight < textContainerRef.current.scrollHeight; + + setTooltipNeeded(isOverflowingHorizontally || isOverflowingVertically); + }, [channelBannerInfo?.text]); const intl = useIntl(); const channelBannerTextAriaLabel = intl.formatMessage({id: 'channel_banner.aria_label', defaultMessage: 'Channel banner text'}); @@ -75,6 +88,7 @@ export default function ChannelBanner({channelId}: Props) { className='channelBannerTooltip' delayClose={true} forcedPlacement='bottom' + disabled={!tooltipNeeded} >
{content} diff --git a/webapp/channels/src/components/channel_banner/style.scss b/webapp/channels/src/components/channel_banner/style.scss index bd072073967..d2d6f3fe887 100644 --- a/webapp/channels/src/components/channel_banner/style.scss +++ b/webapp/channels/src/components/channel_banner/style.scss @@ -5,7 +5,6 @@ width: 100%; min-height: variables.$announcement-bar-height; max-height: variables.$announcement-bar-height; - align-items: center; justify-content: center; padding-block: 6px; padding-inline: 24px; @@ -18,18 +17,38 @@ text-align: center; text-overflow: ellipsis; + &:has(p:first-child) { + margin-top: 3px; + } + a { color: var(--channel-banner-text-color); text-decoration: underline; } + + .markdown__heading { + overflow: hidden; + margin: 2px; + font-size: 18px; + text-overflow: ellipsis; + } + + :first-child { + overflow: hidden; + text-overflow: ellipsis; + } + + .markdown__table { + background: unset; + } } } .channelBannerTooltip { + top: -2px !important; overflow: auto; min-width: 400px; max-width: min(1000px, 70vw); max-height: 80vh; pointer-events: auto; } - diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx index 23c0c665241..b04f5448e58 100644 --- a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx @@ -19,7 +19,7 @@ import {Constants} from 'utils/constants'; import MenuItemArchiveChannel from '../menu_items/archive_channel'; import MenuItemChannelBookmarks from '../menu_items/channel_bookmarks_submenu'; -import MenuItemChannelSettings from '../menu_items/channel_settings_submenu'; +import MenuItemChannelSettings from '../menu_items/channel_settings_menu'; import MenuItemCloseChannel from '../menu_items/close_channel'; import MenuItemGroupsMenuItems from '../menu_items/groups'; import MenuItemLeaveChannel from '../menu_items/leave_channel'; @@ -44,7 +44,7 @@ interface Props extends Menu.FirstMenuItemProps { pluginItems: ReactNode[]; } -const ChannelHeaderPublicMenu = ({channel, user, isMuted, isReadonly, isDefault, isMobile, isFavorite, isLicensedForLDAPGroups, pluginItems, ...rest}: Props) => { +const ChannelHeaderPublicMenu = ({channel, user, isMuted, isDefault, isMobile, isFavorite, isLicensedForLDAPGroups, pluginItems, ...rest}: Props) => { const isGroupConstrained = channel?.group_constrained === true; const isArchived = channel.delete_at !== 0; const isPrivate = channel?.type === Constants.PRIVATE_CHANNEL; @@ -71,8 +71,6 @@ const ChannelHeaderPublicMenu = ({channel, user, isMuted, isReadonly, isDefault, channel={channel} /> { + const dispatch = useDispatch(); + + const handleOpenChannelSettings = () => { + dispatch( + openModal({ + modalId: ModalIdentifiers.CHANNEL_SETTINGS, + dialogType: ChannelSettingsModal, + dialogProps: { + channelId: channel.id, + focusOriginElement: 'channelHeaderDropdownButton', + isOpen: true, + }, + }), + ); + }; + + return ( + + } + onClick={handleOpenChannelSettings} + leadingElement={} + /> + ); +}; + +export default memo(ChannelSettingsMenu); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx deleted file mode 100644 index 5ce9472acf0..00000000000 --- a/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {memo} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; -import {useDispatch} from 'react-redux'; - -import { - ChevronRightIcon, - CogOutlineIcon, -} from '@mattermost/compass-icons/components'; -import type {Channel} from '@mattermost/types/channels'; - -import {Permissions} from 'mattermost-redux/constants'; - -import {openModal} from 'actions/views/modals'; - -import ConvertChannelModal from 'components/convert_channel_modal'; -import EditChannelHeaderModal from 'components/edit_channel_header_modal'; -import EditChannelPurposeModal from 'components/edit_channel_purpose_modal'; -import * as Menu from 'components/menu'; -import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; -import RenameChannelModal from 'components/rename_channel_modal'; - -import {Constants, ModalIdentifiers} from 'utils/constants'; - -type Props = { - channel: Channel; - isReadonly: boolean; - isDefault: boolean; -} - -const ChannelSettingsSubmenu = ({channel, isReadonly, isDefault}: Props): JSX.Element => { - const dispatch = useDispatch(); - const {formatMessage} = useIntl(); - const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; - const handleRenameChannel = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.RENAME_CHANNEL, - dialogType: RenameChannelModal, - dialogProps: {channel}, - }), - ); - }; - - const handleEditHeader = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER, - dialogType: EditChannelHeaderModal, - dialogProps: {channel}, - }), - ); - }; - - const handleEditPurpose = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE, - dialogType: EditChannelPurposeModal, - dialogProps: {channel}, - }), - ); - }; - - const handleConvertToPrivate = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.CONVERT_CHANNEL, - dialogType: ConvertChannelModal, - dialogProps: { - channelId: channel.id, - channelDisplayName: channel.display_name, - }, - }), - ); - }; - - return ( - - } - leadingElement={} - trailingElements={} - menuId={'channelSettings-menu'} - menuAriaLabel={formatMessage({id: 'channelSettings', defaultMessage: 'Channel Settings'})} - > - {!isReadonly && ( - - } - /> - )} - - {!isReadonly && ( - - - } - /> - - )} - - {!isReadonly && ( - - - } - /> - - )} - - {!isDefault && channel.type === Constants.OPEN_CHANNEL && ( - - - } - /> - - )} - - ); -}; - -export default memo(ChannelSettingsSubmenu); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx deleted file mode 100644 index 87f0a572ee1..00000000000 --- a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useDispatch} from 'react-redux'; - -import * as modalActions from 'actions/views/modals'; - -import ConvertChannelModal from 'components/convert_channel_modal'; -import {WithTestMenuContext} from 'components/menu/menu_context_test'; - -import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; -import {ModalIdentifiers} from 'utils/constants'; -import {TestHelper} from 'utils/test_helper'; - -import ConvertPublictoPrivate from './convert_public_to_private'; - -describe('components/ChannelHeaderMenu/MenuItems/ConvertPublicToPrivate', () => { - beforeEach(() => { - jest.spyOn(modalActions, 'openModal'); - - // Mock useDispatch to return our custom dispatch function - jest.spyOn(require('react-redux'), 'useDispatch'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - const channel = TestHelper.getChannelMock(); - - test('renders the component correctly, handle click event', () => { - renderWithContext( - - - , {}, - ); - - const menuItem = screen.getByText('Convert to Private Channel'); - expect(menuItem).toBeInTheDocument(); - - fireEvent.click(menuItem); // Simulate click on the menu item - expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called - expect(modalActions.openModal).toHaveBeenCalledTimes(1); - expect(modalActions.openModal).toHaveBeenCalledWith({ - modalId: ModalIdentifiers.CONVERT_CHANNEL, - dialogType: ConvertChannelModal, - dialogProps: { - channelId: channel.id, - channelDisplayName: channel.display_name, - }, - }); - }); -}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx deleted file mode 100644 index 0477a303f09..00000000000 --- a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useDispatch} from 'react-redux'; - -import type {Channel} from '@mattermost/types/channels'; - -import {openModal} from 'actions/views/modals'; - -import ConvertChannelModal from 'components/convert_channel_modal'; -import * as Menu from 'components/menu'; - -import {ModalIdentifiers} from 'utils/constants'; - -type Props = { - channel: Channel; -} - -const ConvertPublictoPrivate = ({channel}: Props): JSX.Element => { - const dispatch = useDispatch(); - const handleConvertToPrivate = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.CONVERT_CHANNEL, - dialogType: ConvertChannelModal, - dialogProps: { - channelId: channel.id, - channelDisplayName: channel.display_name, - }, - }), - ); - }; - - return ( - - } - /> - ); -}; - -export default React.memo(ConvertPublictoPrivate); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx deleted file mode 100644 index fd0e38c8bd6..00000000000 --- a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useDispatch} from 'react-redux'; - -import * as modalActions from 'actions/views/modals'; - -import {WithTestMenuContext} from 'components/menu/menu_context_test'; -import RenameChannelModal from 'components/rename_channel_modal'; - -import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; -import {ModalIdentifiers} from 'utils/constants'; -import {TestHelper} from 'utils/test_helper'; - -import EditChannelSettings from './edit_channel_settings'; - -describe('components/ChannelHeaderMenu/MenuItems/EditChannelSettings', () => { - const channel = TestHelper.getChannelMock(); - beforeEach(() => { - jest.spyOn(modalActions, 'openModal'); - jest.spyOn(require('react-redux'), 'useDispatch'); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('renders the component correctly, handle click event', () => { - renderWithContext( - - - , {}, - ); - - const menuItem = screen.getByText('Rename Channel'); - expect(menuItem).toBeInTheDocument(); - - fireEvent.click(menuItem); // Simulate click on the menu item - expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called - expect(modalActions.openModal).toHaveBeenCalledTimes(1); - expect(modalActions.openModal).toHaveBeenCalledWith({ - modalId: ModalIdentifiers.RENAME_CHANNEL, - dialogType: RenameChannelModal, - dialogProps: {channel}, - }); - }); -}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx deleted file mode 100644 index cd70ccd9b6c..00000000000 --- a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useDispatch} from 'react-redux'; - -import type {Channel} from '@mattermost/types/channels'; - -import {Permissions} from 'mattermost-redux/constants'; - -import {openModal} from 'actions/views/modals'; - -import EditChannelHeaderModal from 'components/edit_channel_header_modal'; -import EditChannelPurposeModal from 'components/edit_channel_purpose_modal'; -import * as Menu from 'components/menu'; -import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; -import RenameChannelModal from 'components/rename_channel_modal'; - -import {Constants, ModalIdentifiers} from 'utils/constants'; - -import MenuItemConvertToPrivate from './convert_public_to_private'; - -type Props = { - channel: Channel; - isReadonly: boolean; - isDefault: boolean; -} - -const EditChannelSettings = ({channel, isReadonly, isDefault}: Props): JSX.Element => { - const dispatch = useDispatch(); - const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; - - const handleRenameChannel = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.RENAME_CHANNEL, - dialogType: RenameChannelModal, - dialogProps: {channel}, - }), - ); - }; - - const handleEditHeader = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER, - dialogType: EditChannelHeaderModal, - dialogProps: {channel}, - }), - ); - }; - - const handleEditPurpose = () => { - dispatch( - openModal({ - modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE, - dialogType: EditChannelPurposeModal, - dialogProps: {channel}, - }), - ); - }; - return ( - <> - {!isReadonly && ( - <> - - } - /> - - - } - /> - - } - /> - - - )} - - {!isDefault && channel.type === Constants.OPEN_CHANNEL && ( - - - - )} - - ); -}; - -export default React.memo(EditChannelSettings); diff --git a/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx b/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx index 9253ce117c7..f4e19898e85 100644 --- a/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx +++ b/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx @@ -24,10 +24,12 @@ export type Props = { placeholder: string; onDisplayNameChange: (name: string) => void; onURLChange: (url: string) => void; + currentUrl?: string; autoFocus?: boolean; - onErrorStateChange?: (isError: boolean) => void; + onErrorStateChange?: (isError: boolean, errorMessage?: string) => void; team?: Team; urlError?: string; + readOnly?: boolean; } import './channel_name_form_field.scss'; @@ -54,14 +56,20 @@ const ChannelNameFormField = (props: Props): JSX.Element => { const intl = useIntl(); const {formatMessage} = intl; - const displayNameModified = useRef(false); + // Track if the field has been interacted with + const [hasInteracted, setHasInteracted] = useState(false); const [displayNameError, setDisplayNameError] = useState(''); const displayName = useRef(''); const urlModified = useRef(false); - const [url, setURL] = useState(''); + const [url, setURL] = useState(props.currentUrl || ''); const [urlError, setURLError] = useState(''); const [inputCustomMessage, setInputCustomMessage] = useState(null); + // Initialize displayName.current with props.value when component mounts + useEffect(() => { + displayName.current = props.value; + }, [props.value]); + const currentTeamName = useSelector(getCurrentTeam)?.name; const teamName = props.team ? props.team.name : currentTeamName; @@ -69,10 +77,27 @@ const ChannelNameFormField = (props: Props): JSX.Element => { e.preventDefault(); const {target: {value: updatedDisplayName}} = e; - const displayNameErrors = validateDisplayName(intl, updatedDisplayName); + // Mark as interacted when user types + if (!hasInteracted && updatedDisplayName.trim() !== '') { + setHasInteracted(true); + } + + // Only validate if the user has interacted with the field + if (hasInteracted) { + const displayNameErrors = validateDisplayName(intl, updatedDisplayName); + + if (displayNameErrors.length) { + setDisplayNameError(displayNameErrors[displayNameErrors.length - 1]); + setInputCustomMessage({ + type: 'error', + value: displayNameErrors[displayNameErrors.length - 1], + }); + } else { + setDisplayNameError(''); + setInputCustomMessage(null); + } + } - // set error if any, else clear it - setDisplayNameError(displayNameErrors.length ? displayNameErrors[displayNameErrors.length - 1] : ''); displayName.current = updatedDisplayName; props.onDisplayNameChange(updatedDisplayName); @@ -83,19 +108,32 @@ const ChannelNameFormField = (props: Props): JSX.Element => { setURLError(''); props.onURLChange(cleanURL); } - }, [props.onDisplayNameChange, props.onURLChange]); + }, [props.onDisplayNameChange, props.onURLChange, hasInteracted, intl]); const handleOnDisplayNameBlur = useCallback(() => { + // Always mark as interacted on blur + setHasInteracted(true); + + // Validate on blur - always show errors on blur regardless of interaction state + const displayNameErrors = validateDisplayName(intl, displayName.current); + setDisplayNameError(displayNameErrors.length ? displayNameErrors[displayNameErrors.length - 1] : ''); + + if (displayNameErrors.length) { + setInputCustomMessage({ + type: 'error', + value: displayNameErrors[displayNameErrors.length - 1], + }); + } else { + setInputCustomMessage(null); + } + + // Handle URL generation if needed if (displayName.current && !url) { const url = generateSlug(); setURL(url); props.onURLChange(url); } - if (!displayNameModified.current) { - displayNameModified.current = true; - setInputCustomMessage(null); - } - }, [props.onURLChange, displayName.current, url, displayNameModified]); + }, [props.onURLChange, displayName.current, url, intl]); const handleOnURLChange = useCallback((e: React.ChangeEvent) => { e.preventDefault(); @@ -110,12 +148,41 @@ const ChannelNameFormField = (props: Props): JSX.Element => { props.onURLChange(cleanURL); }, [props.onURLChange]); + // Add a URL blur handler to validate the URL when the user moves away from the field + const handleOnURLBlur = useCallback(() => { + // Only validate if the URL has been modified + if (urlModified.current) { + const urlErrors = validateChannelUrl(url, intl); + let lastError = ''; + if (urlErrors.length && typeof urlErrors[urlErrors.length - 1] === 'string') { + // Safe to assert as string because we're always providing intl to validateChannelUrl and have extra type safe check + lastError = urlErrors[urlErrors.length - 1] as string; + } + setURLError(lastError); + } + }, [url, intl]); + useEffect(() => { + // Only report URL errors if the URL has been explicitly modified and validated + // This prevents showing errors during typing if (props.onErrorStateChange) { - props.onErrorStateChange(Boolean(displayNameError) || Boolean(urlError)); + if (displayNameError) { + props.onErrorStateChange(true, displayNameError); + } else if (urlModified.current && urlError) { + props.onErrorStateChange(true, urlError); + } else { + props.onErrorStateChange(false, ''); + } } }, [displayNameError, urlError]); + // Effect to set URL from props if it's modified (used in modals for reset button event which sets the value outside the onChange event) + useEffect(() => { + if (props.currentUrl) { + setURL(props.currentUrl); + } + }, [props.currentUrl]); + return ( <> { label={formatMessage({id: 'channel_modal.name.label', defaultMessage: 'Channel name'})} placeholder={props.placeholder} limit={Constants.MAX_CHANNELNAME_LENGTH} + + // Only pass minLength after the user has interacted with the field + minLength={hasInteracted ? Constants.MIN_CHANNELNAME_LENGTH : undefined} value={props.value} customMessage={inputCustomMessage} onChange={handleOnDisplayNameChange} onBlur={handleOnDisplayNameBlur} + disabled={props.readOnly} /> { shortenLength={Constants.DEFAULT_CHANNELURL_SHORTEN_LENGTH} error={urlError || props.urlError} onChange={handleOnURLChange} + onBlur={handleOnURLBlur} /> ); diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx index 3309fe4316c..ae8f3d44ea4 100644 --- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx @@ -46,6 +46,9 @@ export type Props = PropsFromRedux & { */ currentUser: UserProfile; + /** + * Id of the element that triggered the modal opening + */ focusOriginElement?: string; }; diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.test.tsx new file mode 100644 index 00000000000..51cb5290a48 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.test.tsx @@ -0,0 +1,200 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import type {ClientConfig} from '@mattermost/types/config'; +import type {Team} from '@mattermost/types/teams'; + +import * as teams from 'mattermost-redux/selectors/entities/teams'; + +import * as channelActions from 'actions/views/channel'; + +import {renderWithContext} from 'tests/react_testing_utils'; +import {TestHelper} from 'utils/test_helper'; + +import ChannelSettingsArchiveTab from './channel_settings_archive_tab'; + +// Mock the redux actions and selectors +jest.mock('actions/views/channel', () => ({ + deleteChannel: jest.fn().mockReturnValue({type: 'MOCK_DELETE_ACTION'}), +})); + +jest.mock('utils/browser_history', () => ({ + getHistory: jest.fn(), +})); + +jest.mock('mattermost-redux/selectors/entities/general', () => ({ + ...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'), + getConfig: () => mockConfig, +})); + +// Mock the roles selector which is a dependency for other selectors +jest.mock('mattermost-redux/selectors/entities/roles', () => ({ + haveITeamPermission: jest.fn().mockReturnValue(true), + haveIChannelPermission: jest.fn().mockReturnValue(true), + getRoles: jest.fn().mockReturnValue({}), +})); + +// Create a mock channel for testing +const mockChannel = TestHelper.getChannelMock({ + id: 'using-26-letter-channel-id', // so channel id validation length works + team_id: 'team1', + display_name: 'Test Channel', + name: 'test-channel', + type: 'O', +}); + +const baseProps = { + channel: mockChannel, + onHide: jest.fn(), +}; + +let mockConfig: Partial; + +describe('ChannelSettingsArchiveTab', () => { + const {getHistory} = require('utils/browser_history'); + beforeEach(() => { + jest.clearAllMocks(); + + mockConfig = { + ExperimentalViewArchivedChannels: 'false', + }; + + jest.spyOn(teams, 'getCurrentTeam').mockReturnValue({ + id: 'team1', + name: 'team-name', + } as Team); + + const historyPush = jest.fn(); + (getHistory as jest.Mock).mockReturnValue({push: historyPush}); + }); + + it('should render the archive button', () => { + renderWithContext(); + + // Check that the archive button is rendered + const archiveButton = screen.getByText('Archive this channel'); + expect(archiveButton).toBeInTheDocument(); + expect(archiveButton).toHaveAttribute('aria-label', 'Archive channel Test Channel'); + }); + + it('should show confirmation modal when archive button is clicked', async () => { + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Check that the confirmation modal is shown + expect(screen.getByTestId('archiveChannelConfirmModal')).toBeInTheDocument(); + expect(screen.getByText('Archive channel?')).toBeInTheDocument(); + }); + + it('should call deleteChannel and onHide when confirmed', async () => { + const onHide = jest.fn(); + + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Click the confirm button in the modal + await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); + + // Check that deleteChannel was called with the channel ID + expect(channelActions.deleteChannel).toHaveBeenCalledWith(mockChannel.id); + + // Check that onHide was called + expect(onHide).toHaveBeenCalled(); + }); + + it('should close the confirmation modal when canceled', async () => { + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Check that the confirmation modal is shown + expect(screen.getByTestId('archiveChannelConfirmModal')).toBeInTheDocument(); + + // Click the cancel button in the modal + await userEvent.click(screen.getByTestId('cancel-button')); + + // Check that the confirmation modal is hidden + await waitFor(() => { + expect(screen.queryByTestId('archiveChannelConfirmModal')).not.toBeInTheDocument(); + }); + }); + + it('should call deleteChannel which handles redirection when archived channel cannot be viewed', async () => { + // Mock the deleteChannel implementation to simulate the action's behavior + const {deleteChannel} = require('actions/views/channel'); + deleteChannel.mockImplementationOnce(() => { + return () => { + // The action would handle redirection internally + return {data: true}; + }; + }); + + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Check that the confirmation modal is shown + expect(screen.getByTestId('archiveChannelConfirmModal')).toBeInTheDocument(); + + // Click the confirm button in the modal + await userEvent.click(screen.getByText('Confirm')); + + // Check that deleteChannel was called with the channel ID + expect(channelActions.deleteChannel).toHaveBeenCalledWith(mockChannel.id); + }); + + it('should show correct message when archived channels cannot be viewed', async () => { + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Check that the confirmation modal message mentions that archived channels cannot be viewed + const modalMessage = screen.getByTestId('archiveChannelConfirmModal'); + expect(modalMessage).toBeInTheDocument(); + + // Check for the specific text content within the modal + // Use the within function to scope the query to just the modal content + const modalBody = screen.getByTestId('archiveChannelConfirmModal').querySelector('#confirmModalBody'); + expect(modalBody).toBeInTheDocument(); + expect(modalBody).toHaveTextContent(/Archiving a channel removes it from the user interface/); + }); + + it('should call deleteChannel which handles channel ID validation', async () => { + // Create a channel with an invalid ID + const invalidChannel = { + ...mockChannel, + id: 'invalid', // Too short to be a valid channel ID + }; + + // Mock the deleteChannel implementation to simulate the action's behavior + const {deleteChannel} = require('actions/views/channel'); + deleteChannel.mockImplementationOnce(() => { + return () => { + // The action would handle validation internally and return false for invalid IDs + return {data: false}; + }; + }); + + renderWithContext(); + + // Click the archive button + await userEvent.click(screen.getByText('Archive this channel')); + + // Click the confirm button in the modal + await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); + + // Check that deleteChannel was called with the invalid channel ID + expect(channelActions.deleteChannel).toHaveBeenCalledWith(invalidChannel.id); + }); +}); diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx new file mode 100644 index 00000000000..6ca639d7f84 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState, useCallback} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; +import {useDispatch, useSelector} from 'react-redux'; + +import type {Channel} from '@mattermost/types/channels'; + +import {getConfig} from 'mattermost-redux/selectors/entities/general'; + +import {deleteChannel} from 'actions/views/channel'; + +import ConfirmationModal from 'components/confirm_modal'; + +import type {GlobalState} from 'types/store'; + +type ChannelSettingsArchiveTabProps = { + channel: Channel; + onHide: () => void; +} + +function ChannelSettingsArchiveTab({ + channel, + onHide, +}: ChannelSettingsArchiveTabProps) { + const {formatMessage} = useIntl(); + const dispatch = useDispatch(); + + // Redux selector + const canViewArchivedChannels = useSelector((state: GlobalState) => getConfig(state).ExperimentalViewArchivedChannels === 'true'); + + const [showArchiveConfirmModal, setShowArchiveConfirmModal] = useState(false); + + const handleArchiveChannel = useCallback(() => { + setShowArchiveConfirmModal(true); + }, []); + + const doArchiveChannel = async () => { + // Call the delete channel action which handles validation, redirection, and notification sounds + await dispatch(deleteChannel(channel.id)); + + // Close the modal + onHide(); + }; + + return ( +
+ + + + {showArchiveConfirmModal && ( + +

+ +

+

+ {chunks}, + }} + /> +

+
+ } + confirmButtonText={formatMessage({id: 'channel_settings.modal.confirmArchive', defaultMessage: 'Confirm'})} + onConfirm={doArchiveChannel} + onCancel={() => setShowArchiveConfirmModal(false)} + confirmButtonClass='btn btn-danger' + modalClass='archiveChannelConfirmModal' + focusOriginElement='channelSettingsArchiveChannelButton' + /> + )} +
+ ); +} + +export default ChannelSettingsArchiveTab; diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.scss b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.scss new file mode 100644 index 00000000000..512855fd0c0 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.scss @@ -0,0 +1,98 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +@import "utils/_animations"; + +.ChannelSettingsModal__configurationTab { + display: flex; + flex-direction: column; + gap: 16px; + + .channel_banner_header { + display: flex; + + .channel_banner_header__text { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + + span { + &.heading { + color: rgb(var(--center-channel-color-rgb)); + font-size: 16px; + } + + &.subheading { + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + } + } + } + } + + .channel_banner_section_body { + @include fade-in; + + display: flex; + width: 100%; + height: 550px; + flex-direction: column; + gap: 16px; + } + + .AdvancedTextbox { + width: 390px; + + #PreviewInputTextButton { + position: absolute; + z-index: 10; + top: 5px; + right: 5px; + } + + #channel_banner_banner_text_textbox { + min-height: 40px; + max-height: 200px; + } + + .textbox-preview-area { + height: max-content !important; + max-height: 200px; + overflow-y: auto; + + .markdown__heading { + overflow: hidden; + margin: 2px; + font-size: 18px; + text-overflow: ellipsis; + } + } + } + + .setting_section { + display: flex; + flex-direction: row; + gap: 12px; + + .color-input.input-group { + width: 160px; + } + + span.setting_title { + flex: 1; + padding-top: 12px; + color: rgb(var(--center-channel-color-rgb)); + font-size: 12px; + font-weight: 600; + } + + .setting_body { + flex: 4; + } + + .AdvancedTextbox { + margin-top: 0 !important; + } + } +} diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx new file mode 100644 index 00000000000..a2445907980 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx @@ -0,0 +1,316 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {act, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import {renderWithContext} from 'tests/react_testing_utils'; +import {TestHelper} from 'utils/test_helper'; + +import ChannelSettingsConfigurationTab from './channel_settings_configuration_tab'; + +// Mock the redux actions and selectors +jest.mock('mattermost-redux/actions/channels', () => ({ + patchChannel: jest.fn(), +})); + +// Mock the ShowFormat component to make it easier to test +jest.mock('components/advanced_text_editor/show_formatting/show_formatting', () => ( + jest.fn().mockImplementation((props) => ( + + )) +)); + +// Create a mock channel for testing +const mockChannel = TestHelper.getChannelMock({ + id: 'channel1', + team_id: 'team1', + display_name: 'Test Channel', + name: 'test-channel', + purpose: 'Testing purpose', + header: 'Initial header', + type: 'O', + banner_info: { + enabled: false, + text: '', + background_color: '', + }, +}); + +// Create a mock channel with banner enabled +const mockChannelWithBanner = TestHelper.getChannelMock({ + id: 'channel1', + team_id: 'team1', + display_name: 'Test Channel', + name: 'test-channel', + purpose: 'Testing purpose', + header: 'Initial header', + type: 'O', + banner_info: { + enabled: true, + text: 'Test banner text', + background_color: '#ff0000', + }, +}); + +const baseProps = { + channel: mockChannel, + setAreThereUnsavedChanges: jest.fn(), +}; + +describe('ChannelSettingsConfigurationTab', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render with the correct initial values when banner is disabled', () => { + renderWithContext(); + + // Check that the toggle is not enabled + const toggle = screen.getByTestId('channelBannerToggle-button'); + expect(toggle).toBeInTheDocument(); + expect(toggle).not.toHaveClass('active'); + + // Banner text and color inputs should not be visible when banner is disabled + expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument(); + expect(screen.queryByTestId('channel_banner_banner_background_color_picker')).not.toBeInTheDocument(); + }); + + it('should render with the correct initial values when banner is enabled', () => { + renderWithContext(); + + // Check that the toggle is enabled + const toggle = screen.getByTestId('channelBannerToggle-button'); + expect(toggle).toBeInTheDocument(); + expect(toggle).toHaveClass('active'); + + // Banner text and color inputs should be visible when banner is enabled + expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument(); + expect(screen.getByTestId('channel_banner_banner_text_textbox')).toHaveValue('Test banner text'); + + // Check that the color picker has the correct value + expect(screen.getByTestId('color-inputColorValue')).toBeInTheDocument(); + }); + + it('should show banner settings when toggle is clicked', async () => { + renderWithContext(); + + // Initially, banner settings should not be visible + expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument(); + + // Click the toggle to enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Banner settings should now be visible + expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument(); + expect(screen.getByTestId('color-inputColorValue')).toBeInTheDocument(); + }); + + it('should show SaveChangesPanel when changes are made', async () => { + renderWithContext(); + + // Initially, SaveChangesPanel should not be visible + expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument(); + + // Enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should now be visible + expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument(); + }); + + it('should call patchChannel with updated values when Save is clicked', async () => { + const {patchChannel} = require('mattermost-redux/actions/channels'); + patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}}); + + renderWithContext(); + + // Enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Enter banner text + await act(async () => { + const textInput = screen.getByTestId('channel_banner_banner_text_textbox'); + await userEvent.clear(textInput); + await userEvent.type(textInput, 'New banner text'); + }); + + // Set banner color + const colorInput = screen.getByTestId('color-inputColorValue'); + await act(async () => { + await userEvent.clear(colorInput); + await userEvent.type(colorInput, '#AA00AA'); + }); + + // Click the Save button + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Verify patchChannel was called with the updated values + expect(patchChannel).toHaveBeenCalledWith('channel1', { + ...mockChannel, + banner_info: { + enabled: true, + text: 'New banner text', + background_color: expect.any(String), // The exact color might be hard to test due to the mock + }, + }); + }); + + it('should reset form when Reset button is clicked', async () => { + renderWithContext(); + + // Change the banner text + await act(async () => { + const textInput = screen.getByTestId('channel_banner_banner_text_textbox'); + await userEvent.clear(textInput); + await userEvent.type(textInput, 'Changed banner text'); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should now be visible + expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument(); + + // Click the Reset button + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Reset'})); + }); + + // Form should be reset to original values + expect(screen.getByTestId('channel_banner_banner_text_textbox')).toHaveValue('Test banner text'); + + // SaveChangesPanel should be hidden after reset + expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument(); + }); + + it('should show error when banner text is empty but banner is enabled', async () => { + renderWithContext(); + + // Enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Leave banner text empty + await act(async () => { + const textInput = screen.getByTestId('channel_banner_banner_text_textbox'); + await userEvent.clear(textInput); + }); + + // Click the Save button + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // SaveChangesPanel should show error state + const errorMessage = screen.getByText(/Banner text is required/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + it('should show error when banner text exceeds character limit', async () => { + renderWithContext(); + + // Enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Create a string that exceeds the allowed character limit + const longText = 'a'.repeat(1025); + + // Enter long banner text + await act(async () => { + const textInput = screen.getByTestId('channel_banner_banner_text_textbox'); + await userEvent.clear(textInput); + await userEvent.type(textInput, longText); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should show error state + const errorMessage = screen.getByText(/There are errors in the form above/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + it('should toggle preview when preview button is clicked', async () => { + renderWithContext(); + + // Initially, preview should not be active + const previewButton = screen.getByTestId('mock-show-format'); + expect(previewButton).not.toHaveClass('active'); + + // Click the preview button + await act(async () => { + await userEvent.click(previewButton); + }); + + // Preview should now be active + expect(previewButton).toHaveClass('active'); + }); + + it('should disable banner when toggle is clicked while banner is enabled', async () => { + renderWithContext(); + + // Initially, banner settings should be visible + expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument(); + + // Click the toggle to disable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Banner settings should now be hidden + expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument(); + }); + + it('should show error when banner color is empty but banner is enabled', async () => { + const {patchChannel} = require('mattermost-redux/actions/channels'); + patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}}); + + renderWithContext(); + + // Enable the banner + await act(async () => { + await userEvent.click(screen.getByTestId('channelBannerToggle-button')); + }); + + // Enter banner text but leave color empty + await act(async () => { + const textInput = screen.getByTestId('channel_banner_banner_text_textbox'); + await userEvent.clear(textInput); + await userEvent.type(textInput, 'New banner text'); + }); + + // Click the Save button + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // SaveChangesPanel should show error state + const errorMessage = screen.getByText(/Banner color is required/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); +}); diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx new file mode 100644 index 00000000000..9fe5af97477 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx @@ -0,0 +1,291 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {useDispatch} from 'react-redux'; + +import type {Channel} from '@mattermost/types/channels'; +import type {ServerError} from '@mattermost/types/errors'; + +import {patchChannel} from 'mattermost-redux/actions/channels'; + +import ColorInput from 'components/color_input'; +import type {TextboxElement} from 'components/textbox'; +import Toggle from 'components/toggle'; +import AdvancedTextbox from 'components/widgets/advanced_textbox/advanced_textbox'; +import type {SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel'; +import SaveChangesPanel from 'components/widgets/modals/components/save_changes_panel'; + +import './channel_settings_configuration_tab.scss'; + +const CHANNEL_BANNER_MAX_CHARACTER_LIMIT = 1024; +const CHANNEL_BANNER_MIN_CHARACTER_LIMIT = 0; + +const DEFAULT_CHANNEL_BANNER = { + enabled: false, + background_color: '', + text: '', +}; + +type Props = { + channel: Channel; + setAreThereUnsavedChanges?: (unsaved: boolean) => void; + showTabSwitchError?: boolean; +} + +function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, showTabSwitchError}: Props) { + const {formatMessage} = useIntl(); + const dispatch = useDispatch(); + + const heading = formatMessage({id: 'channel_banner.label.name', defaultMessage: 'Channel Banner'}); + const subHeading = formatMessage({id: 'channel_banner.label.subtext', defaultMessage: 'When enabled, a customized banner will display at the top of the channel.'}); + const bannerTextSettingTitle = formatMessage({id: 'channel_banner.banner_text.label', defaultMessage: 'Banner text'}); + const bannerColorSettingTitle = formatMessage({id: 'channel_banner.banner_color.label', defaultMessage: 'Banner color'}); + const bannerTextPlaceholder = formatMessage({id: 'channel_banner.banner_text.placeholder', defaultMessage: 'Channel banner text'}); + + const initialBannerInfo = channel.banner_info || DEFAULT_CHANNEL_BANNER; + + const [formError, setFormError] = useState(''); + const [showBannerTextPreview, setShowBannerTextPreview] = useState(false); + const [updatedChannelBanner, setUpdatedChannelBanner] = useState(initialBannerInfo); + + const [requireConfirm, setRequireConfirm] = useState(false); + const [characterLimitExceeded, setCharacterLimitExceeded] = useState(false); + const [saveChangesPanelState, setSaveChangesPanelState] = useState(); + + // Change handlers + const handleToggle = useCallback(() => { + const newValue = !updatedChannelBanner.enabled; + const toUpdate = { + ...updatedChannelBanner, + enabled: newValue, + }; + if (!newValue) { + toUpdate.text = initialBannerInfo.text; + toUpdate.background_color = initialBannerInfo.background_color; + } + + setUpdatedChannelBanner(toUpdate); + }, [initialBannerInfo, updatedChannelBanner]); + + const handleTextChange = useCallback((e: React.ChangeEvent) => { + setUpdatedChannelBanner({ + ...updatedChannelBanner, + text: e.target.value, + }); + + if (e.target.value.trim().length > CHANNEL_BANNER_MAX_CHARACTER_LIMIT) { + setFormError(formatMessage({ + id: 'channel_settings.save_changes_panel.standard_error', + defaultMessage: 'There are errors in the form above', + })); + setCharacterLimitExceeded(true); + } else if (e.target.value.trim().length <= CHANNEL_BANNER_MIN_CHARACTER_LIMIT) { + setFormError(formatMessage({ + id: 'channel_settings.save_changes_panel.banner_text.required_error', + defaultMessage: 'Channel banner text cannot be empty when enabled', + })); + setCharacterLimitExceeded(true); + } else { + setFormError(''); + setCharacterLimitExceeded(false); + } + }, [formatMessage, updatedChannelBanner]); + + const handleColorChange = useCallback((color: string) => { + setUpdatedChannelBanner({ + ...updatedChannelBanner, + background_color: color, + }); + }, [updatedChannelBanner]); + + const toggleTextPreview = useCallback(() => setShowBannerTextPreview((show) => !show), []); + + const hasUnsavedChanges = useCallback(() => { + return updatedChannelBanner.text !== initialBannerInfo?.text || + updatedChannelBanner.background_color !== initialBannerInfo?.background_color || + updatedChannelBanner.enabled !== initialBannerInfo?.enabled; + }, [initialBannerInfo, updatedChannelBanner]); + + useEffect(() => { + const unsavedChanges = hasUnsavedChanges(); + setRequireConfirm(unsavedChanges); + setAreThereUnsavedChanges?.(unsavedChanges); + }, [hasUnsavedChanges, setAreThereUnsavedChanges]); + + const handleServerError = useCallback((err: ServerError) => { + const errorMsg = err.message || formatMessage({id: 'channel_settings.unknown_error', defaultMessage: 'Something went wrong.'}); + setFormError(errorMsg); + }, [formatMessage]); + + const handleSave = useCallback(async (): Promise => { + if (!channel) { + return false; + } + + if (updatedChannelBanner.enabled && !updatedChannelBanner.text?.trim()) { + setFormError(formatMessage({ + id: 'channel_settings.error_banner_text_required', + defaultMessage: 'Banner text is required', + })); + return false; + } + + if (updatedChannelBanner.enabled && !updatedChannelBanner.background_color?.trim()) { + setFormError(formatMessage({ + id: 'channel_settings.error_banner_color_required', + defaultMessage: 'Banner color is required', + })); + return false; + } + + const updated: Channel = { + ...channel, + }; + + updated.banner_info = { + text: updatedChannelBanner.text, + background_color: updatedChannelBanner.background_color, + enabled: updatedChannelBanner.enabled, + }; + + const {error} = await dispatch(patchChannel(channel.id, updated)); + if (error) { + handleServerError(error as ServerError); + return false; + } + + return true; + }, [channel, dispatch, formatMessage, handleServerError, updatedChannelBanner]); + + const handleSaveChanges = useCallback(async () => { + const success = await handleSave(); + if (!success) { + setSaveChangesPanelState('error'); + return; + } + setSaveChangesPanelState('saved'); + }, [handleSave]); + + const handleCancel = useCallback(() => { + setRequireConfirm(false); + setSaveChangesPanelState(undefined); + setShowBannerTextPreview(false); + + setUpdatedChannelBanner(initialBannerInfo); + setFormError(''); + setCharacterLimitExceeded(false); + }, [initialBannerInfo]); + + const handleClose = useCallback(() => { + setSaveChangesPanelState(undefined); + setRequireConfirm(false); + }, []); + + const hasErrors = Boolean(formError) || + characterLimitExceeded || + showTabSwitchError; + + return ( +
+
+
+ + +
+ +
+ +
+
+ + { + updatedChannelBanner.enabled && +
+ {/*Banner text section*/} +
+ + {bannerTextSettingTitle} + + +
+ {}} + showCharacterCount={true} + useChannelMentions={false} + onChange={handleTextChange} + preview={showBannerTextPreview} + togglePreview={toggleTextPreview} + hasError={characterLimitExceeded} + createMessage={bannerTextPlaceholder} + maxLength={CHANNEL_BANNER_MAX_CHARACTER_LIMIT} + minLength={CHANNEL_BANNER_MIN_CHARACTER_LIMIT} + /> +
+
+ + {/*Banner background color section*/} +
+ + {bannerColorSettingTitle} + + +
+ +
+
+
+ } + + {requireConfirm && ( + + )} +
+ ); +} + +export default ChannelSettingsConfigurationTab; diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx new file mode 100644 index 00000000000..2fc8809a401 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx @@ -0,0 +1,534 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {act, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import type {ChannelType} from '@mattermost/types/channels'; + +import {renderWithContext} from 'tests/react_testing_utils'; +import {TestHelper} from 'utils/test_helper'; + +import ChannelSettingsInfoTab from './channel_settings_info_tab'; + +// Mock the redux actions and selectors +jest.mock('mattermost-redux/actions/channels', () => ({ + patchChannel: jest.fn(), + updateChannelPrivacy: jest.fn(), +})); + +// Mock the ConvertConfirmModal component +jest.mock('components/admin_console/team_channel_settings/convert_confirm_modal', () => { + return jest.fn().mockImplementation(({show, onCancel, onConfirm, displayName}) => { + if (!show) { + return null; + } + return ( +
+
{'Converting '}{displayName}{' to private'}
+ + +
+ ); + }); +}); + +let mockChannelPropertiesPermission = true; +let mockConvertToPublicPermission = true; +let mockConvertToPrivatePermission = true; + +jest.mock('mattermost-redux/selectors/entities/roles', () => ({ + haveITeamPermission: jest.fn().mockReturnValue(true), + haveIChannelPermission: jest.fn().mockImplementation((state, teamId, channelId, permission: string) => { + if (permission === 'manage_private_channel_properties' || permission === 'manage_public_channel_properties') { + return mockChannelPropertiesPermission; + } + if (permission === 'convert_public_channel_to_private') { + return mockConvertToPrivatePermission; + } + if (permission === 'convert_private_channel_to_public') { + return mockConvertToPublicPermission; + } + return true; + }), + getRoles: jest.fn().mockReturnValue({}), +})); + +jest.mock('selectors/views/textbox', () => ({ + showPreviewOnChannelSettingsHeaderModal: jest.fn().mockReturnValue(false), + showPreviewOnChannelSettingsPurposeModal: jest.fn().mockReturnValue(false), +})); + +jest.mock('actions/views/textbox', () => ({ + setShowPreviewOnChannelSettingsHeaderModal: jest.fn(), + setShowPreviewOnChannelSettingsPurposeModal: jest.fn(), +})); + +// Mock the isChannelAdmin function +jest.mock('mattermost-redux/utils/user_utils', () => { + const original = jest.requireActual('mattermost-redux/utils/user_utils'); + return { + ...original, + isChannelAdmin: jest.fn().mockReturnValue(false), + }; +}); + +// Mock the ShowFormat component to make it easier to test +jest.mock('components/advanced_text_editor/show_formatting/show_formatting', () => ( + jest.fn().mockImplementation((props) => ( + + )) +)); + +// Create a mock channel member +const mockChannelMember = TestHelper.getChannelMembershipMock({ + roles: 'channel_user system_admin', +}); + +// Mock the current user +const mockUser = TestHelper.getUserMock({ + id: 'user_id', + roles: 'system_admin', +}); + +jest.mock('mattermost-redux/selectors/entities/channels', () => ({ + ...jest.requireActual('mattermost-redux/selectors/entities/channels') as typeof import('mattermost-redux/selectors/entities/channels'), + getChannelMember: jest.fn(() => mockChannelMember), +})); + +jest.mock('mattermost-redux/selectors/entities/common', () => { + return { + ...jest.requireActual('mattermost-redux/selectors/entities/common') as typeof import('mattermost-redux/selectors/entities/users'), + getCurrentUser: () => mockUser, + }; +}); + +// Create a mock channel for testing +const mockChannel = TestHelper.getChannelMock({ + id: 'channel1', + team_id: 'team1', + display_name: 'Test Channel', + name: 'test-channel', + purpose: 'Testing purpose', + header: 'Initial header', + type: 'O', +}); + +const baseProps = { + channel: mockChannel, + setAreThereUnsavedChanges: jest.fn(), +}; + +describe('ChannelSettingsInfoTab', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockChannelPropertiesPermission = true; + mockConvertToPublicPermission = true; + mockConvertToPrivatePermission = true; + }); + + it('should render with the correct initial values', () => { + renderWithContext(); + + // Check that the channel name field has the correct value. + expect(screen.getByRole('textbox', {name: 'Channel name'})).toHaveValue('Test Channel'); + + // Check that the purpose field has the correct value. + expect(screen.getByTestId('channel_settings_purpose_textbox')).toHaveValue('Testing purpose'); + + // Check that the header field has the correct value. + expect(screen.getByTestId('channel_settings_header_textbox')).toHaveValue('Initial header'); + + // Check that the public channel button is selected. + expect(screen.getByRole('button', {name: /Public Channel/}).classList.contains('selected')).toBe(true); + }); + + it('should show SaveChangesPanel when changes are made', async () => { + renderWithContext(); + + // Initially, SaveChangesPanel should not be visible. + expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument(); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + // Change the channel name. + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Updated Channel Name'); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should now be visible. + expect(screen.queryByRole('button', {name: 'Save'})).toBeInTheDocument(); + }); + + it('should call patchChannel with updated values when Save is clicked (non-privacy changes)', async () => { + const {patchChannel} = require('mattermost-redux/actions/channels'); + patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}}); + + renderWithContext(); + + // Wrap all user interactions in act to handle state updates properly + await act(async () => { + // Change the channel name. + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Updated Channel Name'); + + // Change the channel purpose. + const purposeInput = screen.getByTestId('channel_settings_purpose_textbox'); + await userEvent.clear(purposeInput); + await userEvent.type(purposeInput, 'Updated purpose'); + + // Change the channel header. + const headerInput = screen.getByTestId('channel_settings_header_textbox'); + await userEvent.clear(headerInput); + await userEvent.type(headerInput, 'Updated header'); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Click the Save button in the SaveChangesPanel. + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Verify patchChannel was called with the updated values (without type change). + expect(patchChannel).toHaveBeenCalledWith('channel1', { + ...mockChannel, + display_name: 'Updated Channel Name', + name: 'updated-channel-name', + purpose: 'Updated purpose', + header: 'Updated header', + }); + }); + + it('should reset form when Reset button is clicked', async () => { + renderWithContext(); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + // Change the channel name. + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Updated Channel Name'); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should now be visible. + expect(screen.queryByRole('button', {name: 'Save'})).toBeInTheDocument(); + + // Click the Reset button. + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Reset'})); + }); + + // Form should be reset to original values. + expect(screen.getByRole('textbox', {name: 'Channel name'})).toHaveValue('Test Channel'); + + // SaveChangesPanel should be hidden after reset. + expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument(); + }); + + it('should show error state when save fails', async () => { + const {patchChannel} = require('mattermost-redux/actions/channels'); + patchChannel.mockReturnValue({type: 'MOCK_ACTION', error: {message: 'Error saving channel'}}); + + renderWithContext(); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + // Change the channel name. + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Updated Channel Name'); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Click the Save button. + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // SaveChangesPanel should show 'error' state. + const errorMessage = screen.getByText(/There are errors in the form above/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + // Instead of clicking a non-existent element to trigger a channel name error, + // simulate an invalid input by clearing the channel name (which is required). + it('should show error when channel name field has an error', async () => { + renderWithContext( + , + ); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + // Clear the channel name to simulate an error. + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Updated Channel Name'); + await userEvent.clear(nameInput); + nameInput.blur(); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should show error state. + const errorMessage = screen.getByText(/There are errors in the form above/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + it('should show error when purpose exceeds character limit', async () => { + renderWithContext( + , + ); + + // Create a string that exceeds the allowed character limit + const longPurpose = 'a'.repeat(1025); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + const purposeInput = screen.getByTestId('channel_settings_purpose_textbox'); + await userEvent.clear(purposeInput); + await userEvent.type(purposeInput, longPurpose); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should show error state. + const errorMessage = screen.getByText(/There are errors in the form above/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + it('should show error when header exceeds character limit', async () => { + renderWithContext( + , + ); + + // Create a string that exceeds the header character limit. + const longHeader = 'a'.repeat(1025); + + // Wrap the interaction in act to handle state updates properly + await act(async () => { + const headerInput = screen.getByTestId('channel_settings_header_textbox'); + await userEvent.clear(headerInput); + await userEvent.type(headerInput, longHeader); + }); + + // Add a small delay to ensure all state updates are processed + await new Promise((resolve) => setTimeout(resolve, 0)); + + // SaveChangesPanel should show error state. + const errorMessage = screen.getByText(/There are errors in the form above/); + const errorPanel = errorMessage.closest('.SaveChangesPanel'); + expect(errorPanel).toHaveClass('error'); + }); + + it('should render ChannelNameFormField and AdvancedTextbox as readOnly when user does not have permission', () => { + mockChannelPropertiesPermission = false; + + renderWithContext(); + + // Check that the name input is disabled + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + expect(nameInput).toBeDisabled(); + + // When in readOnly mode, the preview toggle button should not be present + expect(screen.queryByTestId('mock-show-format')).not.toBeInTheDocument(); + }); + + it('should render ChannelNameFormField and AdvancedTextbox as not readOnly when user has permission', () => { + mockChannelPropertiesPermission = true; + + renderWithContext(); + + // Check that the name input is not disabled + const nameInput = screen.getByRole('textbox', {name: 'Channel name'}); + expect(nameInput).not.toBeDisabled(); + + // When not in readOnly mode, at least one preview toggle button should be present + const previewButtons = screen.queryAllByTestId('mock-show-format'); + expect(previewButtons.length).toBeGreaterThan(0); + }); + + it('should not allow channel type change when user lacks permissions', async () => { + // Set permissions to false + mockConvertToPublicPermission = false; + mockConvertToPrivatePermission = false; + + renderWithContext(); + + // Private channel button should be disabled + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + expect(privateButton).toHaveClass('disabled'); + }); + + it('should allow channel type change UI when user has permission to convert to private', async () => { + // Set convert permission to true + mockConvertToPrivatePermission = true; + mockConvertToPublicPermission = true; + + renderWithContext(); + + // Private channel button should not be disabled + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + expect(privateButton).not.toBeDisabled(); + + // Should be able to change to private in the UI + await userEvent.click(privateButton); + + // Verify the private button is now selected + expect(privateButton).toHaveClass('selected'); + }); + + it('should never allow conversion from private to public', async () => { + // Set convert permission to true (even with permission, it should be prevented) + mockConvertToPublicPermission = true; + + // Start with a private channel + const privateChannel = { + ...mockChannel, + type: 'P' as ChannelType, + }; + + renderWithContext( + , + ); + + // Public channel button should be disabled regardless of permissions + const publicButton = screen.getByRole('button', {name: /Public Channel/}); + expect(publicButton).toHaveClass('disabled'); + + // Private button should be selected + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + expect(privateButton).toHaveClass('selected'); + }); + + it('should show ConvertConfirmModal when converting from public to private', async () => { + mockConvertToPrivatePermission = true; + + renderWithContext(); + + // Change to private channel + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + await userEvent.click(privateButton); + + // Click Save button + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Verify the modal is shown + expect(screen.getByTestId('convert-confirm-modal')).toBeInTheDocument(); + }); + + it('should convert channel when confirming in ConvertConfirmModal', async () => { + mockConvertToPrivatePermission = true; + + const {updateChannelPrivacy} = require('mattermost-redux/actions/channels'); + updateChannelPrivacy.mockReturnValue({type: 'MOCK_ACTION', data: {}}); + + renderWithContext(); + + // Change to private channel + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + await userEvent.click(privateButton); + + // Click Save button to show modal + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Click confirm button in modal + await act(async () => { + await userEvent.click(screen.getByText(/Yes, Convert Channel/i)); + }); + + // Verify updateChannelPrivacy was called + expect(updateChannelPrivacy).toHaveBeenCalledWith('channel1', 'P'); + }); + + it('should not convert channel when canceling in ConvertConfirmModal', async () => { + mockConvertToPrivatePermission = true; + + const {updateChannelPrivacy} = require('mattermost-redux/actions/channels'); + updateChannelPrivacy.mockReturnValue({type: 'MOCK_ACTION', data: {}}); + + renderWithContext(); + + // Change to private channel + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + await userEvent.click(privateButton); + + // Click Save button to show modal + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Click cancel button in modal + await act(async () => { + await userEvent.click(screen.getByText(/Cancel/i)); + }); + + // Verify updateChannelPrivacy was not called + expect(updateChannelPrivacy).not.toHaveBeenCalled(); + }); + + it('should handle errors when converting channel privacy', async () => { + mockConvertToPrivatePermission = true; + + const {updateChannelPrivacy} = require('mattermost-redux/actions/channels'); + updateChannelPrivacy.mockReturnValue({ + type: 'MOCK_ACTION', + error: {message: 'Error changing privacy'}, + }); + + renderWithContext(); + + // Change to private channel + const privateButton = screen.getByRole('button', {name: /Private Channel/}); + await userEvent.click(privateButton); + + // Click Save button to show modal + await act(async () => { + await userEvent.click(screen.getByRole('button', {name: 'Save'})); + }); + + // Click confirm button in modal + await act(async () => { + await userEvent.click(screen.getByText(/Yes, Convert Channel/i)); + }); + + // Verify error state is shown + expect(screen.getByText(/There are errors in the form above/)).toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx new file mode 100644 index 00000000000..ba4bc92115d --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx @@ -0,0 +1,471 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useState, useEffect, useMemo} from 'react'; +import {useIntl} from 'react-intl'; +import {useDispatch, useSelector} from 'react-redux'; + +import type {Channel, ChannelType} from '@mattermost/types/channels'; +import type {ServerError} from '@mattermost/types/errors'; + +import {patchChannel, updateChannelPrivacy} from 'mattermost-redux/actions/channels'; +import {General} from 'mattermost-redux/constants'; +import Permissions from 'mattermost-redux/constants/permissions'; +import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; + +import { + setShowPreviewOnChannelSettingsHeaderModal, + setShowPreviewOnChannelSettingsPurposeModal, +} from 'actions/views/textbox'; +import { + showPreviewOnChannelSettingsHeaderModal, + showPreviewOnChannelSettingsPurposeModal, +} from 'selectors/views/textbox'; + +import ConvertConfirmModal from 'components/admin_console/team_channel_settings/convert_confirm_modal'; +import ChannelNameFormField from 'components/channel_name_form_field/channel_name_form_field'; +import type {TextboxElement} from 'components/textbox'; +import AdvancedTextbox from 'components/widgets/advanced_textbox/advanced_textbox'; +import SaveChangesPanel, {type SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel'; +import PublicPrivateSelector from 'components/widgets/public-private-selector/public-private-selector'; + +import Constants from 'utils/constants'; + +import type {GlobalState} from 'types/store'; + +type ChannelSettingsInfoTabProps = { + channel: Channel; + onCancel?: () => void; + setAreThereUnsavedChanges?: (unsaved: boolean) => void; + showTabSwitchError?: boolean; +}; + +function ChannelSettingsInfoTab({ + channel, + onCancel, + setAreThereUnsavedChanges, + showTabSwitchError, +}: ChannelSettingsInfoTabProps) { + const {formatMessage} = useIntl(); + const dispatch = useDispatch(); + const shouldShowPreviewPurpose = useSelector(showPreviewOnChannelSettingsPurposeModal); + const shouldShowPreviewHeader = useSelector(showPreviewOnChannelSettingsHeaderModal); + + // Permissions for transforming channel type + const canConvertToPrivate = useSelector((state: GlobalState) => + haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE), + ); + const canConvertToPublic = useSelector((state: GlobalState) => + haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PRIVATE_CHANNEL_TO_PUBLIC), + ); + + // Permissions for managing channel (name, header, purpose) + const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES; + const canManageChannelProperties = useSelector((state: GlobalState) => + haveIChannelPermission(state, channel.team_id, channel.id, channelPropertiesPermission), + ); + + // Constants + const HEADER_MAX_LENGTH = 1024; + + // Internal state variables + const [internalUrlError, setUrlError] = useState(''); + const [channelNameError, setChannelNameError] = useState(''); + const [characterLimitExceeded, setCharacterLimitExceeded] = useState(false); + const [showConvertConfirmModal, setShowConvertConfirmModal] = useState(false); + + // Removed switchingTabsWithUnsaved state as we now use the showTabSwitchError prop directly + + // The fields we allow editing + const [displayName, setDisplayName] = useState(channel?.display_name ?? ''); + const [channelUrl, setChannelURL] = useState(channel?.name ?? ''); + const [channelPurpose, setChannelPurpose] = useState(channel.purpose ?? ''); + const [channelHeader, setChannelHeader] = useState(channel?.header ?? ''); + const [channelType, setChannelType] = useState(channel?.type as ChannelType ?? Constants.OPEN_CHANNEL as ChannelType); + + // UI Feedback: errors, states + const [formError, setFormError] = useState(''); + + // SaveChangesPanel state + const [saveChangesPanelState, setSaveChangesPanelState] = useState(); + + // Handler for channel name validation errors + const handleChannelNameError = useCallback((isError: boolean, errorMessage?: string) => { + setChannelNameError(errorMessage || ''); + + // If there's an error, update the error to show in the SaveChangesPanel + if (isError && errorMessage) { + setFormError(errorMessage); + } else if (formError === channelNameError) { + // Only clear error if it's the same as the channel name error + setFormError(''); + } + }, [channelNameError, formError, setFormError]); + + // Update parent component when changes occur + useEffect(() => { + // Calculate unsaved changes directly + const unsavedChanges = channel ? ( + displayName !== channel.display_name || + channelUrl !== channel.name || + channelPurpose !== channel.purpose || + channelHeader !== channel.header || + channelType !== channel.type + ) : false; + + setAreThereUnsavedChanges?.(unsavedChanges); + }, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setAreThereUnsavedChanges]); + + const handleURLChange = useCallback((newURL: string) => { + if (internalUrlError) { + setFormError(''); + setSaveChangesPanelState(undefined); + setUrlError(''); + } + setChannelURL(newURL); + }, [internalUrlError]); + + const togglePurposePreview = useCallback(() => { + dispatch(setShowPreviewOnChannelSettingsPurposeModal(!shouldShowPreviewPurpose)); + }, [dispatch, shouldShowPreviewPurpose]); + + const toggleHeaderPreview = useCallback(() => { + dispatch(setShowPreviewOnChannelSettingsHeaderModal(!shouldShowPreviewHeader)); + }, [dispatch, shouldShowPreviewHeader]); + + const handleChannelTypeChange = (type: ChannelType) => { + // Never allow conversion from private to public, regardless of permissions + if (channel.type === Constants.PRIVATE_CHANNEL && type === Constants.OPEN_CHANNEL) { + return; + } + + // Check if user has permission to convert from public to private + if (channel.type === Constants.OPEN_CHANNEL && type === Constants.PRIVATE_CHANNEL && !canConvertToPrivate) { + return; + } + + setChannelType(type); + setFormError(''); + }; + + const handleHeaderChange = useCallback((e: React.ChangeEvent) => { + const newValue = e.target.value; + + // Update the header value + setChannelHeader(newValue); + + // Check for character limit + if (newValue.length > HEADER_MAX_LENGTH) { + setFormError(formatMessage({ + id: 'edit_channel_header_modal.error', + defaultMessage: 'The text entered exceeds the character limit. The channel header is limited to {maxLength} characters.', + }, { + maxLength: HEADER_MAX_LENGTH, + })); + } else if (formError && !channelNameError) { + // Only clear form error if there's no channel name error + // This prevents clearing channel name errors when editing the header + setFormError(''); + } + }, [HEADER_MAX_LENGTH, formError, channelNameError, setFormError, formatMessage]); + + const handlePurposeChange = useCallback((e: React.ChangeEvent) => { + const newValue = e.target.value; + + // Update the purpose value + setChannelPurpose(newValue); + + // Check for character limit + if (newValue.length > Constants.MAX_CHANNELPURPOSE_LENGTH) { + setFormError(formatMessage({ + id: 'channel_settings.error_purpose_length', + defaultMessage: 'The text entered exceeds the character limit. The channel purpose is limited to {maxLength} characters.', + }, { + maxLength: Constants.MAX_CHANNELPURPOSE_LENGTH, + })); + } else if (formError && !channelNameError) { + // Only clear server error if there's no channel name error + // This prevents clearing channel name errors when editing the purpose + setFormError(''); + } + }, [formError, channelNameError, setFormError, formatMessage]); + + const handleServerError = (err: ServerError) => { + const errorMsg = err.message || formatMessage({id: 'channel_settings.unknown_error', defaultMessage: 'Something went wrong.'}); + setFormError(errorMsg); + setSaveChangesPanelState('error'); + + // Check if the error is related to a URL conflict + if (err.message && ( + err.message.toLowerCase().includes('url') || + err.message.toLowerCase().includes('name') || + err.message.toLowerCase().includes('already exists') + )) { + setUrlError(errorMsg); // Set the URL error to show in the URL input + } + }; + + // Validate & Save - using useCallback to ensure it has the latest state values + const handleSave = useCallback(async (): Promise => { + if (!channel) { + return false; + } + + if (!displayName.trim()) { + setFormError(formatMessage({ + id: 'channel_settings.error_display_name_required', + defaultMessage: 'Channel name is required', + })); + return false; + } + + // write the code to validate if the channel is changing from public to private + if (channel.type === Constants.OPEN_CHANNEL && channelType === Constants.PRIVATE_CHANNEL) { + const {error} = await dispatch(updateChannelPrivacy(channel.id, General.PRIVATE_CHANNEL)); + if (error) { + handleServerError(error as ServerError); + return false; + } + } + + // Build updated channel object + const updated: Channel = { + ...channel, + display_name: displayName.trim(), + name: channelUrl.trim(), + purpose: channelPurpose.trim(), + header: channelHeader.trim(), + }; + + const {error} = await dispatch(patchChannel(channel.id, updated)); + if (error) { + handleServerError(error as ServerError); + return false; + } + + return true; + }, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setFormError, handleServerError]); + + // Handle save changes panel actions + const handleSaveChanges = useCallback(async () => { + // Check if privacy is changing from public to private + const isPrivacyChanging = channel.type === Constants.OPEN_CHANNEL && + channelType === Constants.PRIVATE_CHANNEL; + + // If privacy is changing, show confirmation modal + if (isPrivacyChanging) { + setShowConvertConfirmModal(true); + return; + } + + // Otherwise proceed with normal save + const success = await handleSave(); + if (!success) { + setSaveChangesPanelState('error'); + return; + } + setSaveChangesPanelState('saved'); + }, [channel, channelType, handleSave]); + + const handleClose = useCallback(() => { + setSaveChangesPanelState(undefined); + }, []); + + const hideConvertConfirmModal = useCallback(() => { + setShowConvertConfirmModal(false); + }, []); + + const handleCancel = useCallback(() => { + // First, hide the panel immediately to prevent further interactions + setSaveChangesPanelState(undefined); + + // Then reset all form fields to their original values + setDisplayName(channel?.display_name ?? ''); + setChannelURL(channel?.name ?? ''); + setChannelPurpose(channel?.purpose ?? ''); + setChannelHeader(channel?.header ?? ''); + setChannelType(channel?.type as ChannelType ?? Constants.OPEN_CHANNEL as ChannelType); + + // Clear errors + setUrlError(''); + setFormError(''); + setCharacterLimitExceeded(false); + setChannelNameError(''); + + // If parent provided an onCancel callback, call it + if (onCancel) { + onCancel(); + } + }, [channel, onCancel, setFormError]); + + // Calculate if there are errors + const hasErrors = Boolean(formError) || + characterLimitExceeded || + Boolean(channelNameError) || + Boolean(showTabSwitchError) || + Boolean(internalUrlError); + + // Memoize the calculation for whether to show the save changes panel + const shouldShowPanel = useMemo(() => { + const unsavedChanges = channel ? ( + displayName !== channel.display_name || + channelUrl !== channel.name || + channelPurpose !== channel.purpose || + channelHeader !== channel.header || + channelType !== channel.type + ) : false; + + return unsavedChanges || saveChangesPanelState === 'saved'; + }, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, saveChangesPanelState]); + + return ( +
+ {/* ConvertConfirmModal for channel privacy changes */} + { + hideConvertConfirmModal(); + const success = await handleSave(); + if (!success) { + setSaveChangesPanelState('error'); + return; + } + setSaveChangesPanelState('saved'); + }} + displayName={channel?.display_name || ''} + toPublic={false} // Always false since we're only converting from public to private + /> + + {/* Channel Name Section*/} + + { + setDisplayName(name); + }} + onURLChange={handleURLChange} + onErrorStateChange={handleChannelNameError} + urlError={internalUrlError} + currentUrl={channelUrl} + readOnly={!canManageChannelProperties} + /> + + {/* Channel Type Section*/} + + + {/* Purpose Section*/} + {}} + descriptionMessage={formatMessage({ + id: 'channel_settings.purpose.description', + defaultMessage: 'Describe how this channel should be used.', + })} + hasError={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH} + errorMessage={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH ? formatMessage({ + id: 'channel_settings.error_purpose_length', + defaultMessage: 'The channel purpose exceeds the maximum character limit of {maxLength} characters.', + }, { + maxLength: Constants.MAX_CHANNELPURPOSE_LENGTH, + }) : undefined + } + showCharacterCount={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH} + readOnly={!canManageChannelProperties} + name={formatMessage({id: 'channel_settings.purpose.label', defaultMessage: 'Channel Purpose'})} + /> + + {/* Channel Header Section*/} + {}} + descriptionMessage={formatMessage({ + id: 'channel_settings.purpose.header', + defaultMessage: 'This is the text that will appear in the header of the channel beside the channel name. You can use markdown to include links by typing [Link Title](http://example.com).', + })} + hasError={channelHeader.length > HEADER_MAX_LENGTH} + errorMessage={channelHeader.length > HEADER_MAX_LENGTH ? formatMessage({ + id: 'edit_channel_header_modal.error', + defaultMessage: 'The channel header exceeds the maximum character limit of {maxLength} characters.', + }, { + maxLength: HEADER_MAX_LENGTH, + }) : undefined + } + showCharacterCount={channelHeader.length > HEADER_MAX_LENGTH} + readOnly={!canManageChannelProperties} + name={formatMessage({id: 'channel_settings.header.label', defaultMessage: 'Channel Header'})} + /> + + {/* SaveChangesPanel for unsaved changes */} + {shouldShowPanel && ( + + )} +
+ ); +} + +export default ChannelSettingsInfoTab; diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.scss b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.scss new file mode 100644 index 00000000000..7eeb5a4dea7 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.scss @@ -0,0 +1,232 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +.ChannelSettingsModal { + label.Input_legend { + margin-bottom: 4px; + color: var(--center-channel-color); + font-family: Metropolis, sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 20px; + } + + label.Input_subheading { + color: rgba(var(--center-channel-color-rgb), 0.64); + font-family: Open Sans, sans-serif; + font-size: 12px; + font-weight: 400; + } + + .GenericModal__wrapper { + display: flex; + overflow: visible; + max-width: 1200px; + max-height: 90vh; + flex-direction: column; + border: var(--border-default); + border-radius: var(--radius-l); + box-shadow: var(--elevation-6); + + .modal-body { + display: flex; + overflow: visible; + width: auto; + min-height: 150px; + flex-direction: column; + margin: 0; + gap: 24px; + } + } + + &__bodyWrapper { + display: flex; + width: 100%; + max-width: 1024px; + flex-direction: column; + gap: 24px; + } + + .settings-table { + display: flex; + flex-direction: row; + + .settings-links { + min-width: 200px; + } + + .settings-content { + display: flex; + overflow: visible !important; // This is necessary to override the overflow hidden from the modal for emoji list + flex: 1; + flex-direction: column; + + .AdvancedTextbox { + position: relative; + } + + .channel-settings-name-container, .AdvancedTextbox { + margin-top: 24px; + } + } + } + + &__infoTab { + display: flex; + flex-direction: column; + padding-bottom: 60px; + + #PreviewInputTextButton { + position: absolute; + z-index: 10; + top: 5px; + right: 5px; + } + + // This is a workaround forcing styles to override the ones from the textbox component that affect post input + .textbox-preview-area, #channel_settings_purpose_textbox, #channel_settings_header_textbox { + height: 80px !important; + border: none !important; + box-shadow: none !important; + } + + // forcing z-index on the emoji list to be on top of the modal + .suggestion-list { + z-index: 100000; + } + + .textbox-preview-area { + box-shadow: none; + } + } + + &__typeSelector { + margin-top: 16px; + } + + &__purposeContainer, &__headerContainer { + margin-top: 28px; + + textarea { + width: 100%; + min-height: 8em; + max-height: 60vh; + box-sizing: border-box; + padding: 12px 16px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + border-radius: 4px; + background: var(--center-channel-bg); + color: var(--center-channel-color); + font-size: 14px; + line-height: 20px; + resize: none; + + &:hover { + border-color: rgba(var(--center-channel-color-rgb), 0.48); + } + + &:focus { + border-color: var(--button-bg); + box-shadow: inset 0 0 0 1px var(--button-bg); + } + + &.with-error { + border-color: var(--error-text); + + &:focus { + box-shadow: inset 0 0 0 1px var(--error-text); + } + } + } + } + + &__headerContainer { + margin-top: 28px; + + &--preview-button { + display: flex; + align-items: center; + padding: 4px 8px; + border: none; + background: transparent; + color: rgba(var(--center-channel-color-rgb), 0.56); + cursor: pointer; + + &:hover { + color: rgba(var(--center-channel-color-rgb), 0.72); + } + + i { + font-size: 18px; + } + } + + .markdown-preview-wrapper { + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + } + } + + &__archiveTab { + display: flex; + flex-direction: column; + gap: 16px; + + .btn-danger { + align-self: flex-start; + } + } + + @media screen and (max-width: 768px) { + max-width: 100%; + margin: 0; + + .modal-content { + display: flex; + height: 100vh; + max-height: unset; + flex-direction: column; + border-radius: unset; + } + } + + @media screen and (max-height: 900px) and (min-width: 768px) { + .modal-content { + max-height: 90vh; + } + } + + @media screen and (max-height: 600px) { + .modal-content, + .GenericModal__wrapper { + max-height: 85vh !important; + } + } +} + +// styles for the archive channel confirm modal +.archiveChannelConfirmModal { + display: flex; + flex-direction: column; + justify-content: center; + + .GenericModal__header { + display: flex; + justify-content: center; + + h1#genericModalLabel { + width: auto; + } + } + + .GenericModal__body { + .ConfirmModal__body { + text-align: center; + } + } + + .ConfirmModal__footer { + display: flex; + justify-content: center; + gap: 8px; + } +} diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx new file mode 100644 index 00000000000..d9e9f4d7973 --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx @@ -0,0 +1,293 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import type {DeepPartial} from '@mattermost/types/utilities'; + +import {renderWithContext} from 'tests/react_testing_utils'; + +import type {GlobalState} from 'types/store'; + +import ChannelSettingsModal from './channel_settings_modal'; + +// Variables to control permission check results in tests +let mockPrivateChannelPermission = true; +let mockPublicChannelPermission = true; + +// Mock the redux selectors +jest.mock('mattermost-redux/selectors/entities/channels', () => ({ + getChannel: jest.fn().mockImplementation((state, channelId) => { + // Return a mock channel based on the channelId + return { + id: channelId, + team_id: 'team1', + display_name: 'Test Channel', + name: channelId === 'default_channel' ? 'town-square' : 'test-channel', + purpose: 'Testing purpose', + header: 'Channel header', + type: mockChannelType, // Use a variable to control the channel type + create_at: 0, + update_at: 0, + delete_at: 0, + last_post_at: 0, + total_msg_count: 0, + extra_update_at: 0, + creator_id: 'creator1', + last_root_post_at: 0, + scheme_id: '', + group_constrained: false, + }; + }), +})); + +// Mock the roles selector which is used for permission checks +jest.mock('mattermost-redux/selectors/entities/roles', () => ({ + haveIChannelPermission: jest.fn().mockImplementation((state, teamId, channelId, permission) => { + // Return different values based on the permission being checked + if (permission === 'delete_private_channel') { + return mockPrivateChannelPermission; + } + if (permission === 'delete_public_channel') { + return mockPublicChannelPermission; + } + return true; + }), +})); + +// Mock the child components to simplify testing +jest.mock('./channel_settings_info_tab', () => { + return function MockInfoTab(): JSX.Element { + return
{'Info Tab Content'}
; + }; +}); + +jest.mock('./channel_settings_configuration_tab', () => { + return function MockConfigTab(): JSX.Element { + return
{'Configuration Tab Content'}
; + }; +}); + +jest.mock('./channel_settings_archive_tab', () => { + return function MockArchiveTab(): JSX.Element { + return
{'Archive Tab Content'}
; + }; +}); + +// Define the tab type for the settings sidebar +type TabType = { + name: string; + uiName: string; + display?: boolean; +}; + +// Variable to control the channel type in tests +let mockChannelType = 'O'; + +// Mock the settings sidebar +jest.mock('components/settings_sidebar', () => { + return function MockSettingsSidebar({tabs, activeTab, updateTab}: {tabs: TabType[]; activeTab: string; updateTab: (tab: string) => void}): JSX.Element { + return ( +
+ {tabs.filter((tab) => tab.display !== false).map((tab) => ( + + ))} +
+ ); + }; +}); + +const baseProps = { + channelId: 'channel1', + isOpen: true, + onExited: jest.fn(), + focusOriginElement: 'button1', +}; + +describe('ChannelSettingsModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockChannelType = 'O'; // Default to public channel + mockPrivateChannelPermission = true; + mockPublicChannelPermission = true; + }); + + it('should render the modal with correct header text', async () => { + renderWithContext(); + expect(screen.getByText('Channel Settings')).toBeInTheDocument(); + }); + + it('should render Info tab by default', async () => { + renderWithContext(); + + // Wait for the lazy-loaded components + await waitFor(() => { + expect(screen.getByTestId('info-tab')).toBeInTheDocument(); + }); + }); + + it('should switch tabs when clicked', async () => { + renderWithContext(); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // Initially the info tab should be active + expect(screen.getByTestId('info-tab')).toBeInTheDocument(); + + // Find and click the archive tab + const archiveTab = screen.getByRole('tab', {name: 'archive'}); + await userEvent.click(archiveTab); + + // Now the archive tab should be visible + expect(screen.getByTestId('archive-tab')).toBeInTheDocument(); + }); + + it('should not show archive tab for default channel', async () => { + renderWithContext( + , + ); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // The info tab should be visible + expect(screen.getByTestId('info-tab')).toBeInTheDocument(); + + // The archive tab should not be in the document + expect(screen.queryByRole('tab', {name: 'archive'})).not.toBeInTheDocument(); + }); + + it('should show archive tab for public channel when user has permission', async () => { + mockChannelType = 'O'; + mockPublicChannelPermission = true; + + renderWithContext(); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // The archive tab should be visible + expect(screen.getByRole('tab', {name: 'archive'})).toBeInTheDocument(); + }); + + it('should not show archive tab for public channel when user does not have permission', async () => { + mockChannelType = 'O'; + mockPublicChannelPermission = false; + + renderWithContext(); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // The archive tab should not be in the document + expect(screen.queryByRole('tab', {name: 'archive'})).not.toBeInTheDocument(); + }); + + it('should show archive tab for private channel when user has permission', async () => { + mockChannelType = 'P'; + mockPrivateChannelPermission = true; + + renderWithContext(); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // The archive tab should be visible + expect(screen.getByRole('tab', {name: 'archive'})).toBeInTheDocument(); + }); + + it('should not show archive tab for private channel when user does not have permission', async () => { + mockChannelType = 'P'; + mockPrivateChannelPermission = false; + + renderWithContext(); + + // Wait for the sidebar to load + await waitFor(() => { + expect(screen.getByTestId('settings-sidebar')).toBeInTheDocument(); + }); + + // The archive tab should not be in the document + expect(screen.queryByRole('tab', {name: 'archive'})).not.toBeInTheDocument(); + }); + + it('should not show configuration tab with no license', async () => { + const baseState: DeepPartial = { + entities: { + general: { + license: { + SkuShortName: '', + }, + }, + }, + }; + renderWithContext(, baseState as GlobalState); + expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument(); + }); + + it('should not show configuration tab with professional license', async () => { + const baseState: DeepPartial = { + entities: { + general: { + license: { + SkuShortName: 'professional', + }, + }, + }, + }; + renderWithContext(, baseState as GlobalState); + expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument(); + }); + + it('should not show configuration tab with enterprise license', async () => { + const baseState: DeepPartial = { + entities: { + general: { + license: { + SkuShortName: 'enterprise', + }, + }, + }, + }; + renderWithContext(, baseState as GlobalState); + expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument(); + }); + + it('should show configuration tab when premium license', async () => { + const baseState: DeepPartial = { + entities: { + general: { + license: { + SkuShortName: 'premium', + }, + }, + }, + }; + renderWithContext(, baseState as GlobalState); + expect(screen.getByTestId('configuration-tab-button')).toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.tsx new file mode 100644 index 00000000000..5fb78d9c74c --- /dev/null +++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.tsx @@ -0,0 +1,240 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, { + useState, + useRef, +} from 'react'; +import {useIntl} from 'react-intl'; +import {useSelector, useDispatch} from 'react-redux'; + +import {GenericModal} from '@mattermost/components'; +import type {Channel} from '@mattermost/types/channels'; + +import Permissions from 'mattermost-redux/constants/permissions'; +import {selectChannelBannerEnabled} from 'mattermost-redux/selectors/entities/channel_banner'; +import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; + +import { + setShowPreviewOnChannelSettingsHeaderModal, + setShowPreviewOnChannelSettingsPurposeModal, +} from 'actions/views/textbox'; + +import {focusElement} from 'utils/a11y_utils'; +import Constants from 'utils/constants'; + +import type {GlobalState} from 'types/store'; + +import ChannelSettingsArchiveTab from './channel_settings_archive_tab'; +import ChannelSettingsConfigurationTab from './channel_settings_configuration_tab'; +import ChannelSettingsInfoTab from './channel_settings_info_tab'; + +import './channel_settings_modal.scss'; + +// Lazy-loaded components +const SettingsSidebar = React.lazy(() => import('components/settings_sidebar')); + +type ChannelSettingsModalProps = { + channelId: string; + onExited: () => void; + isOpen: boolean; + focusOriginElement?: string; +}; + +enum ChannelSettingsTabs { + INFO = 'info', + CONFIGURATION = 'configuration', + ARCHIVE = 'archive', +} + +const SHOW_PANEL_ERROR_STATE_TAB_SWITCH_TIMEOUT = 3000; + +function ChannelSettingsModal({channelId, isOpen, onExited, focusOriginElement}: ChannelSettingsModalProps) { + const {formatMessage} = useIntl(); + const dispatch = useDispatch(); + const channel = useSelector((state: GlobalState) => getChannel(state, channelId)) as Channel; + const shouldShowConfigurationTab = useSelector(selectChannelBannerEnabled); + + const canArchivePrivateChannels = useSelector((state: GlobalState) => + haveIChannelPermission(state, channel.team_id, channel.id, Permissions.DELETE_PRIVATE_CHANNEL), + ); + + const canArchivePublicChannels = useSelector((state: GlobalState) => + haveIChannelPermission(state, channel.team_id, channel.id, Permissions.DELETE_PUBLIC_CHANNEL), + ); + + const [show, setShow] = useState(isOpen); + + // Active tab + const [activeTab, setActiveTab] = useState(ChannelSettingsTabs.INFO); + + // State for showing error in the save changes panel when trying to switch tabs with unsaved changes + const [showTabSwitchError, setShowTabSwitchError] = useState(false); + + // State to track if there are unsaved changes + const [areThereUnsavedChanges, setAreThereUnsavedChanges] = useState(false); + + // Refs + const modalBodyRef = useRef(null); + + // Called to set the active tab, prompting save changes panel if there are unsaved changes + const updateTab = (newTab: string) => { + /** + * If there are unsaved changes, show an error in the save changes panel + * and reset it after a timeout to indicate the user needs to save or discard changes + * before switching tabs. + */ + if (areThereUnsavedChanges) { + setShowTabSwitchError(true); + setTimeout(() => { + setShowTabSwitchError(false); + }, SHOW_PANEL_ERROR_STATE_TAB_SWITCH_TIMEOUT); + return; + } + + const tab = newTab as ChannelSettingsTabs; + setActiveTab(tab); + + if (modalBodyRef.current) { + modalBodyRef.current.scrollTop = 0; + } + }; + + const handleHide = () => { + handleHideConfirm(); + }; + + const handleHideConfirm = () => { + // Reset preview states to false when closing the modal + dispatch(setShowPreviewOnChannelSettingsHeaderModal(false)); + dispatch(setShowPreviewOnChannelSettingsPurposeModal(false)); + setShow(false); + }; + + // Called after the fade-out completes + const handleExited = () => { + // Clear anything if needed + setActiveTab(ChannelSettingsTabs.INFO); + if (focusOriginElement) { + focusElement(focusOriginElement, true); + } + onExited(); + }; + + // Renders content based on active tab + const renderTabContent = () => { + switch (activeTab) { + case ChannelSettingsTabs.INFO: + return renderInfoTab(); + case ChannelSettingsTabs.CONFIGURATION: + return renderConfigurationTab(); + case ChannelSettingsTabs.ARCHIVE: + return renderArchiveTab(); + default: + return renderInfoTab(); + } + }; + + const renderInfoTab = () => { + return ( + + ); + }; + + const renderConfigurationTab = () => { + return ( + + ); + }; + + const renderArchiveTab = () => { + return ( + + ); + }; + + // Define tabs for the settings sidebar + const tabs = [ + { + name: ChannelSettingsTabs.INFO, + uiName: formatMessage({id: 'channel_settings.tab.info', defaultMessage: 'Info'}), + icon: 'icon icon-information-outline', + iconTitle: formatMessage({id: 'generic_icons.info', defaultMessage: 'Info Icon'}), + }, + { + name: ChannelSettingsTabs.CONFIGURATION, + uiName: formatMessage({id: 'channel_settings.tab.configuration', defaultMessage: 'Configuration'}), + icon: 'icon icon-cog-outline', + iconTitle: formatMessage({id: 'generic_icons.settings', defaultMessage: 'Settings Icon'}), + display: shouldShowConfigurationTab, + }, + { + name: ChannelSettingsTabs.ARCHIVE, + uiName: formatMessage({id: 'channel_settings.tab.archive', defaultMessage: 'Archive Channel'}), + icon: 'icon icon-archive-outline', + iconTitle: formatMessage({id: 'generic_icons.archive', defaultMessage: 'Archive Icon'}), + newGroup: true, + display: channel.name !== Constants.DEFAULT_CHANNEL && // archive is not available for the default channel + ((channel.type === Constants.PRIVATE_CHANNEL && canArchivePrivateChannels) || + (channel.type === Constants.OPEN_CHANNEL && canArchivePublicChannels)), + }, + ]; + + // Renders the body: left sidebar for tabs, the content on the right + const renderModalBody = () => { + return ( +
+
+ + + +
+
+ {renderTabContent()} +
+
+ ); + }; + + const modalTitle = formatMessage({id: 'channel_settings.modal.title', defaultMessage: 'Channel Settings'}); + + return ( + +
+ {renderModalBody()} +
+
+ ); +} + +export default ChannelSettingsModal; diff --git a/webapp/channels/src/components/channel_view/channel_view.tsx b/webapp/channels/src/components/channel_view/channel_view.tsx index f1760672c4f..e7cf738bb8a 100644 --- a/webapp/channels/src/components/channel_view/channel_view.tsx +++ b/webapp/channels/src/components/channel_view/channel_view.tsx @@ -20,7 +20,7 @@ const ChannelHeader = makeAsyncComponent('ChannelHeader', lazy(() => import('com const FileUploadOverlay = makeAsyncComponent('FileUploadOverlay', lazy(() => import('components/file_upload_overlay'))); const ChannelBookmarks = makeAsyncComponent('ChannelBookmarks', lazy(() => import('components/channel_bookmarks'))); const AdvancedCreatePost = makeAsyncComponent('AdvancedCreatePost', lazy(() => import('components/advanced_create_post'))); -const ChannelBanner = makeAsyncComponent('ChannelBanner', lazy(() => import('components/channel_banner'))); +const ChannelBanner = makeAsyncComponent('ChannelBanner', lazy(() => import('components/channel_banner/channel_banner'))); export type Props = PropsFromRedux & RouteComponentProps<{ postid?: string; diff --git a/webapp/channels/src/components/confirm_modal.tsx b/webapp/channels/src/components/confirm_modal.tsx index 5359682b6f3..adac02c06ef 100644 --- a/webapp/channels/src/components/confirm_modal.tsx +++ b/webapp/channels/src/components/confirm_modal.tsx @@ -1,12 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import classNames from 'classnames'; import React from 'react'; import {FormattedMessage} from 'react-intl'; import {GenericModal} from '@mattermost/components'; +import {focusElement} from 'utils/a11y_utils'; + import './confirm_modal.scss'; type Props = { @@ -81,6 +82,12 @@ type Props = { * Set to hide the cancel button */ hideCancel?: boolean; + + /* + * The element that triggered the modal + */ + focusOriginElement?: string; + }; type State = { @@ -123,6 +130,13 @@ export default class ConfirmModal extends React.Component { this.props.onCancel?.(this.state.checked); }; + handleExited = () => { + this.props.onExited?.(); + if (this.props.focusOriginElement) { + focusElement(this.props.focusOriginElement!, true); + } + }; + render() { let checkbox; if (this.props.showCheckbox) { @@ -169,11 +183,12 @@ export default class ConfirmModal extends React.Component { return ( @@ -191,11 +206,11 @@ export default class ConfirmModal extends React.Component { {this.props.checkboxInFooter && checkbox} {cancelButton} diff --git a/webapp/channels/src/components/integrations/outgoing_oauth_connections/__snapshots__/abstract_outgoing_oauth_connection.test.tsx.snap b/webapp/channels/src/components/integrations/outgoing_oauth_connections/__snapshots__/abstract_outgoing_oauth_connection.test.tsx.snap index ec8108994dc..f408a60eaec 100644 --- a/webapp/channels/src/components/integrations/outgoing_oauth_connections/__snapshots__/abstract_outgoing_oauth_connection.test.tsx.snap +++ b/webapp/channels/src/components/integrations/outgoing_oauth_connections/__snapshots__/abstract_outgoing_oauth_connection.test.tsx.snap @@ -498,6 +498,7 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn title="Save Outgoing OAuth Connection" > } modalLocation="center" + onExited={[Function]} onHide={[Function]} show={false} showCloseButton={true} @@ -571,7 +573,7 @@ https://myothersite.com/api/v2" > } modalLocation="center" + onExited={[Function]} onHide={[Function]} show={false} showCloseButton={true} @@ -1327,7 +1333,7 @@ https://myothersite.com/api/v2" > } modalLocation="center" + onExited={[Function]} onHide={[Function]} show={false} showCloseButton={true} @@ -2082,7 +2092,7 @@ https://myothersite.com/api/v2" > - - - - - -
- -
- - -
-
- -
- - - fake-channel/channels/ - - - -
-

- -

-
-
- - - - -
-
-`; diff --git a/webapp/channels/src/components/rename_channel_modal/index.ts b/webapp/channels/src/components/rename_channel_modal/index.ts deleted file mode 100644 index a18a61a59a3..00000000000 --- a/webapp/channels/src/components/rename_channel_modal/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import type {Dispatch} from 'redux'; - -import type {GlobalState} from '@mattermost/types/store'; - -import {patchChannel} from 'mattermost-redux/actions/channels'; -import {createSelector} from 'mattermost-redux/selectors/create_selector'; -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; - -import {getSiteURL} from 'utils/url'; - -import RenameChannelModal from './rename_channel_modal'; - -const mapStateToPropsRenameChannel = createSelector( - 'mapStateToPropsRenameChannel', - (state: GlobalState) => { - const currentTeamId = state.entities.teams.currentTeamId; - const team = getTeam(state, currentTeamId); - const currentTeamUrl = `${getSiteURL()}/${team ? team.name : ''}`; - return { - currentTeamUrl, - team, - }; - }, - (teamInfo) => ({...teamInfo}), -); - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators({ - patchChannel, - }, dispatch), - }; -} - -export default connect(mapStateToPropsRenameChannel, mapDispatchToProps)(RenameChannelModal); diff --git a/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.test.tsx b/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.test.tsx deleted file mode 100644 index 8154b2960b1..00000000000 --- a/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.test.tsx +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import type {Channel} from '@mattermost/types/channels'; -import type {Team} from '@mattermost/types/teams'; - -import {RequestStatus} from 'mattermost-redux/constants'; - -import {shallowWithIntl} from 'tests/helpers/intl-test-helper'; -import {TestHelper} from 'utils/test_helper'; - -import RenameChannelModal from './rename_channel_modal'; -import type {RenameChannelModal as RenameChannelModalClass} from './rename_channel_modal'; - -describe('components/RenameChannelModal', () => { - const channel: Channel = TestHelper.getChannelMock({ - id: 'fake-id', - name: 'fake-channel', - display_name: 'Fake Channel', - }); - - const team: Team = TestHelper.getTeamMock({ - name: 'Fake Team', - display_name: 'fake-team', - }); - - const baseProps = { - show: true, - onExited: jest.fn(), - channel: {...channel}, - requestStatus: RequestStatus.NOT_STARTED, - team: {...team}, - currentTeamUrl: 'fake-channel', - actions: {patchChannel: jest.fn().mockResolvedValue({data: true})}, - }; - - test('should match snapshot', () => { - const wrapper = shallowWithIntl( - , - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('should submit form', () => { - const {actions: {patchChannel}} = baseProps; - const props = {...baseProps, requestStatus: RequestStatus.STARTED}; - const wrapper = shallowWithIntl( - , - ); - - wrapper.find('#display_name').simulate( - 'change', {preventDefault: jest.fn(), target: {value: 'valid name'}}, - ); - - wrapper.find('#save-button').simulate('click'); - - expect(patchChannel).toHaveBeenCalled(); - }); - - describe('should validate channel url (name)', () => { - const testCases: Array<[{name: string; value: string}, boolean]> = [ - [{name: 'must be one or more characters', value: ''}, false], - [{name: 'must start with a letter or number', value: '_channel'}, false], - [{name: 'must end with a letter or number', value: 'channel_'}, false], - [{name: 'can contain two underscores in a row', value: 'channel__two'}, true], - [{name: 'can not resemble direct message channel url', value: 'uzsfmtmniifsjgesce4u7yznyh__uzsfmtmniifsjgesce4u7yznyh'}, false], - [{name: 'valid channel url', value: 'a_valid_channel'}, true], - ]; - - testCases.forEach(([testCaseProps, patchShouldHaveBeenCalled]) => { - it(testCaseProps.name, () => { - const {actions: {patchChannel}} = baseProps; - const wrapper = shallowWithIntl( - , - ); - - wrapper.setState({channelName: testCaseProps.value}); - - wrapper.find('#save-button').simulate('click'); - if (patchShouldHaveBeenCalled) { - expect(patchChannel).toHaveBeenCalled(); - } else { - expect(patchChannel).not.toHaveBeenCalled(); - } - }); - }); - }); - - test('should not call patchChannel as channel.name.length > Constants.MAX_CHANNELNAME_LENGTH (64)', () => { - const {actions: {patchChannel}} = baseProps; - const wrapper = shallowWithIntl( - , - ); - - wrapper.find('#display_name').simulate( - 'change', {preventDefault: jest.fn(), target: {value: 'string-above-sixtyfour-characters-to-test-the-channel-maxlength-limit-properly-in-the-component'}}, - ); - - wrapper.find('#save-button').simulate('click'); - - expect(patchChannel).not.toHaveBeenCalled(); - }); - - test('should change state when display_name is edited', () => { - const wrapper = shallowWithIntl( - , - ); - - wrapper.find('#display_name').simulate( - 'change', {preventDefault: jest.fn(), target: {value: 'New Fake Channel'}}, - ); - - expect(wrapper.state('displayName')).toBe('New Fake Channel'); - }); - - test('should call setError function', () => { - const wrapper = shallowWithIntl( - , - ); - - const instance = wrapper.instance() as RenameChannelModalClass; - - instance.setError({message: 'This is an error message'}); - expect(wrapper.state('serverError')).toBe('This is an error message'); - }); - - test('should call unsetError function', () => { - const props = {...baseProps, serverError: {message: 'This is an error message'}}; - const wrapper = shallowWithIntl( - , - ); - - wrapper.setState({serverError: props.serverError.message}); - expect(wrapper.state('serverError')).toBe('This is an error message'); - - wrapper.find('#save-button').simulate('click'); - expect(wrapper.state('serverError')).toBe(''); - }); - - test('should call handleSubmit function', async () => { - const patchChannel = jest.fn(). - mockResolvedValueOnce({error: true}). - mockResolvedValue({data: true}); - - const wrapper = shallowWithIntl( - , - ); - - wrapper.setState({displayName: 'Changed Name', channelName: 'changed-name'}); - - const instance = wrapper.instance() as RenameChannelModalClass; - instance.onSaveSuccess = jest.fn(); - instance.setError = jest.fn(); - - await instance.handleSubmit(); - expect(patchChannel).toHaveBeenCalledTimes(1); - expect(wrapper.state('displayName')).toBe('Changed Name'); - expect(wrapper.state('channelName')).toBe('changed-name'); - expect(instance.onSaveSuccess).not.toBeCalled(); - expect(instance.setError).toBeCalledTimes(1); - expect(instance.setError).toBeCalledWith(true); - - await instance.handleSubmit(); - expect(patchChannel).toHaveBeenCalledTimes(2); - expect(wrapper.state('displayName')).toBe('Changed Name'); - expect(wrapper.state('channelName')).toBe('changed-name'); - expect(instance.onSaveSuccess).toBeCalledTimes(1); - expect(instance.setError).toBeCalledTimes(1); - }); - - test('should call handleCancel', () => { - const wrapper = shallowWithIntl( - , - ); - - const instance = wrapper.instance() as RenameChannelModalClass; - instance.handleCancel(); - - expect(wrapper.state('show')).toBeFalsy(); - }); - - test('should call handleHide function', () => { - const wrapper = shallowWithIntl( - , - ); - - const instance = wrapper.instance() as RenameChannelModalClass; - instance.handleHide(); - - expect(wrapper.state('show')).toBeFalsy(); - }); - - test('should call onNameChange function', () => { - const changedName = {target: {value: 'changed-name'}}; - const wrapper = shallowWithIntl( - , - ); - - const instance = wrapper.instance() as RenameChannelModalClass; - instance.onNameChange(changedName); - - expect(wrapper.state('channelName')).toBe('changed-name'); - }); -}); diff --git a/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.tsx b/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.tsx deleted file mode 100644 index 4501dd94bf7..00000000000 --- a/webapp/channels/src/components/rename_channel_modal/rename_channel_modal.tsx +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import type {ChangeEvent, MouseEvent} from 'react'; -import {Modal} from 'react-bootstrap'; -import {defineMessages, FormattedMessage, injectIntl} from 'react-intl'; -import type {IntlShape} from 'react-intl'; - -import type {Channel} from '@mattermost/types/channels'; -import type {ServerError} from '@mattermost/types/errors'; -import type {Team} from '@mattermost/types/teams'; - -import type {ActionResult} from 'mattermost-redux/types/actions'; - -import WithTooltip from 'components/with_tooltip'; - -import {getHistory} from 'utils/browser_history'; -import Constants from 'utils/constants'; -import {getShortenedURL, validateChannelUrl} from 'utils/url'; -import * as Utils from 'utils/utils'; - -const holders = defineMessages({ - maxLength: { - id: 'rename_channel.maxLength', - defaultMessage: 'This field must be less than {maxLength, number} characters', - }, - url: { - id: 'rename_channel.url', - defaultMessage: 'URL', - }, - defaultError: { - id: 'rename_channel.defaultError', - defaultMessage: ' - Cannot be changed for the default channel', - }, -}); - -type Props = { - - /** - * react-intl helper object - */ - intl: IntlShape; - - /** - * Function that is called when modal is hidden - */ - onExited: () => void; - - /** - * Object with info about current channel - */ - channel: Channel; - - /** - * Object with info about current team - */ - team?: Team; - - /** - * String with the current team URL - */ - currentTeamUrl: string; - - /* - * Object with redux action creators - */ - actions: { - - /* - * Action creator to patch current channel - */ - patchChannel: (channelId: string, patch: Channel) => Promise; - }; -} - -type State = { - displayName: string; - channelName: string; - serverError?: string; - urlErrors: React.ReactNode[]; - displayNameError: React.ReactNode; - invalid: boolean; - show: boolean; -}; - -export class RenameChannelModal extends React.PureComponent { - private textbox?: HTMLInputElement; - - constructor(props: Props) { - super(props); - - this.state = { - displayName: props.channel.display_name, - channelName: props.channel.name, - serverError: '', - urlErrors: [], - displayNameError: '', - invalid: false, - show: true, - }; - } - - setError = (err: ServerError) => { - this.setState({serverError: err.message}); - }; - - unsetError = () => { - this.setState({serverError: ''}); - }; - - handleEntering = () => { - if (this.textbox) { - Utils.placeCaretAtEnd(this.textbox); - } - }; - - handleHide = (e?: MouseEvent) => { - if (e) { - e.preventDefault(); - } - - this.setState({ - serverError: '', - urlErrors: [], - displayNameError: '', - invalid: false, - show: false, - }); - }; - - handleSubmit = async (e?: MouseEvent): Promise => { - if (e) { - e.preventDefault(); - } - - const channel = Object.assign({}, this.props.channel); - const oldName = channel.name; - const oldDisplayName = channel.display_name; - const state = {...this.state, serverError: ''}; - const {formatMessage} = this.props.intl; - const {actions: {patchChannel}} = this.props; - - channel.display_name = this.state.displayName.trim(); - if (!channel.display_name || channel.display_name.length < Constants.MIN_CHANNELNAME_LENGTH) { - state.displayNameError = ( - - ); - state.invalid = true; - } else if (channel.display_name.length > Constants.MAX_CHANNELNAME_LENGTH) { - state.displayNameError = formatMessage(holders.maxLength, {maxLength: Constants.MAX_CHANNELNAME_LENGTH}); - state.invalid = true; - } else { - state.displayNameError = ''; - } - - channel.name = this.state.channelName.trim(); - const urlErrors = validateChannelUrl(channel.name); - if (urlErrors.length > 0) { - state.invalid = true; - } - state.urlErrors = urlErrors; - - this.setState(state); - - if (state.invalid) { - return; - } - if (oldName === channel.name && oldDisplayName === channel.display_name) { - this.onSaveSuccess(); - return; - } - - const {data, error} = await patchChannel(channel.id, channel); - - if (data) { - this.onSaveSuccess(); - } else if (error) { - this.setError(error); - } - }; - - onSaveSuccess = () => { - this.handleHide(); - this.unsetError(); - if (this.props.team) { - getHistory().push('/' + this.props.team.name + '/channels/' + this.state.channelName); - } - }; - - handleCancel = (e?: MouseEvent) => { - this.setState({ - displayName: this.props.channel.display_name, - channelName: this.props.channel.name, - }); - - this.handleHide(e); - }; - - onNameChange = (e: ChangeEvent | {target: {value: string}}) => { - const name = e.target.value.trim().replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(); - this.setState({channelName: name}); - }; - - onDisplayNameChange = (e: ChangeEvent) => { - this.setState({displayName: e.target.value}); - }; - - getTextbox = (node: HTMLInputElement) => { - this.textbox = node; - }; - - render(): JSX.Element { - let displayNameError = null; - if (this.state.displayNameError) { - displayNameError =

{this.state.displayNameError}

; - } - - let urlErrors = null; - let urlHelpText = null; - let urlInputClass = 'input-group input-group--limit'; - if (this.state.urlErrors.length > 0) { - urlErrors =

{this.state.urlErrors}

; - urlInputClass += ' has-error'; - } else { - urlHelpText = ( -

- -

- ); - } - - let serverError = null; - if (this.state.serverError) { - serverError =
{this.state.serverError}
; - } - - const {formatMessage} = this.props.intl; - - let urlInputLabel = formatMessage(holders.url); - let readOnlyHandleInput = false; - if (this.props.channel.name === Constants.DEFAULT_CHANNEL) { - urlInputLabel += formatMessage(holders.defaultError); - readOnlyHandleInput = true; - } - - const fullUrl = this.props.currentTeamUrl + '/channels'; - const shortUrl = `${getShortenedURL(fullUrl, 35)}/`; - - return ( - - - - - - -
- -
- - - {displayNameError} -
-
- - -
- - {shortUrl} - - -
- {urlHelpText} - {urlErrors} -
- {serverError} -
- - - - -
-
- ); - } -} - -export default injectIntl(RenameChannelModal); diff --git a/webapp/channels/src/components/settings_sidebar/settings_sidebar.tsx b/webapp/channels/src/components/settings_sidebar/settings_sidebar.tsx index 39e346bfcc4..eb8c9f01f45 100644 --- a/webapp/channels/src/components/settings_sidebar/settings_sidebar.tsx +++ b/webapp/channels/src/components/settings_sidebar/settings_sidebar.tsx @@ -8,13 +8,14 @@ import {FormattedMessage} from 'react-intl'; import Constants from 'utils/constants'; import {isKeyPressed} from 'utils/keyboard'; -import {a11yFocus} from 'utils/utils'; export type Tab = { icon: string | {url: string}; iconTitle: string; name: string; uiName: string; + newGroup?: boolean; + display?: boolean; // Controls whether the tab is displayed, defaults to true } export type Props = { @@ -26,13 +27,49 @@ export type Props = { }; export default class SettingsSidebar extends React.PureComponent { - buttonRefs: Array>; - totalTabs: Tab[]; + buttonRefs: Map>; constructor(props: Props) { super(props); - this.totalTabs = [...this.props.tabs, ...this.props.pluginTabs || []]; - this.buttonRefs = this.totalTabs.map(() => React.createRef()); + + // Initialize an empty Map for button refs + this.buttonRefs = new Map(); + + // Initialize refs for all tabs + this.initializeButtonRefs(props.tabs, props.pluginTabs); + } + + // Initialize or update button refs for all tabs + private initializeButtonRefs(tabs: Tab[], pluginTabs?: Tab[]) { + // Clear existing refs if reinitializing + this.buttonRefs.clear(); + + // Create refs for all tabs, regardless of display status + tabs.forEach((tab) => { + this.buttonRefs.set(tab.name, React.createRef()); + }); + + // Create refs for plugin tabs if they exist + if (pluginTabs?.length) { + pluginTabs.forEach((tab) => { + this.buttonRefs.set(tab.name, React.createRef()); + }); + } + } + + // Update refs when props change + componentDidUpdate(prevProps: Props) { + // Check if tabs or pluginTabs have changed + if (prevProps.tabs !== this.props.tabs || prevProps.pluginTabs !== this.props.pluginTabs) { + this.initializeButtonRefs(this.props.tabs, this.props.pluginTabs); + } + } + + // Get all visible tabs in the correct order + private getVisibleTabs(): Tab[] { + const visibleTabs = this.props.tabs.filter((tab) => tab.display !== false); + const visiblePluginTabs = this.props.pluginTabs?.filter((tab) => tab.display !== false) || []; + return [...visibleTabs, ...visiblePluginTabs]; } public handleClick = (tab: Tab, e: React.MouseEvent) => { @@ -41,27 +78,57 @@ export default class SettingsSidebar extends React.PureComponent { (e.target as Element).closest('.settings-modal')?.classList.add('display--content'); }; - public handleKeyUp = (index: number, e: React.KeyboardEvent) => { + public handleKeyUp = (tab: Tab, e: React.KeyboardEvent) => { + // Only handle UP and DOWN arrow keys + if (!isKeyPressed(e, Constants.KeyCodes.UP) && !isKeyPressed(e, Constants.KeyCodes.DOWN)) { + return; + } + + // Prevent default behavior + e.preventDefault(); + + // Get all visible tabs + const visibleTabs = this.getVisibleTabs(); + + // If no tabs are visible, do nothing + if (visibleTabs.length === 0) { + return; + } + + // Find the current tab's position in the visible tabs + const currentIndex = visibleTabs.findIndex((t) => t.name === tab.name); + + // If tab not found in visible tabs, do nothing + if (currentIndex === -1) { + return; + } + + let nextIndex: number; + + // Determine which tab to focus based on the key pressed if (isKeyPressed(e, Constants.KeyCodes.UP)) { - if (index > 0) { - this.props.updateTab(this.totalTabs[index - 1].name); - a11yFocus(this.buttonRefs[index - 1].current); - } else { - this.props.updateTab(this.totalTabs[this.totalTabs.length - 1].name); - a11yFocus(this.buttonRefs[this.buttonRefs.length - 1].current); - } - } else if (isKeyPressed(e, Constants.KeyCodes.DOWN)) { - if (index < this.totalTabs.length - 1) { - this.props.updateTab(this.totalTabs[index + 1].name); - a11yFocus(this.buttonRefs[index + 1].current); - } else { - this.props.updateTab(this.totalTabs[0].name); - a11yFocus(this.buttonRefs[0].current); - } + // UP arrow key - move to previous tab or wrap to last + nextIndex = currentIndex > 0 ? currentIndex - 1 : visibleTabs.length - 1; + } else { + // DOWN arrow key - move to next tab or wrap to first + nextIndex = currentIndex < visibleTabs.length - 1 ? currentIndex + 1 : 0; + } + + // Get the target tab + const targetTab = visibleTabs[nextIndex]; + + // Update the active tab + this.props.updateTab(targetTab.name); + + // Focus the target tab button directly + const targetButton = this.buttonRefs.get(targetTab.name)?.current; + if (targetButton) { + // Use direct focus instead of a11yFocus to ensure Cypress tests can detect the focus change + targetButton.focus(); } }; - private renderTab(tab: Tab, index: number) { + private renderTab(tab: Tab) { const key = `${tab.name}_li`; const isActive = this.props.activeTab === tab.name; @@ -84,52 +151,63 @@ export default class SettingsSidebar extends React.PureComponent { } return ( - + + {tab.newGroup &&
} + +
); } public render() { - const tabList = this.props.tabs.map((tab, index) => this.renderTab(tab, index)); + // Filter regular tabs and plugin tabs separately for rendering + const visibleTabs = this.props.tabs.filter((tab) => tab.display !== false); + + // Map regular tabs + const tabList = visibleTabs.map((tab) => this.renderTab(tab)); + let pluginTabList: React.ReactNode; if (this.props.pluginTabs?.length) { - pluginTabList = ( - <> -
-
+ const visiblePluginTabs = this.props.pluginTabs.filter((tab) => tab.display !== false); + if (visiblePluginTabs.length) { + pluginTabList = ( + <> +
- +
+ +
+ {visiblePluginTabs.map((tab) => this.renderTab(tab))}
- {this.props.pluginTabs.map((tab, index) => this.renderTab(tab, index + this.props.tabs.length))} -
- - ); + + ); + } } return ( diff --git a/webapp/channels/src/components/start_trial_form_modal/__snapshots__/start_trial_form_modal.test.tsx.snap b/webapp/channels/src/components/start_trial_form_modal/__snapshots__/start_trial_form_modal.test.tsx.snap index b31442d390c..3c33a6bec3b 100644 --- a/webapp/channels/src/components/start_trial_form_modal/__snapshots__/start_trial_form_modal.test.tsx.snap +++ b/webapp/channels/src/components/start_trial_form_modal/__snapshots__/start_trial_form_modal.test.tsx.snap @@ -121,7 +121,6 @@ Object { role="alert" >