diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2e6425172..dc841d10a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developers@coda.co. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@networkcanvas.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/build-resources/scripts/afterSignHook.js b/build-resources/scripts/afterSignHook.js index 15fe3bab0..13eca274a 100644 --- a/build-resources/scripts/afterSignHook.js +++ b/build-resources/scripts/afterSignHook.js @@ -5,11 +5,10 @@ const path = require('path'); const electronNotarize = require('@electron/notarize'); async function note(params) { - // Only notarize the app on Mac OS only. + // Only notarize the app on macOS. if (process.platform !== 'darwin') { return; } - console.log('afterSign hook triggered', params); const appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`); if (!fs.existsSync(appPath)) { @@ -23,9 +22,9 @@ async function note(params) { tool: 'notarytool', appBundleId: 'org.codaco.NetworkCanvasInterviewer6', appPath, - appleApiKey: '~/.private_keys/AuthKey_J58L47W6H9.p8', - appleApiKeyId: 'J58L47W6H9', // This is taken from the filename of the .p8 file in your icloud drive - appleApiIssuer: '69a6de92-60bf-47e3-e053-5b8c7c11a4d1', + appleApiKey: '~/.private_keys/AuthKey_A78M67RCH9.p8', + appleApiKeyId: 'A78M67RCH9', // Taken from https://appstoreconnect.apple.com/access/integrations/api + appleApiIssuer: '69a6de92-60bf-47e3-e053-5b8c7c11a4d1',// As above }); console.log('Done notarizing'); diff --git a/config.xml b/config.xml index 499ee606f..1f25c52ce 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - @@ -7,7 +7,7 @@ A tool for conducting Network Canvas Interviews. - + Complex Data Collective diff --git a/package-lock.json b/package-lock.json index 72bd56ae4..d3e705135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "dependencies": { "@babel/runtime": "7.10.1", "@xmldom/xmldom": "~0.8.10", @@ -30,7 +30,7 @@ "@codaco/eslint-plugin-spellcheck": "0.0.14", "@codaco/shared-consts": "~0.0.1-alpha.3", "@codaco/ui": "~5.8.5", - "@electron/notarize": "~1.2.3", + "@electron/notarize": "~2.3.0", "@faker-js/faker": "~6.0.0-alpha.5", "@zippytech/sorty": "^2.0.0", "ajv": "^6.5.4", @@ -2886,13 +2886,14 @@ } }, "node_modules/@electron/notarize": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", - "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.0.tgz", + "integrity": "sha512-EiTBU0BwE7HZZjAG1fFWQaiQpCuPrVGn7jPss1kUjD6eTTdXXd29RiZqEqkgN7xqt/Pgn4g3I7Saqovanrfj3w==", "dev": true, "dependencies": { "debug": "^4.1.1", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" }, "engines": { "node": ">= 10.0.0" @@ -8252,9 +8253,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001524", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", - "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "dev": true, "funding": [ { @@ -12518,9 +12519,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/errno": { "version": "0.1.8", @@ -26253,8 +26252,6 @@ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -38183,13 +38180,14 @@ } }, "@electron/notarize": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", - "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.0.tgz", + "integrity": "sha512-EiTBU0BwE7HZZjAG1fFWQaiQpCuPrVGn7jPss1kUjD6eTTdXXd29RiZqEqkgN7xqt/Pgn4g3I7Saqovanrfj3w==", "dev": true, "requires": { "debug": "^4.1.1", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" }, "dependencies": { "fs-extra": { @@ -42465,9 +42463,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001524", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", - "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "dev": true }, "capture-exit": { @@ -45785,9 +45783,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "errno": { "version": "0.1.8", @@ -56334,8 +56330,6 @@ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, - "optional": true, - "peer": true, "requires": { "err-code": "^2.0.2", "retry": "^0.12.0" diff --git a/package.json b/package.json index 0e7613adf..2eca41f47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "productName": "Network Canvas Interviewer", "description": "A tool for conducting Network Canvas Interviews.", "author": "Complex Data Collective", @@ -56,7 +56,7 @@ "@codaco/eslint-plugin-spellcheck": "0.0.14", "@codaco/shared-consts": "~0.0.1-alpha.3", "@codaco/ui": "~5.8.5", - "@electron/notarize": "~1.2.3", + "@electron/notarize": "~2.3.0", "@faker-js/faker": "~6.0.0-alpha.5", "@zippytech/sorty": "^2.0.0", "ajv": "^6.5.4", diff --git a/public/package.json b/public/package.json index bef634fc5..d9bac8f52 100644 --- a/public/package.json +++ b/public/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "productName": "Network Canvas Interviewer", "description": "A tool for conducting Network Canvas Interviews.", "author": "Complex Data Collective", diff --git a/src/utils/__tests__/createSorter.test.js b/src/utils/__tests__/createSorter.test.js index e1c30e02b..9865de34a 100644 --- a/src/utils/__tests__/createSorter.test.js +++ b/src/utils/__tests__/createSorter.test.js @@ -320,6 +320,117 @@ describe('Types', () => { }); }); +describe('Categorical sorting', () => { + it('sorts items based on categorical values', () => { + const mockItems = [ + { + category: ['cow'], + name: 'alice', + }, + { + category: ['duck'], + name: 'bob', + }, + { + category: ['lizard'], + name: 'charlie', + }, + { + category: ['cow'], + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['duck', 'lizard', 'cow'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['alice', 'david', 'charlie', 'bob']); + }); + + it('handles items with multiple categories', () => { + const mockItems = [ + { + category: ['duck', 'lizard'], + name: 'alice', + }, + { + category: ['cow', 'duck'], + name: 'bob', + }, + { + category: ['cow'], + name: 'charlie', + }, + { + category: ['lizard'], + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['cow', 'duck', 'lizard'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['david', 'alice', 'bob', 'charlie']); + }); + + it('handles missing categories', () => { + const mockItems = [ + { + name: 'alice', + }, + { + category: ['duck'], + name: 'bob', + }, + { + category: ['lizard'], + name: 'charlie', + }, + { + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['lizard', 'duck', 'cow'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['bob', 'charlie', 'alice', 'david']); + }); +}); + describe('Order direction', () => { it('orders ascending with "asc"', () => { const mockItems = [ @@ -994,7 +1105,7 @@ describe('processProtocolSortRule', () => { property: 'category', direction: 'asc', }; - expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('string'); + expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('categorical'); }); it('ordinal', () => { diff --git a/src/utils/createSorter.js b/src/utils/createSorter.js index 163e4e4ae..f71ba2583 100644 --- a/src/utils/createSorter.js +++ b/src/utils/createSorter.js @@ -74,6 +74,36 @@ const stringFunction = ({ property, direction }) => (a, b) => { return collator.compare(secondValue, firstValue); }; +const categoricalFunction = ({ property, direction, hierarchy = [] }) => (a, b) => { + // hierarchy is whatever order the variables were specified in the variable definition + const firstValues = get(a, property, []); + const secondValues = get(b, property, []); + + for (let i = 0; i < Math.max(firstValues.length, secondValues.length); i += 1) { + const firstValue = i < firstValues.length ? firstValues[i] : null; + const secondValue = i < secondValues.length ? secondValues[i] : null; + + if (firstValue !== secondValue) { + // If one of the values is not in the hierarchy, it is sorted to the end of the list + const firstIndex = hierarchy.indexOf(firstValue); + const secondIndex = hierarchy.indexOf(secondValue); + + if (firstIndex === -1) { + return 1; + } + if (secondIndex === -1) { + return -1; + } + + if (direction === 'asc') { + return firstIndex - secondIndex; + } return secondIndex - firstIndex; // desc + } + } + + return 0; +}; + /** * Creates a sort function that sorts items according to the index of their * property value in a hierarchy array. @@ -97,7 +127,6 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (firstIndex > secondIndex) { return -1; } - if (firstIndex < secondIndex) { return 1; } @@ -105,7 +134,6 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (firstIndex < secondIndex) { return -1; } - if (firstIndex > secondIndex) { return 1; } @@ -147,7 +175,7 @@ const getSortFunction = (rule) => { const { property, direction = 'asc', - type, // REQUIRED! number, boolean, string, date, hierarchy + type, // REQUIRED! number, boolean, string, date, hierarchy, categorical } = rule; // LIFO/FIFO rule sorted by _createdIndex @@ -165,8 +193,10 @@ const getSortFunction = (rule) => { if (type === 'hierarchy') { return hierarchyFunction(rule); } + if (type === 'categorical') { return categoricalFunction(rule); } + // eslint-disable-next-line no-console - console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy.'); + console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy, categorical'); return stringFunction(rule); }; @@ -195,6 +225,7 @@ const createSorter = (sortRules = []) => { * - hierarchy * - number * - date + * - categorical * * Network Canvas Variables can be of type: * - "boolean", @@ -210,7 +241,6 @@ const createSorter = (sortRules = []) => { export const mapNCType = (type) => { switch (type) { case 'text': - case 'categorical': case 'layout': return 'string'; case 'number': @@ -221,6 +251,8 @@ export const mapNCType = (type) => { return 'date'; case 'ordinal': return 'hierarchy'; + case 'categorical': + return 'categorical'; case 'scalar': return 'number'; default: @@ -271,6 +303,7 @@ export const processProtocolSortRule = (codebookVariables) => (sortRule) => { type: mapNCType(type), // Generate a hierarchy if the variable is ordinal based on the ordinal options ...type === 'ordinal' && { hierarchy: variableDefinition.options.map((option) => option.value) }, + ...type === 'categorical' && { hierarchy: variableDefinition.options.map((option) => option.value) }, }; };