Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@dhis2/app-runtime": "^3.14.0",
"@dhis2/pwa": "^12.3.0",
"@dhis2/ui": "^10.1.13",
"fuse.js": "^7.1.0",
"react-router": "^7.2.0",
"styled-jsx": "^4.0.1"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ const CommandPalette = ({ apps, commands, shortcuts }) => {
case 'Enter':
event.preventDefault()
currentItem?.['action']?.()
if (currentItem?.type === APP || currentItem?.type === SHORTCUT) {
if (
currentItem?.type === APP ||
currentItem?.type === SHORTCUT
) {
resetModal()
}
break
Expand Down
99 changes: 46 additions & 53 deletions src/components/header-bar/command-palette/utils/filter.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,26 @@
import Fuse from 'fuse.js'
import {
ALL_APPS_VIEW,
ALL_COMMANDS_VIEW,
ALL_SHORTCUTS_VIEW,
FILTERABLE_ACTION,
} from './constants.js'

/**
* Copied from here:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
*/
function escapeRegExpCharacters(text) {
return text.replace(/[/.*+?^${}()|[\]\\]/g, '\\$&')
}

function removePunctuationMarks(text) {
return text.replace(/[.,!;:`"'?\-_\s]/g, '')
}

function removeAccentMarks(str) {
/**
* normalisation reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
*/
return str.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
}

export function processString(text) {
const str = removePunctuationMarks(text)
return removeAccentMarks(str)
}

export const filterItemsArray = (items, filter) => {
if (!filter.length) {
return items
if (!items?.length) {
return []
}
return items.filter(({ displayName, name }) => {
// Include both the translated name and the base name in the searchable string, so searching for either will return the result
// (the translated string is still the one that will be displayed)
const itemName = `${displayName ?? ''}${name ?? ''}`
const formattedItemName = itemName.toLowerCase()
const formattedFilter = filter.toLowerCase()

const escapedFilter = escapeRegExpCharacters(formattedFilter)
if (formattedItemName.match(escapedFilter)) {
return true
}

const normalisedItemName = processString(formattedItemName)
const normalisedFilter = processString(formattedFilter)
if (normalisedFilter.length) {
return normalisedItemName.includes(normalisedFilter)
}
return false
const fuse = new Fuse(items, {
includeScore: true,
threshold: 0.4,
// ignoreLocation: true,
ignoreDiacritics: true,
shouldSort: true,
keys: ['displayName', 'name'],
})

return filter ? fuse.search(filter).map((result) => result.item) : items
}

export const filterItemsPerView = ({
Expand All @@ -62,6 +31,16 @@ export const filterItemsPerView = ({
filter,
currentView,
}) => {
if (currentView === ALL_APPS_VIEW) {
return filterItemsArray(apps, filter)
}
if (currentView === ALL_COMMANDS_VIEW) {
return filterItemsArray(commands, filter)
}
if (currentView === ALL_SHORTCUTS_VIEW) {
return filterItemsArray(shortcuts, filter)
}

const searchableActions = actions.filter(
(action) => action.type === FILTERABLE_ACTION
)
Expand All @@ -71,20 +50,34 @@ export const filterItemsPerView = ({
const filteredShortcuts = filterItemsArray(shortcuts, filter)
const filteredActions = filterItemsArray(searchableActions, filter)

if (currentView === ALL_APPS_VIEW) {
return filteredApps
}
if (currentView === ALL_COMMANDS_VIEW) {
return filteredCommands
}
if (currentView === ALL_SHORTCUTS_VIEW) {
return filteredShortcuts
// Group app with its shortcuts
// Filter for matched apps and return them with their shortcuts
// Append remaining shortcuts that match
const shortcutsByApp = new Map()
for (const shortcut of shortcuts) {
if (!shortcutsByApp.has(shortcut.appName)) {
shortcutsByApp.set(shortcut.appName, [])
}
shortcutsByApp.get(shortcut.appName).push(shortcut)
}

const filteredAppsWithShortcuts = filteredApps.flatMap((app) => [
app,
...(shortcutsByApp.get(app.displayName) ?? []),
])

const matchedAppNames = new Set(
filteredApps.map((app) => app.displayName || app.name)
)

const remainingShortcuts = filteredShortcuts.filter(
(shortcut) => !matchedAppNames.has(shortcut.appName)
)

return [
...filteredApps,
...filteredAppsWithShortcuts,
...filteredCommands,
...filteredShortcuts,
...remainingShortcuts,
...filteredActions,
]
}
14 changes: 14 additions & 0 deletions src/components/header-bar/command-palette/utils/filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,18 @@ describe('filter helper functions', () => {
itemsToSearch
)
})

test('filterItemsArray should consider appName if it is available', () => {
const itemsToSearch = [
{ name: 'category options', appName: 'Maintenance' },
{ name: 'Indicators', appName: 'Maintenance' },
{ name: 'notifications', appName: 'System Settings' },
]
expect(filterItemsArray(itemsToSearch, 'maintenance')).toEqual(
itemsToSearch.slice(0, 2)
)
expect(filterItemsArray(itemsToSearch, 'system')).toEqual(
itemsToSearch.slice(-1)
)
})
})
10 changes: 9 additions & 1 deletion src/components/header-bar/header-bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const HeaderBar = ({
return []
}

return data.apps.modules?.reduce((acc, currModule) => {
const results = data.apps.modules?.reduce((acc, currModule) => {
const { defaultAction, icon, displayName: appName } = currModule
const shortcuts =
currModule.shortcuts?.map(({ name, displayName, url }) => {
Expand All @@ -113,6 +113,14 @@ export const HeaderBar = ({

return [...acc, ...shortcuts]
}, [])

// Sorting shortcuts by appName for order
// Method: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
const sortedShortcuts = results.sort((shortcut1, shortcut2) =>
shortcut1.appName.localeCompare(shortcut2.appName)
)

return sortedShortcuts
}, [data, navigate])

// See https://jira.dhis2.org/browse/LIBS-180
Expand Down
Loading
Loading