refactor(editor): Remove user activation modal (no-changelog) (#6361)

* Remove user activation modal

* remove export from index.ts

* Update pnpm-lock.yaml

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Ricardo Espinoza 2023-06-05 13:47:06 -04:00 committed by GitHub
parent dc58340eee
commit e95e8de500
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2 additions and 423 deletions

View file

@ -1,66 +0,0 @@
import { WorkflowPage, NDV, UserActivationSurveyModal } from '../pages';
import SettingsWithActivationModalEnabled from '../fixtures/Settings_user_activation_modal_enabled.json';
import { v4 as uuid } from 'uuid';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
const userActivationSurveyModal = new UserActivationSurveyModal();
const BASE_WEBHOOK_URL = 'http://localhost:5678/webhook';
describe('User activation survey', () => {
before(() => {
cy.skipSetup();
});
it('Should show activation survey', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.reply(SettingsWithActivationModalEnabled);
});
const path = uuid();
const method = 'GET';
workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.actions.openNode('Webhook');
//input http method
cy.getByTestId('parameter-input-httpMethod').click();
cy.getByTestId('parameter-input-httpMethod')
.find('.el-select-dropdown')
.find('.option-headline')
.contains(method)
.click();
//input path method
cy.getByTestId('parameter-input-path')
.find('.parameter-input')
.find('input')
.clear()
.type(path);
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.activateWorkflow();
cy.intercept('GET', '/rest/workflows').as('getWorkflows');
cy.intercept('GET', '/rest/credentials').as('getCredentials');
cy.intercept('GET', '/rest/active').as('getActive');
cy.request(method, `${BASE_WEBHOOK_URL}/${path}`).then((response) => {
expect(response.status).to.eq(200);
cy.visit('/');
cy.reload();
cy.wait(['@getWorkflows', '@getCredentials', '@getActive']);
userActivationSurveyModal.getters.modalContainer().should('be.visible');
userActivationSurveyModal.getters.feedbackInput().should('be.visible');
userActivationSurveyModal.getters.skipButton().should('be.visible');
userActivationSurveyModal.getters.feedbackInput().type('testing');
userActivationSurveyModal.getters.feedbackInput().should('have.value', 'testing');
userActivationSurveyModal.getters.sendFeedbackButton().click();
});
});
});

View file

@ -1,90 +0,0 @@
{
"data": {
"endpointWebhook": "webhook",
"endpointWebhookTest": "webhook-test",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveManualExecutions": false,
"executionTimeout": -1,
"maxExecutionTimeout": 3600,
"workflowCallerPolicyDefaultOption": "workflowsFromSameOwner",
"timezone": "America/New_York",
"urlBaseWebhook": "http://localhost:5678/",
"urlBaseEditor": "http://localhost:5678",
"versionCli": "0.221.2",
"oauthCallbackUrls": {
"oauth1": "http://localhost:5678/rest/oauth1-credential/callback",
"oauth2": "http://localhost:5678/rest/oauth2-credential/callback"
},
"versionNotifications": {
"enabled": true,
"endpoint": "https://api.n8n.io/api/versions/",
"infoUrl": "https://docs.n8n.io/release-notes/"
},
"instanceId": "c229842c6d1e217486d04caf7223758e08385156ca87a58286c850760c7161f4",
"telemetry": {
"enabled": true
},
"posthog": {
"enabled": false,
"apiHost": "https://ph.n8n.io",
"apiKey": "phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo",
"autocapture": false,
"disableSessionRecording": true,
"debug": false
},
"personalizationSurveyEnabled": false,
"userActivationSurveyEnabled": true,
"defaultLocale": "en",
"userManagement": {
"enabled": true,
"showSetupOnFirstLoad": false,
"smtpSetup": false
},
"sso": {
"saml": {
"loginEnabled": false,
"loginLabel": ""
},
"ldap": {
"loginEnabled": false,
"loginLabel": ""
}
},
"publicApi": {
"enabled": false,
"latestVersion": 1,
"path": "api",
"swaggerUi": {
"enabled": true
}
},
"workflowTagsDisabled": false,
"logLevel": "info",
"hiringBannerEnabled": true,
"templates": {
"enabled": true,
"host": "https://api.n8n.io/api/"
},
"onboardingCallPromptEnabled": true,
"executionMode": "regular",
"pushBackend": "sse",
"communityNodesEnabled": true,
"deployment": {
"type": "default"
},
"isNpmAvailable": false,
"allowedModules": {},
"enterprise": {
"sharing": true,
"ldap": true,
"saml": false,
"logStreaming": false,
"advancedExecutionFilters": false
},
"hideUsagePage": false,
"license": {
"environment": "production"
}
}
}

View file

@ -1,5 +1,3 @@
export * from './credentials-modal'; export * from './credentials-modal';
export * from './message-box'; export * from './message-box';
export * from './workflow-sharing-modal'; export * from './workflow-sharing-modal';
export * from './user-activation-survey-modal';

View file

@ -1,10 +0,0 @@
import { BasePage } from './../base';
export class UserActivationSurveyModal extends BasePage {
getters = {
modalContainer: () => cy.getByTestId('userActivationSurvey-modal').last(),
feedbackInput: () => cy.getByTestId('activation-feedback-input').find('textarea'),
sendFeedbackButton: () => cy.getByTestId('send-activation-feedback-button'),
skipButton: () => cy.getByTestId('skip-button'),
};
}

View file

@ -258,8 +258,6 @@ export class Server extends AbstractServer {
}, },
personalizationSurveyEnabled: personalizationSurveyEnabled:
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'), config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
userActivationSurveyEnabled:
config.getEnv('userActivationSurvey.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'), defaultLocale: config.getEnv('defaultLocale'),
userManagement: { userManagement: {
enabled: isUserManagementEnabled(), enabled: isUserManagementEnabled(),

View file

@ -1043,15 +1043,6 @@ export const schema = {
}, },
}, },
userActivationSurvey: {
enabled: {
doc: 'Whether user activation survey is enabled.',
format: Boolean,
default: true,
env: 'N8N_USER_ACTIVATION_SURVEY_ENABLED',
},
},
diagnostics: { diagnostics: {
enabled: { enabled: {
doc: 'Whether diagnostic mode is enabled.', doc: 'Whether diagnostic mode is enabled.',

View file

@ -118,7 +118,6 @@ export async function workflowExecutionCompleted(
await UserService.updateUserSettings(owner.id, { await UserService.updateUserSettings(owner.id, {
firstSuccessfulWorkflowId: workflowId, firstSuccessfulWorkflowId: workflowId,
userActivated: true, userActivated: true,
showUserActivationSurvey: true,
}); });
} }

View file

@ -33,10 +33,6 @@ export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'la
lastName: string; lastName: string;
} }
export class UserSettingsUpdatePayload { export class UserSettingsUpdatePayload {
@IsBoolean({ message: 'showUserActivationSurvey should be a boolean' })
@IsOptional()
showUserActivationSurvey: boolean;
@IsBoolean({ message: 'userActivated should be a boolean' }) @IsBoolean({ message: 'userActivated should be a boolean' })
@IsOptional() @IsOptional()
userActivated: boolean; userActivated: boolean;

View file

@ -47,7 +47,6 @@
"@jsplumb/core": "^5.13.2", "@jsplumb/core": "^5.13.2",
"@jsplumb/util": "^5.13.2", "@jsplumb/util": "^5.13.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"canvas-confetti": "^1.6.0",
"codemirror-lang-html-n8n": "^1.0.0", "codemirror-lang-html-n8n": "^1.0.0",
"codemirror-lang-n8n-expression": "^0.2.0", "codemirror-lang-n8n-expression": "^0.2.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",

View file

@ -71,7 +71,6 @@ const defaultSettings: IN8nUISettings = {
variables: { variables: {
limit: -1, limit: -1,
}, },
userActivationSurveyEnabled: false,
deployment: { deployment: {
type: 'default', type: 'default',
}, },

View file

@ -31,7 +31,6 @@ export const waitAllPromises = async () => new Promise((resolve) => setTimeout(r
export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = { export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
settings: { settings: {
userActivationSurveyEnabled: false,
allowedModules: {}, allowedModules: {},
communityNodesEnabled: false, communityNodesEnabled: false,
defaultLocale: '', defaultLocale: '',

View file

@ -91,10 +91,6 @@
<ImportCurlModal /> <ImportCurlModal />
</ModalRoot> </ModalRoot>
<ModalRoot :name="USER_ACTIVATION_SURVEY_MODAL">
<WorkflowSuccessModal />
</ModalRoot>
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY"> <ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
<template #default="{ modalName, activeId, mode }"> <template #default="{ modalName, activeId, mode }">
<CommunityPackageManageConfirmModal <CommunityPackageManageConfirmModal
@ -149,7 +145,6 @@ import {
IMPORT_CURL_MODAL_KEY, IMPORT_CURL_MODAL_KEY,
LOG_STREAM_MODAL_KEY, LOG_STREAM_MODAL_KEY,
ASK_AI_MODAL_KEY, ASK_AI_MODAL_KEY,
USER_ACTIVATION_SURVEY_MODAL,
VERSION_CONTROL_PUSH_MODAL_KEY, VERSION_CONTROL_PUSH_MODAL_KEY,
} from '@/constants'; } from '@/constants';
@ -175,7 +170,6 @@ import ExecutionsModal from './ExecutionsModal.vue';
import ActivationModal from './ActivationModal.vue'; import ActivationModal from './ActivationModal.vue';
import ImportCurlModal from './ImportCurlModal.vue'; import ImportCurlModal from './ImportCurlModal.vue';
import WorkflowShareModal from './WorkflowShareModal.ee.vue'; import WorkflowShareModal from './WorkflowShareModal.ee.vue';
import WorkflowSuccessModal from './UserActivationSurveyModal.vue';
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue'; import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
import VersionControlPushModal from '@/components/VersionControlPushModal.ee.vue'; import VersionControlPushModal from '@/components/VersionControlPushModal.ee.vue';
@ -205,7 +199,6 @@ export default defineComponent({
WorkflowShareModal, WorkflowShareModal,
ImportCurlModal, ImportCurlModal,
EventDestinationSettingsModal, EventDestinationSettingsModal,
WorkflowSuccessModal,
VersionControlPushModal, VersionControlPushModal,
}, },
data: () => ({ data: () => ({
@ -231,7 +224,6 @@ export default defineComponent({
WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY,
IMPORT_CURL_MODAL_KEY, IMPORT_CURL_MODAL_KEY,
LOG_STREAM_MODAL_KEY, LOG_STREAM_MODAL_KEY,
USER_ACTIVATION_SURVEY_MODAL,
VERSION_CONTROL_PUSH_MODAL_KEY, VERSION_CONTROL_PUSH_MODAL_KEY,
}), }),
}); });

View file

@ -1,181 +0,0 @@
<template>
<Modal
width="500px"
:title="locale.baseText('userActivationSurveyModal.title')"
:eventBus="modalBus"
:name="USER_ACTIVATION_SURVEY_MODAL"
:center="true"
:beforeClose="beforeClosingModal"
>
<template #content>
<div :class="$style.container">
<div :class="$style.description">
<i18n path="userActivationSurveyModal.description.workflowRan">
<template #workflow>
<n8n-text :bold="true"> {{ workflowName }} </n8n-text>
</template>
<template #ranSuccessfully>
<n8n-text>
{{
locale.baseText('userActivationSurveyModal.description.workflowRanSuccessfully')
}}
</n8n-text>
</template>
<template #savedTime>
<n8n-text>
{{ locale.baseText('userActivationSurveyModal.description.savedTime') }}
</n8n-text>
</template>
</i18n>
</div>
<div :class="$style.form">
<n8n-input-label
:label="$locale.baseText('userActivationSurveyModal.form.label')"
color="text-dark"
>
<n8n-input
type="textarea"
:maxlength="FEEDBACK_MAX_LENGTH"
:rows="3"
v-model="feedback"
@input="onInput"
data-test-id="activation-feedback-input"
/>
</n8n-input-label>
</div>
</div>
</template>
<template #footer>
<div :class="$style.modalFooter">
<n8n-button
size="large"
type="secondary"
data-test-id="skip-button"
:label="locale.baseText('userActivationSurveyModal.form.button.skip')"
@click="onSkip"
/>
<n8n-button
:disabled="!hasAnyChanges"
@click="onShareFeedback"
size="large"
float="right"
:label="locale.baseText('userActivationSurveyModal.form.button.shareFeedback')"
data-test-id="send-activation-feedback-button"
/>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import Modal from '@/components/Modal.vue';
import { USER_ACTIVATION_SURVEY_MODAL } from '@/constants';
import { useUsersStore } from '@/stores/users.store';
import confetti from 'canvas-confetti';
import { telemetry } from '@/plugins/telemetry';
import { i18n as locale } from '@/plugins/i18n';
import { Notification } from 'element-ui';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from 'n8n-design-system';
const FEEDBACK_MAX_LENGTH = 300;
const userStore = useUsersStore();
const workflowStore = useWorkflowsStore();
const hasAnyChanges = ref(false);
const feedback = ref('');
const modalBus = createEventBus();
const workflowName = ref('');
onMounted(async () => {
const currentSettings = getCurrentSettings();
try {
const { name } = await workflowStore.fetchWorkflow(
currentSettings?.firstSuccessfulWorkflowId ?? '',
);
workflowName.value = name;
setTimeout(showConfetti, 500);
} catch (e) {}
});
const onShareFeedback = () => {
telemetry.track('User responded to activation modal', { response: getFeedback() });
showSharedFeedbackSuccess();
modalBus.emit('close');
};
const onSkip = () => {
modalBus.emit('close');
};
const getCurrentSettings = () => {
return userStore.currentUser?.settings;
};
const getFeedback = () => {
return feedback.value.slice(0, FEEDBACK_MAX_LENGTH);
};
const beforeClosingModal = async () => {
const currentUser = userStore.currentUser;
if (currentUser) {
try {
await userStore.updateUserSettings({ showUserActivationSurvey: false });
} catch {
showSharedFeedbackError();
return false;
}
}
return true;
};
const onInput = () => {
hasAnyChanges.value = true;
};
const showSharedFeedbackSuccess = () => {
Notification.success({
title: locale.baseText('userActivationSurveyModal.sharedFeedback.success'),
message: '',
position: 'bottom-right',
});
};
const showSharedFeedbackError = () => {
Notification.error({
title: locale.baseText('userActivationSurveyModal.sharedFeedback.error'),
message: '',
position: 'bottom-right',
});
};
const showConfetti = () => {
void confetti({
particleCount: 200,
spread: 100,
origin: { y: 0.6 },
zIndex: 2050,
colors: ['5C4EC2', 'D7E6F1', 'FF9284', '8D7FED', 'B8AFF9', 'FF6D5A'],
});
};
</script>
<style module lang="scss">
.form {
margin-top: var(--spacing-l);
}
.description > * {
font-size: var(--font-size-s);
}
.container > * {
margin-bottom: var(--spacing-s);
&:last-child {
margin-bottom: 0;
}
}
</style>

View file

@ -47,7 +47,6 @@ export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm'; export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const IMPORT_CURL_MODAL_KEY = 'importCurl'; export const IMPORT_CURL_MODAL_KEY = 'importCurl';
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream'; export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
export const USER_ACTIVATION_SURVEY_MODAL = 'userActivationSurvey';
export const VERSION_CONTROL_PUSH_MODAL_KEY = 'versionControlPush'; export const VERSION_CONTROL_PUSH_MODAL_KEY = 'versionControlPush';

View file

@ -23,8 +23,7 @@ import { I18nPlugin, i18nInstance } from './plugins/i18n';
import { createPinia, PiniaVuePlugin } from 'pinia'; import { createPinia, PiniaVuePlugin } from 'pinia';
import { useWebhooksStore, useUsersStore } from '@/stores'; import { useWebhooksStore } from '@/stores';
import { VIEWS } from '@/constants';
Vue.config.productionTip = false; Vue.config.productionTip = false;
@ -43,10 +42,6 @@ new Vue({
router.afterEach((to, from) => { router.afterEach((to, from) => {
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to }); void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
const userStore = useUsersStore();
if (userStore.currentUser && to.name && to.name !== VIEWS.SIGNOUT && !to.name.includes('Modal')) {
void userStore.showUserActivationSurveyModal();
}
}); });
if (!import.meta.env.PROD) { if (!import.meta.env.PROD) {

View file

@ -1906,15 +1906,6 @@
"settings.sso.actionBox.title": "Available on Enterprise plan", "settings.sso.actionBox.title": "Available on Enterprise plan",
"settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.", "settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.",
"settings.sso.actionBox.buttonText": "See plans", "settings.sso.actionBox.buttonText": "See plans",
"userActivationSurveyModal.title": "Congrats! Your workflow ran automatically 👏",
"userActivationSurveyModal.description.workflowRan": "{workflow} {ranSuccessfully} {savedTime}",
"userActivationSurveyModal.description.workflowRanSuccessfully": "ran successfully",
"userActivationSurveyModal.description.savedTime": "for the first time. Looks like n8n just saved you some time! Can you help us make n8n even better and answer the following question?",
"userActivationSurveyModal.form.label": "What almost stopped you from creating this workflow?",
"userActivationSurveyModal.form.button.shareFeedback": "Share feedback",
"userActivationSurveyModal.form.button.skip": "Skip",
"userActivationSurveyModal.sharedFeedback.success": "Thanks for your feedback",
"userActivationSurveyModal.sharedFeedback.error": "Problem sharing feedback, try again",
"sso.login.divider": "or", "sso.login.divider": "or",
"sso.login.button": "Continue with SSO", "sso.login.button": "Continue with SSO",
"executionUsage.currentUsage": "{text} {count}", "executionUsage.currentUsage": "{text} {count}",

View file

@ -127,13 +127,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
this.settings.personalizationSurveyEnabled this.settings.personalizationSurveyEnabled
); );
}, },
isUserActivationSurveyEnabled(): boolean {
return (
this.settings.telemetry &&
this.settings.telemetry.enabled &&
this.settings.userActivationSurveyEnabled
);
},
telemetry(): ITelemetrySettings { telemetry(): ITelemetrySettings {
return this.settings.telemetry; return this.settings.telemetry;
}, },

View file

@ -30,7 +30,6 @@ import {
WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY,
WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY,
WORKFLOW_SHARE_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
USER_ACTIVATION_SURVEY_MODAL,
VERSION_CONTROL_PUSH_MODAL_KEY, VERSION_CONTROL_PUSH_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import type { import type {
@ -135,9 +134,6 @@ export const useUIStore = defineStore(STORES.UI, {
activeId: null, activeId: null,
showAuthSelector: false, showAuthSelector: false,
}, },
[USER_ACTIVATION_SURVEY_MODAL]: {
open: false,
},
[VERSION_CONTROL_PUSH_MODAL_KEY]: { [VERSION_CONTROL_PUSH_MODAL_KEY]: {
open: false, open: false,
}, },

View file

@ -22,7 +22,7 @@ import {
validatePasswordToken, validatePasswordToken,
validateSignupToken, validateSignupToken,
} from '@/api/users'; } from '@/api/users';
import { PERSONALIZATION_MODAL_KEY, USER_ACTIVATION_SURVEY_MODAL, STORES } from '@/constants'; import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
import type { import type {
ICredentialsResponse, ICredentialsResponse,
IInviteResponse, IInviteResponse,
@ -321,16 +321,6 @@ export const useUsersStore = defineStore(STORES.USERS, {
uiStore.openModal(PERSONALIZATION_MODAL_KEY); uiStore.openModal(PERSONALIZATION_MODAL_KEY);
} }
}, },
async showUserActivationSurveyModal() {
const settingsStore = useSettingsStore();
if (settingsStore.isUserActivationSurveyEnabled) {
const currentUser = this.currentUser;
if (currentUser?.settings?.showUserActivationSurvey) {
const uiStore = useUIStore();
uiStore.openModal(USER_ACTIVATION_SURVEY_MODAL);
}
}
},
async skipOwnerSetup(): Promise<void> { async skipOwnerSetup(): Promise<void> {
try { try {
const rootStore = useRootStore(); const rootStore = useRootStore();

View file

@ -2023,7 +2023,6 @@ export interface IUserManagementSettings {
export interface IUserSettings { export interface IUserSettings {
isOnboarded?: boolean; isOnboarded?: boolean;
showUserActivationSurvey?: boolean;
firstSuccessfulWorkflowId?: string; firstSuccessfulWorkflowId?: string;
userActivated?: boolean; userActivated?: boolean;
allowSSOManualLogin?: boolean; allowSSOManualLogin?: boolean;
@ -2072,7 +2071,6 @@ export interface IN8nUISettings {
debug: boolean; debug: boolean;
}; };
personalizationSurveyEnabled: boolean; personalizationSurveyEnabled: boolean;
userActivationSurveyEnabled: boolean;
defaultLocale: string; defaultLocale: string;
userManagement: IUserManagementSettings; userManagement: IUserManagementSettings;
sso: { sso: {

View file

@ -901,9 +901,6 @@ importers:
axios: axios:
specifier: ^0.21.1 specifier: ^0.21.1
version: 0.21.4(debug@4.3.2) version: 0.21.4(debug@4.3.2)
canvas-confetti:
specifier: ^1.6.0
version: 1.6.0
codemirror-lang-html-n8n: codemirror-lang-html-n8n:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -10361,10 +10358,6 @@ packages:
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==} resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
dev: true dev: true
/canvas-confetti@1.6.0:
resolution: {integrity: sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA==}
dev: false
/capital-case@1.0.4: /capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
dependencies: dependencies: