diff --git a/admin/.eslintrc.js b/admin/.eslintrc.js index 73dce291f..33ab3c0c5 100644 --- a/admin/.eslintrc.js +++ b/admin/.eslintrc.js @@ -39,7 +39,7 @@ module.exports = { { src: './src', extensions: ['.js', '.vue'], - ignores: [], + ignores: ['contributionLink.options.repetition.null'], enableFix: false, }, ], diff --git a/admin/jest.config.js b/admin/jest.config.js index b7226bd8f..9b9842bad 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -22,7 +22,7 @@ module.exports = { '^.+\\.(js|jsx)?$': 'babel-jest', '/node_modules/vee-validate/dist/rules': 'babel-jest', }, - setupFiles: ['/test/testSetup.js'], + setupFiles: ['/test/testSetup.js', 'jest-canvas-mock'], testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], diff --git a/admin/package.json b/admin/package.json index e36308fd9..cbbce5a2a 100644 --- a/admin/package.json +++ b/admin/package.json @@ -38,7 +38,9 @@ "graphql": "^15.6.1", "identity-obj-proxy": "^3.0.0", "jest": "26.6.3", + "jest-canvas-mock": "^2.3.1", "portal-vue": "^2.1.7", + "qrcanvas-vue": "2.1.1", "regenerator-runtime": "^0.13.9", "stats-webpack-plugin": "^0.7.0", "vue": "^2.6.11", diff --git a/admin/public/img/gdd-coin.png b/admin/public/img/gdd-coin.png new file mode 100644 index 000000000..32cb8b2b2 Binary files /dev/null and b/admin/public/img/gdd-coin.png differ diff --git a/admin/src/components/ContributionLink.spec.js b/admin/src/components/ContributionLink.spec.js new file mode 100644 index 000000000..66ea20ef9 --- /dev/null +++ b/admin/src/components/ContributionLink.spec.js @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils' +import ContributionLink from './ContributionLink.vue' + +const localVue = global.localVue + +const mocks = { + $t: jest.fn((t) => t), +} + +describe('ContributionLink', () => { + let wrapper + + const Wrapper = () => { + return mount(ContributionLink, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".contribution-link"', () => { + expect(wrapper.find('div.contribution-link').exists()).toBeTruthy() + }) + + it('emits toggle::collapse new Contribution', async () => { + wrapper.vm.editContributionLinkData() + expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() + }) + }) +}) diff --git a/admin/src/components/ContributionLink.vue b/admin/src/components/ContributionLink.vue new file mode 100644 index 000000000..02b525b44 --- /dev/null +++ b/admin/src/components/ContributionLink.vue @@ -0,0 +1,62 @@ + + diff --git a/admin/src/components/ContributionLinkForm.spec.js b/admin/src/components/ContributionLinkForm.spec.js new file mode 100644 index 000000000..195664a62 --- /dev/null +++ b/admin/src/components/ContributionLinkForm.spec.js @@ -0,0 +1,64 @@ +import { mount } from '@vue/test-utils' +import ContributionLinkForm from './ContributionLinkForm.vue' + +const localVue = global.localVue + +global.alert = jest.fn() + +const propsData = { + contributionLinkData: {}, +} + +const mocks = { + $t: jest.fn((t) => t), +} + +describe('ContributionLinkForm', () => { + let wrapper + + const Wrapper = () => { + return mount(ContributionLinkForm, { localVue, mocks, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".contribution-link-form"', () => { + expect(wrapper.find('div.contribution-link-form').exists()).toBe(true) + }) + + it('function onReset', () => { + beforeEach(() => { + wrapper.setData({ + form: { + name: 'name', + memo: 'memo', + amount: 100, + startDate: 'startDate', + endDate: 'endDate', + cycle: 'once', + repetition: '1', + maxAmount: 100, + }, + }) + wrapper.vm.onReset() + }) + expect(wrapper.vm.form).toEqual({ + amount: null, + cycle: 'once', + endDate: null, + maxAmount: null, + memo: null, + name: null, + repetition: null, + startDate: null, + }) + }) + + it('onSubmit valid form', () => { + wrapper.vm.onSubmit() + }) + }) +}) diff --git a/admin/src/components/ContributionLinkForm.vue b/admin/src/components/ContributionLinkForm.vue new file mode 100644 index 000000000..386ac8f9e --- /dev/null +++ b/admin/src/components/ContributionLinkForm.vue @@ -0,0 +1,212 @@ + + diff --git a/admin/src/components/ContributionLinkList.spec.js b/admin/src/components/ContributionLinkList.spec.js new file mode 100644 index 000000000..99847aa12 --- /dev/null +++ b/admin/src/components/ContributionLinkList.spec.js @@ -0,0 +1,150 @@ +import { mount } from '@vue/test-utils' +import ContributionLinkList from './ContributionLinkList.vue' +import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup' +// import { deleteContributionLink } from '../graphql/deleteContributionLink' + +const localVue = global.localVue + +const mockAPIcall = jest.fn() + +const mocks = { + $t: jest.fn((t) => t), + $apollo: { + mutate: mockAPIcall, + }, +} + +const propsData = { + items: [ + { + id: 1, + name: 'Meditation', + memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', + amount: '200', + startDate: '2022-04-01', + endDate: '2022-08-01', + cycle: 'täglich', + repetition: '3', + maxAmount: 0, + link: 'https://localhost/redeem/CL-1a2345678', + }, + ], +} + +describe('ContributionLinkList', () => { + let wrapper + + const Wrapper = () => { + return mount(ContributionLinkList, { localVue, mocks, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".contribution-link-list"', () => { + expect(wrapper.find('div.contribution-link-list').exists()).toBeTruthy() + }) + + describe('edit contribution link', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.vm.editContributionLink() + }) + it('emits editContributionLinkData', async () => { + expect(wrapper.vm.$emit('editContributionLinkData')).toBeTruthy() + }) + }) + + describe('delete contribution link', () => { + let spy + + beforeEach(async () => { + jest.clearAllMocks() + wrapper.vm.deleteContributionLink() + }) + + describe('with success', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) + mockAPIcall.mockResolvedValue() + await wrapper.find('.test-delete-link').trigger('click') + }) + + it('opens the modal ', () => { + expect(spy).toBeCalled() + }) + + // it('calls the API', () => { + // expect(mockAPIcall).toBeCalledWith( + // expect.objectContaining({ + // mutation: deleteContributionLink, + // variables: { + // id: 1, + // }, + // }), + // ) + // }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ') + }) + }) + + describe('with error', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) + mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' }) + await wrapper.find('.test-delete-link').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Something went wrong :(') + }) + }) + + describe('cancel delete', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(false)) + mockAPIcall.mockResolvedValue() + await wrapper.find('.test-delete-link').trigger('click') + }) + + it('does not call the API', () => { + expect(mockAPIcall).not.toBeCalled() + }) + }) + }) + + describe('show contribution link', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.setData({ + modalData: [ + { + id: 1, + name: 'Meditation', + memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', + amount: '200', + startDate: '2022-04-01', + endDate: '2022-08-01', + cycle: 'täglich', + repetition: '3', + maxAmount: 0, + link: 'https://localhost/redeem/CL-1a2345678', + }, + ], + }) + wrapper.vm.showContributionLink() + }) + + it('shows modalData', () => { + expect(wrapper.emitted('modalData')).toEqual() + }) + }) + }) +}) diff --git a/admin/src/components/ContributionLinkList.vue b/admin/src/components/ContributionLinkList.vue new file mode 100644 index 000000000..7483124fa --- /dev/null +++ b/admin/src/components/ContributionLinkList.vue @@ -0,0 +1,103 @@ + + diff --git a/admin/src/components/FigureQrCode.spec.js b/admin/src/components/FigureQrCode.spec.js new file mode 100644 index 000000000..2c18a31a1 --- /dev/null +++ b/admin/src/components/FigureQrCode.spec.js @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils' +import FigureQrCode from './FigureQrCode.vue' + +const localVue = global.localVue + +const propsData = { + link: '', +} + +describe('FigureQrCode', () => { + let wrapper + + const Wrapper = () => { + return mount(FigureQrCode, { localVue, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".figure-qr-code"', () => { + expect(wrapper.find('div.figure-qr-code').exists()).toBe(true) + }) + + it('renders the QRCanvas Element ".canvas"', () => { + expect(wrapper.find('.canvas').exists()).toBe(true) + }) + }) +}) diff --git a/admin/src/components/FigureQrCode.vue b/admin/src/components/FigureQrCode.vue new file mode 100644 index 000000000..eb5e07409 --- /dev/null +++ b/admin/src/components/FigureQrCode.vue @@ -0,0 +1,53 @@ + + + diff --git a/admin/src/components/Tables/OpenCreationsTable.spec.js b/admin/src/components/Tables/OpenCreationsTable.spec.js index 9ff348562..ad28a6fb5 100644 --- a/admin/src/components/Tables/OpenCreationsTable.spec.js +++ b/admin/src/components/Tables/OpenCreationsTable.spec.js @@ -69,6 +69,7 @@ const propsData = { { key: 'edit_creation', label: 'edit' }, { key: 'confirm', label: 'save' }, ], + toggleDetails: false, } const mocks = { @@ -125,5 +126,9 @@ describe('OpenCreationsTable', () => { expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy() }) }) + + it('funtion updateUserData', () => { + wrapper.vm.updateUserData([111, 222, 333], [444, 555, 666]) + }) }) }) diff --git a/admin/src/components/Tables/OpenCreationsTable.vue b/admin/src/components/Tables/OpenCreationsTable.vue index d2e9669e6..1e61f00b0 100644 --- a/admin/src/components/Tables/OpenCreationsTable.vue +++ b/admin/src/components/Tables/OpenCreationsTable.vue @@ -70,12 +70,23 @@ export default { required: true, }, }, + data() { + return { + creationUserData: { + amount: null, + date: null, + memo: null, + moderator: null, + }, + } + }, methods: { updateCreationData(data) { - this.creationUserData.amount = data.amount - this.creationUserData.date = data.date - this.creationUserData.memo = data.memo - this.creationUserData.moderator = data.moderator + this.creationUserData = data + // this.creationUserData.amount = data.amount + // this.creationUserData.date = data.date + // this.creationUserData.memo = data.memo + // this.creationUserData.moderator = data.moderator data.row.toggleDetails() }, updateUserData(rowItem, newCreation) { diff --git a/admin/src/graphql/createContributionLink.js b/admin/src/graphql/createContributionLink.js new file mode 100644 index 000000000..aaccf29b0 --- /dev/null +++ b/admin/src/graphql/createContributionLink.js @@ -0,0 +1,27 @@ +import gql from 'graphql-tag' + +export const createContributionLink = gql` + mutation ( + $startDate: String! + $endDate: String! + $name: String! + $amount: Decimal! + $memo: String! + $cycle: String + $repetition: String + $maxAmount: Decimal + ) { + createContributionLink( + startDate: $startDate + endDate: $endDate + name: $name + amount: $amount + memo: $memo + cycle: $cycle + repetition: $repetition + maxAmount: $maxAmount + ) { + link + } + } +` diff --git a/admin/src/graphql/deleteContributionLink.js b/admin/src/graphql/deleteContributionLink.js new file mode 100644 index 000000000..d0a938627 --- /dev/null +++ b/admin/src/graphql/deleteContributionLink.js @@ -0,0 +1,7 @@ +import gql from 'graphql-tag' + +export const deleteContributionLink = gql` + mutation ($id: Int!) { + deleteContributionLink(id: $id) + } +` diff --git a/admin/src/graphql/listContributionLinks.js b/admin/src/graphql/listContributionLinks.js new file mode 100644 index 000000000..d46564942 --- /dev/null +++ b/admin/src/graphql/listContributionLinks.js @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const listContributionLinks = gql` + query { + listContributionLinks { + id + startDate + endDate + name + memo + amount + cycle + repetition + maxAmount + link + } + } +` diff --git a/admin/src/graphql/showContributionLink.js b/admin/src/graphql/showContributionLink.js new file mode 100644 index 000000000..99dd4faef --- /dev/null +++ b/admin/src/graphql/showContributionLink.js @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const showContributionLink = gql` + query ($id: Int!) { + showContributionLink { + id + startDate + endDate + name + memo + amount + cycle + repetition + maxAmount + code + } + } +` diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index b667a1ada..6bf09b382 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -1,6 +1,37 @@ { "all_emails": "Alle Nutzer", "back": "zurück", + "contributionLink": { + "amount": "Betrag", + "clear": "Löschen", + "contributionLinks": "Beitragslinks", + "create": "Anlegen", + "cycle": "Zyklus", + "endDate": "Enddatum", + "maximumAmount": "maximaler Betrag", + "memo": "Nachricht", + "name": "Name", + "newContributionLink": "Neuer Beitragslink", + "noContributionLinks": "Es sind keine Beitragslinks angelegt.", + "noDateSelected": "Kein Datum ausgewählt", + "noEndDate": "Kein Enddatum gewählt.", + "noStartDate": "Kein Startdatum gewählt.", + "options": { + "cycle": { + "daily": "täglich", + "hourly": "stündlich", + "monthly": "monatlich", + "once": "einmalig", + "weekly": "wöchentlich", + "yearly": "jährlich" + }, + "repetition": { + "null": "Bitte wähle eine Wiederholung" + } + }, + "repetition": "Wiederholung", + "startDate": "Startdatum" + }, "creation": "Schöpfung", "creationList": "Schöpfungsliste", "creation_form": { @@ -44,7 +75,8 @@ "lastname": "Nachname", "math": { "exclaim": "!", - "pipe": "|" + "pipe": "|", + "plus": "+" }, "moderator": "Moderator", "multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.", diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 982c42d92..58fb264b9 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -1,6 +1,37 @@ { "all_emails": "All users", "back": "back", + "contributionLink": { + "amount": "Amount", + "clear": "Clear", + "contributionLinks": "Contribution Links", + "create": "Create", + "cycle": "Cycle", + "endDate": "End-Date", + "maximumAmount": "Maximum amount", + "memo": "Memo", + "name": "Name", + "newContributionLink": "New contribution link", + "noContributionLinks": "No contribution links have been created.", + "noDateSelected": "No date selected", + "noEndDate": "No end-date", + "noStartDate": "No start-date", + "options": { + "cycle": { + "daily": "daily", + "hourly": "hourly", + "monthly": "monthly", + "once": "unique", + "weekly": "weekly", + "yearly": "yearly" + }, + "repetition": { + "null": "please select a repetition" + } + }, + "repetition": "Repetition", + "startDate": "Start-date" + }, "creation": "Creation", "creationList": "Creation list", "creation_form": { @@ -44,7 +75,8 @@ "lastname": "Lastname", "math": { "exclaim": "!", - "pipe": "|" + "pipe": "|", + "plus": "+" }, "moderator": "Moderator", "multiple_creation_text": "Please select one or more members for which you would like to perform creations.", diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue index ed7ac8ad7..8cd1d559c 100644 --- a/admin/src/pages/Overview.vue +++ b/admin/src/pages/Overview.vue @@ -28,13 +28,24 @@ + diff --git a/admin/yarn.lock b/admin/yarn.lock index af1d18fa6..28f577a12 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -932,6 +932,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.11.2", "@babel/runtime@^7.16.0": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.14.0": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825" @@ -4397,7 +4404,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -4845,6 +4852,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg== + cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" @@ -7821,6 +7833,14 @@ javascript-stringify@^2.0.1: resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== +jest-canvas-mock@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341" + integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ== + dependencies: + cssfontparser "^1.2.1" + moo-color "^1.0.2" + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -9478,6 +9498,13 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +moo-color@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74" + integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ== + dependencies: + color-name "^1.1.4" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -10959,6 +10986,27 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qrcanvas-vue@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/qrcanvas-vue/-/qrcanvas-vue-2.1.1.tgz#27b449f99eaf46f324b300215469bfdf8ef77d88" + integrity sha512-86NMjOJ5XJGrrqrD2t+zmZxZKNuW1Is7o88UOiM8qFxDBjuTyfq9VJE9/2rN5XxThsjBuY4bRrQqL9blVwnI9w== + dependencies: + "@babel/runtime" "^7.16.0" + qrcanvas "^3.1.2" + +qrcanvas@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/qrcanvas/-/qrcanvas-3.1.2.tgz#81a25e91b2c27e9ace91da95591cbfb100d68702" + integrity sha512-lNcAyCHN0Eno/mJ5eBc7lHV/5ejAJxII0UELthG3bNnlLR+u8hCc7CR+hXBawbYUf96kNIosXfG2cJzx92ZWKg== + dependencies: + "@babel/runtime" "^7.11.2" + qrcode-generator "^1.4.4" + +qrcode-generator@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7" + integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw== + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"