diff --git a/SELF_HOSTED_INSTRUCTIONS.md b/SELF_HOSTED_INSTRUCTIONS.md index 00af7534e1..26b6c47df7 100644 --- a/SELF_HOSTED_INSTRUCTIONS.md +++ b/SELF_HOSTED_INSTRUCTIONS.md @@ -64,7 +64,7 @@ mv prod latest && rm uwazi.tgz You should now be able to run an Uwazi instance in production mode: -`DATABASE_NAME=my_db_name INDEX_NAME=my_db_name NODE_ENV=production FILES_ROOT_PATH=/xxxx/yyyy/uwazi/ node server.js` +`DATABASE_NAME=my_db_name INDEX_NAME=my_db_name NODE_ENV=production FILES_ROOT_PATH=/xxxx/yyyy/uwazi/ node server.js --no-experimental-fetch` By default, Uwazi runs on `localhost` port 3000, so point your browser to http://localhost:3000 and authenticate yourself with the default username "admin" and password "change this password now". diff --git a/app/api/services/informationextraction/InformationExtraction.ts b/app/api/services/informationextraction/InformationExtraction.ts index b7bd04f7fa..97d2d654b2 100644 --- a/app/api/services/informationextraction/InformationExtraction.ts +++ b/app/api/services/informationextraction/InformationExtraction.ts @@ -399,9 +399,12 @@ class InformationExtraction { if (propertyTypeIsSelectOrMultiSelect(property.type)) { const thesauri = await dictionatiesModel.getById(property.content); + const [groups, rootValues] = _.partition(thesauri?.values || [], r => r.values); + const groupedValues = groups.map(group => group.values || []).flat(); + const allValues = rootValues.concat(groupedValues); params.options = - thesauri?.values?.map(value => ({ label: value.label, id: value.id as string })) || []; + allValues.map(value => ({ label: value.label, id: value.id as string })) || []; } if (property.type === 'relationship') { const candidates = await fetchCandidates(property); diff --git a/app/api/services/informationextraction/specs/InformationExtraction.spec.ts b/app/api/services/informationextraction/specs/InformationExtraction.spec.ts index 513079e23c..233dd246bd 100644 --- a/app/api/services/informationextraction/specs/InformationExtraction.spec.ts +++ b/app/api/services/informationextraction/specs/InformationExtraction.spec.ts @@ -360,6 +360,14 @@ describe('InformationExtraction', () => { id: 'C', label: 'C', }, + { + id: '1A', + label: '1A', + }, + { + id: '1B', + label: '1B', + }, ], }, tenant: 'tenant1', diff --git a/app/api/services/informationextraction/specs/fixtures.ts b/app/api/services/informationextraction/specs/fixtures.ts index fd87182dee..b98f1db615 100644 --- a/app/api/services/informationextraction/specs/fixtures.ts +++ b/app/api/services/informationextraction/specs/fixtures.ts @@ -765,7 +765,7 @@ const fixtures: DBFixture = { }), ]), ], - dictionaries: [factory.thesauri('thesauri1', ['A', 'B', 'C'])], + dictionaries: [factory.nestedThesauri('thesauri1', ['A', 'B', 'C', { 1: ['1A', '1B'] }])], }; export { fixtures, factory }; diff --git a/app/api/suggestions/specs/fixtures.ts b/app/api/suggestions/specs/fixtures.ts index 42b54dfbc2..5b201df0ab 100644 --- a/app/api/suggestions/specs/fixtures.ts +++ b/app/api/suggestions/specs/fixtures.ts @@ -1400,6 +1400,8 @@ const selectAcceptanceFixtureBase: DBFixture = { factory.v2.database.translationDBO('A', 'Aes', 'es', dictionaryTranslationContext), factory.v2.database.translationDBO('B', 'B', 'en', dictionaryTranslationContext), factory.v2.database.translationDBO('B', 'Bes', 'es', dictionaryTranslationContext), + factory.v2.database.translationDBO('1', '1', 'en', dictionaryTranslationContext), + factory.v2.database.translationDBO('1', '1es', 'es', dictionaryTranslationContext), factory.v2.database.translationDBO('1A', '1A', 'en', dictionaryTranslationContext), factory.v2.database.translationDBO('1A', '1Aes', 'es', dictionaryTranslationContext), factory.v2.database.translationDBO('1B', '1B', 'en', dictionaryTranslationContext), diff --git a/app/api/suggestions/specs/suggestions.spec.ts b/app/api/suggestions/specs/suggestions.spec.ts index fcf5135583..cd54b6124d 100644 --- a/app/api/suggestions/specs/suggestions.spec.ts +++ b/app/api/suggestions/specs/suggestions.spec.ts @@ -922,12 +922,23 @@ describe('suggestions', () => { const { acceptedSuggestion, metadataValues, allFiles } = await prepareAndAcceptSelectSuggestion('A', 'en', 'property_select', 'select_extractor'); expect(acceptedSuggestion.state).toEqual(matchState()); - expect(metadataValues).toMatchObject([ + expect(metadataValues).toEqual([ [{ value: 'A', label: 'A' }], [{ value: 'A', label: 'Aes' }], ]); expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); }); + + it('should handle grouped values', async () => { + const { acceptedSuggestion, metadataValues, allFiles } = + await prepareAndAcceptSelectSuggestion('1A', 'en', 'property_select', 'select_extractor'); + expect(acceptedSuggestion.state).toEqual(matchState()); + expect(metadataValues).toEqual([ + [{ value: '1A', label: '1A', parent: { value: '1', label: '1' } }], + [{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }], + ]); + expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); + }); }); describe('multiselect', () => { @@ -1022,14 +1033,14 @@ describe('suggestions', () => { 'multiselect_extractor' ); expect(acceptedSuggestion.state).toEqual(matchState()); - expect(metadataValues).toMatchObject([ + expect(metadataValues).toEqual([ [ - { value: '1A', label: '1A' }, - { value: '1B', label: '1B' }, + { value: '1A', label: '1A', parent: { value: '1', label: '1' } }, + { value: '1B', label: '1B', parent: { value: '1', label: '1' } }, ], [ - { value: '1A', label: '1Aes' }, - { value: '1B', label: '1Bes' }, + { value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }, + { value: '1B', label: '1Bes', parent: { value: '1', label: '1es' } }, ], ]); expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); @@ -1047,15 +1058,15 @@ describe('suggestions', () => { } ); expect(acceptedSuggestion.state).toEqual(matchState(false)); - expect(metadataValues).toMatchObject([ + expect(metadataValues).toEqual([ [ { value: 'A', label: 'A' }, - { value: '1A', label: '1A' }, + { value: '1A', label: '1A', parent: { value: '1', label: '1' } }, { value: 'B', label: 'B' }, ], [ { value: 'A', label: 'Aes' }, - { value: '1A', label: '1Aes' }, + { value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }, { value: 'B', label: 'Bes' }, ], ]); @@ -1074,14 +1085,14 @@ describe('suggestions', () => { } ); expect(acceptedSuggestion.state).toEqual(matchState(false)); - expect(metadataValues).toMatchObject([ + expect(metadataValues).toEqual([ [ { value: 'A', label: 'A' }, - { value: '1A', label: '1A' }, + { value: '1A', label: '1A', parent: { value: '1', label: '1' } }, ], [ { value: 'A', label: 'Aes' }, - { value: '1A', label: '1Aes' }, + { value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }, ], ]); expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); @@ -1099,9 +1110,9 @@ describe('suggestions', () => { } ); expect(acceptedSuggestion.state).toEqual(matchState(false)); - expect(metadataValues).toMatchObject([ - [{ value: '1A', label: '1A' }], - [{ value: '1A', label: '1Aes' }], + expect(metadataValues).toEqual([ + [{ value: '1A', label: '1A', parent: { value: '1', label: '1' } }], + [{ value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }], ]); expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); }); @@ -1118,14 +1129,14 @@ describe('suggestions', () => { } ); expect(acceptedSuggestion.state).toEqual(matchState()); - expect(metadataValues).toMatchObject([ + expect(metadataValues).toEqual([ [ { value: 'A', label: 'A' }, - { value: '1A', label: '1A' }, + { value: '1A', label: '1A', parent: { value: '1', label: '1' } }, ], [ { value: 'A', label: 'Aes' }, - { value: '1A', label: '1Aes' }, + { value: '1A', label: '1Aes', parent: { value: '1', label: '1es' } }, ], ]); expect(allFiles).toEqual(selectAcceptanceFixtureBase.files); diff --git a/app/react/V2/Components/Forms/MultiselectList.tsx b/app/react/V2/Components/Forms/MultiselectList.tsx index f1d172f781..c198036e42 100644 --- a/app/react/V2/Components/Forms/MultiselectList.tsx +++ b/app/react/V2/Components/Forms/MultiselectList.tsx @@ -27,6 +27,7 @@ interface MultiselectListProps { foldableGroups?: boolean; singleSelect?: boolean; allowSelelectAll?: boolean; + startOnSelected?: boolean; } const SelectedCounter = ({ selectedItems }: { selectedItems: string[] }) => ( @@ -46,13 +47,24 @@ const MultiselectList = ({ foldableGroups = false, singleSelect = false, allowSelelectAll = false, + startOnSelected = false, }: MultiselectListProps) => { const [selectedItems, setSelectedItems] = useState(value || []); - const [showAll, setShowAll] = useState(true); + const [showAll, setShowAll] = useState(!(startOnSelected && selectedItems.length)); const [searchTerm, setSearchTerm] = useState(''); const [filteredItems, setFilteredItems] = useState(items); const [openGroups, setOpenGroups] = useState([]); + useEffect(() => { + if (startOnSelected) { + const groupsToExpand = items + .filter(item => item.items?.some(childItem => value?.includes(childItem.value))) + .map(item => item.value); + + setOpenGroups(groupsToExpand); + } + }, [items, value, startOnSelected]); + useEffect(() => { if (value) { setSelectedItems(value); @@ -251,12 +263,13 @@ const MultiselectList = ({ { label: All, value: 'true', - defaultChecked: true, + defaultChecked: !startOnSelected, }, { label: , value: 'false', disabled: selectedItems.length === 0, + defaultChecked: startOnSelected, }, ]} onChange={applyFilter} diff --git a/app/react/V2/Routes/Settings/IX/components/ExtractorModal.tsx b/app/react/V2/Routes/Settings/IX/components/ExtractorModal.tsx index 2549f6cd2f..9bee6c7303 100644 --- a/app/react/V2/Routes/Settings/IX/components/ExtractorModal.tsx +++ b/app/react/V2/Routes/Settings/IX/components/ExtractorModal.tsx @@ -189,6 +189,7 @@ const ExtractorModal = ({ checkboxes foldableGroups allowSelelectAll={values.length > 0} + startOnSelected={values.length > 0} />
diff --git a/app/react/V2/Routes/Settings/IX/components/Icons.tsx b/app/react/V2/Routes/Settings/IX/components/Icons.tsx index daab3aa486..1fade09fdb 100644 --- a/app/react/V2/Routes/Settings/IX/components/Icons.tsx +++ b/app/react/V2/Routes/Settings/IX/components/Icons.tsx @@ -9,13 +9,13 @@ import { } from 'V2/Components/CustomIcons'; const propertyIcons = { - text: , - date: , - numeric: , - markdown: , - select: , - multiselect: , - relationship: , + text: , + date: , + numeric: , + markdown: , + select: , + multiselect: , + relationship: , }; export { propertyIcons }; diff --git a/app/react/V2/Routes/Settings/IX/components/SuggestedValue.tsx b/app/react/V2/Routes/Settings/IX/components/SuggestedValue.tsx index b9c66d377e..e8b5a53a73 100644 --- a/app/react/V2/Routes/Settings/IX/components/SuggestedValue.tsx +++ b/app/react/V2/Routes/Settings/IX/components/SuggestedValue.tsx @@ -8,6 +8,7 @@ import { EntitySuggestionType } from 'shared/types/suggestionType'; import { ClientTemplateSchema } from 'app/istore'; import { Translate } from 'app/I18N'; import { thesauriAtom } from 'V2/atoms'; +import { ClientThesaurus, ClientThesaurusValue } from 'app/apiResponseTypes'; const SuggestedValue = ({ value, @@ -44,6 +45,23 @@ const SuggestedValue = ({ const { content, type } = property || {}; const thesaurus = thesauris.find(t => t._id === content); + const getLabelFromThesaurus = (id: string, _thesaurus: ClientThesaurus | undefined) => { + if (!_thesaurus) { + return ''; + } + + const flattenedValues = _thesaurus.values.reduce((acc: any, v) => { + if (v.values) { + return [...acc, ...v.values]; + } + return [...acc, v]; + }, []); + + const thesaurusValue = flattenedValues.find((v: ClientThesaurusValue) => v.id === id); + + return thesaurusValue?.label || ''; + }; + const getCurrentValue = () => { if (value === '' || value === undefined) { return '-'; @@ -53,7 +71,7 @@ const SuggestedValue = ({ } if (type === 'select' || type === 'multiselect' || type === 'relationship') { - const label = thesaurus?.values.find(v => v.id === value)?.label; + const label = getLabelFromThesaurus(value as string, thesaurus); return {label}; } @@ -68,7 +86,7 @@ const SuggestedValue = ({ return secondsToDate((suggestion.suggestedValue as string | number) || '', locale); } if (type === 'select' || type === 'multiselect' || type === 'relationship') { - const label = thesaurus?.values.find(v => v.id === suggestion.suggestedValue)?.label; + const label = getLabelFromThesaurus(suggestion.suggestedValue as string, thesaurus); return {label}; } return suggestion.suggestedValue!.toString(); diff --git a/app/react/stories/Forms/MultiselectList.stories.tsx b/app/react/stories/Forms/MultiselectList.stories.tsx index 5c8617f2e2..cbaeed3e62 100644 --- a/app/react/stories/Forms/MultiselectList.stories.tsx +++ b/app/react/stories/Forms/MultiselectList.stories.tsx @@ -15,7 +15,7 @@ const Primary: Story = { render: args => (
-
+
@@ -39,6 +41,7 @@ const Basic: Story = { foldableGroups: true, hasErrors: false, allowSelelectAll: false, + startOnSelected: false, items: [ { searchLabel: 'Someone', label: 'Someone', value: 'someone' }, { searchLabel: 'Another', label: 'Another', value: 'another' }, @@ -121,6 +124,47 @@ const WithGroups: Story = { }, }; -export { Basic, WithError, WithGroups }; +const InitialState: Story = { + ...Primary, + args: { + ...Basic.args, + value: ['red', 'orange', 'banana'], + startOnSelected: true, + items: [ + { + searchLabel: 'Colors', + label: 'Colors', + value: 'colors', + items: [ + { searchLabel: 'Red', label: 'Red', value: 'red' }, + { searchLabel: 'Blue', label: 'Blue', value: 'blue' }, + { searchLabel: 'Green', label: 'Green', value: 'green' }, + ], + }, + { + searchLabel: 'Animals', + label: 'Animals', + value: 'animals', + items: [ + { searchLabel: 'Dog', label: 'Dog', value: 'dog' }, + { searchLabel: 'Cat', label: 'Cat', value: 'cat' }, + { searchLabel: 'Bird', label: 'Bird', value: 'bird' }, + ], + }, + { + searchLabel: 'Fruits', + label: 'Fruits', + value: 'fruits', + items: [ + { searchLabel: 'Apple', label: 'Apple', value: 'apple' }, + { searchLabel: 'Banana', label: 'Banana', value: 'banana' }, + { searchLabel: 'Orange', label: 'Orange', value: 'orange' }, + ], + }, + ], + }, +}; + +export { Basic, WithError, WithGroups, InitialState }; export default meta; diff --git a/cypress/e2e/settings/information-extraction.cy.ts b/cypress/e2e/settings/information-extraction.cy.ts index b1b21cadc6..1e5a65935c 100644 --- a/cypress/e2e/settings/information-extraction.cy.ts +++ b/cypress/e2e/settings/information-extraction.cy.ts @@ -157,8 +157,9 @@ describe('Information Extraction', () => { cy.contains('button', 'Edit Extractor').click(); cy.getByTestId('modal').within(() => { cy.get('input[id="extractor-name"]').type(' edited', { delay: 0 }); + cy.get('label[for="filter_true"]').click(); editPropertyForExtractor('ordenesDeLaCorte', 'Ordenes de la corte', 'Title'); - editPropertyForExtractor('causa', 'Causa', 'Title'); + editPropertyForExtractor('causa', 'Causa', 'Title', false); cy.contains('button', 'Next').click(); checkTemplatesList(['Ordenes de la corte', 'Ordenes del presidente']); cy.contains('button', 'Update').click(); diff --git a/package.json b/package.json index a7b5506f18..2e1be31632 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.176.3", + "version": "1.177.0", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react" @@ -147,7 +147,7 @@ "filesize": "^10.1.0", "flag-icon-css": "^4.1.7", "flowbite": "^2.3.0", - "flowbite-datepicker": "^1.2.6", + "flowbite-datepicker": "^1.2.7", "flowbite-react": "^0.10.1", "formatcoords": "^1.1.3", "franc": "5.0.0", @@ -187,10 +187,10 @@ "pdfjs-dist": "^4.3.136", "postcss-loader": "^8.1.1", "postcss-prefix-selector": "^1.16.0", - "prom-client": "^15.1.2", + "prom-client": "^15.1.3", "prop-types": "^15.8.1", "qrcode.react": "^3.1.0", - "qs": "^6.12.1", + "qs": "^6.12.3", "react": "^18.3.1", "react-color": "^2.19.3", "react-datepicker": "6.9.0", @@ -252,7 +252,7 @@ "@babel/eslint-parser": "7.24.7", "@babel/helper-call-delegate": "^7.12.13", "@babel/helper-get-function-arity": "^7.16.7", - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/parser": "^7.24.7", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", @@ -306,7 +306,7 @@ "@types/node-uuid": "^0.0.28", "@types/nodemailer": "^6.4.15", "@types/prop-types": "^15.7.3", - "@types/qs": "^6.9.14", + "@types/qs": "^6.9.15", "@types/react": "^18.3.1", "@types/react-dnd": "^3.0.2", "@types/react-dom": "^18.0.9", diff --git a/yarn.lock b/yarn.lock index b7217c1d1c..e3b2a5db8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,10 +899,10 @@ dependencies: "@babel/types" "^7.24.7" -"@babel/helper-string-parser@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" - integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== +"@babel/helper-string-parser@^7.24.7", "@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" @@ -5379,10 +5379,10 @@ dependencies: "@types/node" "*" -"@types/qs@*", "@types/qs@^6.9.14", "@types/qs@^6.9.5": - version "6.9.14" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b" - integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA== +"@types/qs@*", "@types/qs@^6.9.15", "@types/qs@^6.9.5": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== "@types/range-parser@*": version "1.2.3" @@ -10125,10 +10125,10 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.206.0.tgz#f4f794f8026535278393308e01ea72f31000bfef" integrity sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w== -flowbite-datepicker@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/flowbite-datepicker/-/flowbite-datepicker-1.2.6.tgz#97fbd46496ec72f0322e5ea4d3d2462437ec1c0e" - integrity sha512-UbU/xXs9HFiwWfL4M1vpwIo8EpS0NUQSOvYnp0Z9u3N118nU7lPFGoUOq7su9d0aOJy9FssXzx1SZwN8MXhE1g== +flowbite-datepicker@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/flowbite-datepicker/-/flowbite-datepicker-1.2.7.tgz#274cc882a6a71ecffdca39b20409e986d40ebbdf" + integrity sha512-lT0SOrux9Hu9TMO1EG03r9B8VMppRr0wmz+M4YC9qVR0BCG83Qaky4HGHcAehcoBSXh13DfIg0jFKwSxWZuXBA== dependencies: flowbite "^2.0.0" @@ -14868,10 +14868,10 @@ progress@2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prom-client@^15.1.2: - version "15.1.2" - resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.2.tgz#78d79f12c35d395ca97edf7111c18210cf07f815" - integrity sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ== +prom-client@^15.1.3: + version "15.1.3" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2" + integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g== dependencies: "@opentelemetry/api" "^1.4.0" tdigest "^0.1.1" @@ -15027,10 +15027,10 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.10.0, qs@^6.11.0, qs@^6.11.2, qs@^6.12.1: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== +qs@^6.10.0, qs@^6.11.0, qs@^6.11.2, qs@^6.12.3: + version "6.12.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.3.tgz#e43ce03c8521b9c7fd7f1f13e514e5ca37727754" + integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ== dependencies: side-channel "^1.0.6"