diff --git a/components/header-bar/src/command-palette/apps.js b/components/header-bar/src/command-palette/apps.js
deleted file mode 100755
index 49ab2ff3d..000000000
--- a/components/header-bar/src/command-palette/apps.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import { colors, elevations, spacers } from '@dhis2/ui-constants'
-import { IconApps24 } from '@dhis2/ui-icons'
-import { Layer } from '@dhis2-ui/layer'
-import PropTypes from 'prop-types'
-import React, { useState, useCallback, useRef, useEffect } from 'react'
-import { Actions, BackButton, Search } from './fields.js'
-import { ViewSwitcher } from './views.js'
-
-const MIN_APPS_NUM = 8
-
-export const Container = ({ children, setShow, show }) => {
- return (
- setShow(false)} translucent={show}>
-
- {children}
-
-
-
- )
-}
-
-Container.propTypes = {
- children: PropTypes.node,
- setShow: PropTypes.func,
- show: PropTypes.bool,
-}
-
-const CommandPalette = ({ apps, commands }) => {
- const [show, setShow] = useState(false)
- const [filter, setFilter] = useState('')
-
- const [currentView, setCurrentView] = useState('home')
-
- const showActions = filter.length <= 0 && currentView === 'home'
- const showBackButton = currentView !== 'home'
-
- const handleVisibilityToggle = useCallback(() => setShow(!show), [show])
- const handleFilterChange = useCallback(({ value }) => setFilter(value), [])
-
- const handleClearSearch = () => setFilter('')
-
- const containerEl = useRef(null)
-
- const handleKeyDown = useCallback(
- (event) => {
- switch (event.key) {
- case 'Escape':
- event.preventDefault()
- if (currentView === 'home') {
- setShow(false)
- } else {
- setCurrentView('home')
- }
- break
- }
-
- if ((event.metaKey || event.ctrlKey) && event.key === '/') {
- setShow(!show)
- }
- },
- [currentView, show]
- )
-
- const handleFocus = () => {
- // this is about the focus of the element
- // on launch: focus entire element
- }
-
- useEffect(() => {
- document.addEventListener('keydown', handleKeyDown)
- document.addEventListener('focus', handleFocus)
- return () => {
- document.removeEventListener('keydown', handleKeyDown)
- document.removeEventListener('focus', handleFocus)
- }
- }, [handleKeyDown])
-
- return (
-
-
-
- {show ? (
-
-
-
-
-
- {showBackButton ? (
-
- ) : null}
-
- {showActions ? (
-
MIN_APPS_NUM}
- showCommands={commands?.length > 0}
- />
- ) : null}
-
-
-
- ) : null}
-
-
-
- )
-}
-
-CommandPalette.propTypes = {
- apps: PropTypes.array,
- commands: PropTypes.array,
-}
-
-export default CommandPalette
diff --git a/components/header-bar/src/command-palette/command-palette.js b/components/header-bar/src/command-palette/command-palette.js
new file mode 100755
index 000000000..6e5569148
--- /dev/null
+++ b/components/header-bar/src/command-palette/command-palette.js
@@ -0,0 +1,214 @@
+import { clearSensitiveCaches, useConfig } from '@dhis2/app-runtime'
+import { colors, spacers } from '@dhis2/ui-constants'
+import {
+ IconApps16,
+ IconApps24,
+ IconLogOut16,
+ IconTerminalWindow16,
+} from '@dhis2/ui-icons'
+import PropTypes from 'prop-types'
+import React, { useState, useCallback, useRef, useEffect } from 'react'
+import { joinPath } from '../join-path.js'
+import i18n from '../locales/index.js'
+import BackButton from './sections/back-button.js'
+import Container from './sections/container.js'
+import Heading from './sections/heading.js'
+import ListItem from './sections/list-item.js'
+import Search from './sections/search-field.js'
+import HomeView from './views/home-view.js'
+import ListView from './views/list-view.js'
+
+const MIN_APPS_NUM = 8
+
+const CommandPalette = ({ apps, commands }) => {
+ const { baseUrl } = useConfig()
+ const [show, setShow] = useState(false)
+ const [filter, setFilter] = useState('')
+
+ const [currentView, setCurrentView] = useState('home')
+
+ const showActions = filter.length <= 0 && currentView === 'home'
+ const showBackButton = currentView !== 'home'
+
+ const handleVisibilityToggle = useCallback(() => setShow(!show), [show])
+ const handleFilterChange = useCallback(({ value }) => setFilter(value), [])
+
+ const handleClearSearch = () => setFilter('')
+
+ const containerEl = useRef(null)
+
+ const handleKeyDown = useCallback(
+ (event) => {
+ switch (event.key) {
+ case 'Escape':
+ event.preventDefault()
+ if (currentView === 'home') {
+ setShow(false)
+ } else {
+ setCurrentView('home')
+ }
+ break
+ }
+
+ if ((event.metaKey || event.ctrlKey) && event.key === '/') {
+ setShow(!show)
+ }
+ },
+ [currentView, show]
+ )
+
+ const handleFocus = () => {
+ // this is about the focus of the element
+ // on launch: focus entire element
+ }
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown)
+ document.addEventListener('focus', handleFocus)
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ document.removeEventListener('focus', handleFocus)
+ }
+ }, [handleKeyDown])
+
+ return (
+
+
+
+ {show ? (
+
+
+
+
+ {showBackButton ? (
+
+ ) : null}
+ {/* switch views */}
+ {currentView === 'apps' && (
+
+ )}
+ {currentView === 'commands' && (
+
+ )}
+ {currentView === 'home' && (
+
+ )}
+ {/* actions sections */}
+ {showActions ? (
+ <>
+
+ {apps?.length > MIN_APPS_NUM ? (
+
+ }
+ onClickHandler={() =>
+ setCurrentView('apps')
+ }
+ />
+ ) : null}
+ {commands?.length > 0 ? (
+
+ }
+ onClickHandler={() =>
+ setCurrentView('commands')
+ }
+ />
+ ) : null}
+
+ }
+ onClickHandler={async () => {
+ await clearSensitiveCaches()
+ window.location.assign(
+ joinPath(
+ baseUrl,
+ 'dhis-web-commons-security/logout.action'
+ )
+ )
+ }}
+ href={joinPath(
+ baseUrl,
+ 'dhis-web-commons-security/logout.action'
+ )}
+ />
+ >
+ ) : null}
+
+
+
+ ) : null}
+
+
+
+ )
+}
+
+CommandPalette.propTypes = {
+ apps: PropTypes.array,
+ commands: PropTypes.array,
+}
+
+export default CommandPalette
diff --git a/components/header-bar/src/command-palette/fields.js b/components/header-bar/src/command-palette/fields.js
deleted file mode 100644
index 82fe8ca93..000000000
--- a/components/header-bar/src/command-palette/fields.js
+++ /dev/null
@@ -1,387 +0,0 @@
-import { clearSensitiveCaches, useConfig } from '@dhis2/app-runtime'
-import { colors, spacers, theme } from '@dhis2/ui-constants'
-import {
- IconApps16,
- IconTerminalWindow16,
- IconLogOut16,
- IconArrowLeft16,
- IconSearch16,
-} from '@dhis2/ui-icons'
-import PropTypes from 'prop-types'
-import React, { useState, useRef, useEffect } from 'react'
-import { InputField } from '../../../input/src/input-field/input-field.js'
-import { joinPath } from '../join-path.js'
-import i18n from '../locales/index.js'
-
-export function Search({ value, onChange }) {
- return (
- <>
- }
- clearable
- />
-
- >
- )
-}
-
-Search.propTypes = {
- value: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
-}
-
-export function AppItem({ name, path, img }) {
- return (
-
-
- {name}
-
-
- )
-}
-
-AppItem.propTypes = {
- img: PropTypes.string,
- name: PropTypes.string,
- path: PropTypes.string,
-}
-
-export function ListItem({
- title,
- path,
- icon,
- image,
- description,
- type,
- onClickHandler,
-}) {
- const showDescription = type === 'commands'
- return (
-
- {icon && {icon}}
- {image && }
- {title}
- {showDescription && (
- {description}
- )}
-
-
- )
-}
-
-ListItem.propTypes = {
- description: PropTypes.string,
- icon: PropTypes.node,
- image: PropTypes.string,
- path: PropTypes.string,
- title: PropTypes.string,
- type: PropTypes.string,
- onClickHandler: PropTypes.func,
-}
-
-export function List({ filteredItems, type }) {
- const divRef = useRef(null)
- const [activeItem, setActiveItem] = useState(-1)
- const lastIndex = filteredItems.length - 1
-
- const handleKeyDown = (event) => {
- switch (event.key) {
- case 'ArrowDown':
- setActiveItem(activeItem >= lastIndex ? 0 : activeItem + 1)
- break
- case 'ArrowUp':
- setActiveItem(activeItem > 0 ? activeItem - 1 : lastIndex)
- break
- case 'Enter':
- event.preventDefault()
- event.target?.click()
- break
- }
- }
-
- useEffect(() => {
- if (divRef) {
- if (filteredItems.length && activeItem > -1) {
- divRef.current.children[activeItem].focus()
- }
- }
- }, [activeItem, filteredItems])
-
- // useEffect(() => {
- // if (!divRef && !divRef.current) {return}
- // const div = divRef.current
- // div.addEventListener('keydown', handleKeyDown)
- // return () => {
- // div.removeEventListener('keydown', handleKeyDown)
- // }
- // }, [])
- return (
-
- {filteredItems.map(
- (
- { displayName, name, defaultAction, icon, description },
- idx
- ) => (
-
- )
- )}
-
- {/* // todo: use list with type/view filter to render correct item component */}
-
-
-
- )
-}
-List.propTypes = {
- filteredItems: PropTypes.array,
- type: PropTypes.string,
-}
-
-export function Actions({ setView, showApps, showCommands }) {
- const { baseUrl } = useConfig()
- console.log(showApps, showCommands)
-
- return (
- <>
-
-
- {showApps ? (
- }
- onClickHandler={() => setView('apps')}
- />
- ) : null}
- {showCommands ? (
- }
- onClickHandler={() => setView('commands')}
- />
- ) : null}
- }
- onClickHandler={async () => {
- await clearSensitiveCaches()
- window.location.assign(
- joinPath(
- baseUrl,
- 'dhis-web-commons-security/logout.action'
- )
- )
- }}
- href={joinPath(
- baseUrl,
- 'dhis-web-commons-security/logout.action'
- )}
- />
- >
- )
-}
-
-Actions.propTypes = {
- setView: PropTypes.func,
- showApps: PropTypes.bool,
- showCommands: PropTypes.bool,
-}
-
-export function Heading({ filter, filteredItems, heading }) {
- return (
-
-
- {filter
- ? filteredItems.length > 0
- ? i18n.t(`Results for ${filter}`)
- : i18n.t(`Nothing found for ${filter}`)
- : i18n.t(`${heading}`)}
-
-
-
- )
-}
-
-Heading.propTypes = {
- filter: PropTypes.string,
- filteredItems: PropTypes.array,
- heading: PropTypes.string,
-}
-
-export function BackButton({ setView, handleClearSearch }) {
- const handleClick = () => {
- setView('home')
- handleClearSearch()
- }
- return (
- <>
-
-
- >
- )
-}
-
-BackButton.propTypes = {
- handleClearSearch: PropTypes.func,
- setView: PropTypes.func,
-}
diff --git a/components/header-bar/src/command-palette/sections/app-item.js b/components/header-bar/src/command-palette/sections/app-item.js
new file mode 100644
index 000000000..6bb49ed11
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/app-item.js
@@ -0,0 +1,53 @@
+import { colors, spacers } from '@dhis2/ui-constants'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function AppItem({ name, path, img }) {
+ return (
+
+
+ {name}
+
+
+ )
+}
+
+AppItem.propTypes = {
+ img: PropTypes.string,
+ name: PropTypes.string,
+ path: PropTypes.string,
+}
+
+export default AppItem
diff --git a/components/header-bar/src/command-palette/sections/back-button.js b/components/header-bar/src/command-palette/sections/back-button.js
new file mode 100644
index 000000000..80cca36e4
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/back-button.js
@@ -0,0 +1,56 @@
+import { colors, spacers } from '@dhis2/ui-constants'
+import { IconArrowLeft16 } from '@dhis2/ui-icons'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function BackButton({ setView, handleClearSearch }) {
+ const handleClick = () => {
+ setView('home')
+ handleClearSearch()
+ }
+ return (
+ <>
+
+
+ >
+ )
+}
+
+BackButton.propTypes = {
+ handleClearSearch: PropTypes.func,
+ setView: PropTypes.func,
+}
+
+export default BackButton
diff --git a/components/header-bar/src/command-palette/sections/container.js b/components/header-bar/src/command-palette/sections/container.js
new file mode 100644
index 000000000..ae8a5df53
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/container.js
@@ -0,0 +1,37 @@
+import { colors, elevations } from '@dhis2/ui-constants'
+import { Layer } from '@dhis2-ui/layer'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+const Container = ({ children, setShow, show }) => {
+ return (
+ setShow(false)} translucent={show}>
+
+ {children}
+
+
+
+ )
+}
+
+Container.propTypes = {
+ children: PropTypes.node,
+ setShow: PropTypes.func,
+ show: PropTypes.bool,
+}
+
+export default Container
diff --git a/components/header-bar/src/command-palette/sections/heading.js b/components/header-bar/src/command-palette/sections/heading.js
new file mode 100644
index 000000000..65288e8b8
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/heading.js
@@ -0,0 +1,38 @@
+import { colors, spacers } from '@dhis2/ui-constants'
+import PropTypes from 'prop-types'
+import React from 'react'
+import i18n from '../../locales/index.js'
+
+function Heading({ filter, filteredItems, heading }) {
+ return (
+
+
+ {filter
+ ? filteredItems.length > 0
+ ? i18n.t(`Results for "${filter}"`)
+ : i18n.t(`Nothing found for "${filter}"`)
+ : i18n.t(`${heading}`)}
+
+
+
+ )
+}
+
+Heading.propTypes = {
+ filter: PropTypes.string,
+ filteredItems: PropTypes.array,
+ heading: PropTypes.string,
+}
+
+export default Heading
diff --git a/components/header-bar/src/command-palette/sections/list-item.js b/components/header-bar/src/command-palette/sections/list-item.js
new file mode 100644
index 000000000..d3b7eda61
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/list-item.js
@@ -0,0 +1,106 @@
+import { colors, spacers } from '@dhis2/ui-constants'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function ListItem({
+ title,
+ path,
+ icon,
+ image,
+ description,
+ type,
+ onClickHandler,
+}) {
+ const showDescription = type === 'commands'
+ return (
+
+
+ {icon &&
{icon}}
+ {image && (
+
+ )}
+
+
+ {title}
+ {showDescription && (
+ {description}
+ )}
+
+
+
+ )
+}
+
+ListItem.propTypes = {
+ description: PropTypes.string,
+ icon: PropTypes.node,
+ image: PropTypes.string,
+ path: PropTypes.string,
+ title: PropTypes.string,
+ type: PropTypes.string,
+ onClickHandler: PropTypes.func,
+}
+
+export default ListItem
diff --git a/components/header-bar/src/command-palette/sections/list.js b/components/header-bar/src/command-palette/sections/list.js
new file mode 100644
index 000000000..2c91d37db
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/list.js
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types'
+import React, { useState, useRef, useEffect } from 'react'
+import ListItem from './list-item.js'
+
+function List({ filteredItems, type }) {
+ const divRef = useRef(null)
+ const [activeItem, setActiveItem] = useState(-1)
+ const lastIndex = filteredItems.length - 1
+
+ const handleKeyDown = (event) => {
+ switch (event.key) {
+ case 'ArrowDown':
+ setActiveItem(activeItem >= lastIndex ? 0 : activeItem + 1)
+ break
+ case 'ArrowUp':
+ setActiveItem(activeItem > 0 ? activeItem - 1 : lastIndex)
+ break
+ case 'Enter':
+ event.preventDefault()
+ event.target?.click()
+ break
+ }
+ }
+
+ useEffect(() => {
+ if (divRef) {
+ if (filteredItems.length && activeItem > -1) {
+ divRef.current.children[activeItem].focus()
+ }
+ }
+ }, [activeItem, filteredItems])
+
+ return (
+
+ {filteredItems.map(
+ (
+ { displayName, name, defaultAction, icon, description },
+ idx
+ ) => (
+
+ )
+ )}
+
+
+ )
+}
+List.propTypes = {
+ filteredItems: PropTypes.array,
+ type: PropTypes.string,
+}
+
+export default List
diff --git a/components/header-bar/src/command-palette/sections/search-field.js b/components/header-bar/src/command-palette/sections/search-field.js
new file mode 100644
index 000000000..722057588
--- /dev/null
+++ b/components/header-bar/src/command-palette/sections/search-field.js
@@ -0,0 +1,49 @@
+import { colors, theme } from '@dhis2/ui-constants'
+import { IconSearch16 } from '@dhis2/ui-icons'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { InputField } from '../../../../input/src/input-field/input-field.js'
+import i18n from '../../locales/index.js'
+
+function Search({ value, onChange }) {
+ return (
+ <>
+ }
+ clearable
+ />
+
+ >
+ )
+}
+
+Search.propTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+export default Search
diff --git a/components/header-bar/src/command-palette/utils/escapeCharacters.js b/components/header-bar/src/command-palette/utils/escapeCharacters.js
new file mode 100644
index 000000000..26ed7e12f
--- /dev/null
+++ b/components/header-bar/src/command-palette/utils/escapeCharacters.js
@@ -0,0 +1,7 @@
+/**
+ * Copied from here:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
+ */
+export function escapeRegExpCharacters(text) {
+ return text.replace(/[/.*+?^${}()|[\]\\]/g, '\\$&')
+}
diff --git a/components/header-bar/src/command-palette/views.js b/components/header-bar/src/command-palette/views/home-view.js
similarity index 65%
rename from components/header-bar/src/command-palette/views.js
rename to components/header-bar/src/command-palette/views/home-view.js
index 427639936..a713d3f91 100644
--- a/components/header-bar/src/command-palette/views.js
+++ b/components/header-bar/src/command-palette/views/home-view.js
@@ -1,46 +1,11 @@
import PropTypes from 'prop-types'
import React, { useEffect, useRef, useState } from 'react'
-import { AppItem, Heading, List } from './fields.js'
+import AppItem from '../sections/app-item.js'
+import Heading from '../sections/heading.js'
+import { escapeRegExpCharacters } from '../utils/escapeCharacters.js'
+import ListView from './list-view.js'
-/**
- * Copied from here:
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
- */
-function escapeRegExpCharacters(text) {
- return text.replace(/[/.*+?^${}()|[\]\\]/g, '\\$&')
-}
-
-export function ListView({ heading, itemsArray, filter, type }) {
- const filteredItems = itemsArray.filter(({ displayName, name }) => {
- const itemName = displayName || name
- const formattedItemName = itemName.toLowerCase()
- const formattedFilter = escapeRegExpCharacters(filter).toLowerCase()
-
- return filter.length > 0
- ? formattedItemName.match(formattedFilter)
- : true
- })
-
- return (
-
-
-
-
- )
-}
-
-ListView.propTypes = {
- filter: PropTypes.string,
- heading: PropTypes.string,
- itemsArray: PropTypes.array,
- type: PropTypes.string,
-}
-
-export function HomeView({ apps, filter }) {
+function HomeView({ apps, filter }) {
const divRef = useRef(null)
const [activeItem, setActiveItem] = useState(-1)
@@ -96,7 +61,7 @@ export function HomeView({ apps, filter }) {
}
}
}, [activeItem, apps.length])
- // filter happens across everything here
+ // filter happens across everything here - apps, commands, shorcuts
const filteredApps = apps.filter(({ displayName, name }) => {
const appName = displayName || name
const formattedAppName = appName.toLowerCase()
@@ -109,13 +74,11 @@ export function HomeView({ apps, filter }) {
return (
+ {/* Search results */}
{filter.length > 0 && (
-
+
)}
+ {/* normal view */}
{filter.length < 1 && (
<>
- )
- case 'commands':
- return (
-
- )
- case 'home':
- default:
- return
- }
-}
-
-ViewSwitcher.propTypes = {
- apps: PropTypes.array,
- commands: PropTypes.array,
- filter: PropTypes.string,
- view: PropTypes.string,
-}
+export default HomeView
diff --git a/components/header-bar/src/command-palette/views/list-view.js b/components/header-bar/src/command-palette/views/list-view.js
new file mode 100644
index 000000000..9371761a6
--- /dev/null
+++ b/components/header-bar/src/command-palette/views/list-view.js
@@ -0,0 +1,37 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Heading from '../sections/heading.js'
+import List from '../sections/list.js'
+import { escapeRegExpCharacters } from '../utils/escapeCharacters.js'
+
+function ListView({ heading, itemsArray, filter, type }) {
+ const filteredItems = itemsArray.filter(({ displayName, name }) => {
+ const itemName = displayName || name
+ const formattedItemName = itemName.toLowerCase()
+ const formattedFilter = escapeRegExpCharacters(filter).toLowerCase()
+
+ return filter.length > 0
+ ? formattedItemName.match(formattedFilter)
+ : true
+ })
+
+ return (
+
+
+
+
+ )
+}
+
+ListView.propTypes = {
+ filter: PropTypes.string,
+ heading: PropTypes.string,
+ itemsArray: PropTypes.array,
+ type: PropTypes.string,
+}
+
+export default ListView
diff --git a/components/header-bar/src/header-bar.js b/components/header-bar/src/header-bar.js
index b0b9ff9eb..d63cc02a3 100755
--- a/components/header-bar/src/header-bar.js
+++ b/components/header-bar/src/header-bar.js
@@ -2,7 +2,7 @@ import { useDataQuery, useConfig } from '@dhis2/app-runtime'
import { colors } from '@dhis2/ui-constants'
import PropTypes from 'prop-types'
import React, { useMemo } from 'react'
-import CommandPalette from './command-palette/apps.js'
+import CommandPalette from './command-palette/command-palette.js'
import { HeaderBarContextProvider } from './header-bar-context.js'
import { joinPath } from './join-path.js'
import i18n from './locales/index.js'