command-dropdown.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import {
  2. Command,
  3. useCommandRegistry,
  4. } from '@/features/ide-react/context/command-registry-context'
  5. import {
  6. DropdownDivider,
  7. DropdownHeader,
  8. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  9. import {
  10. MenuBarDropdown,
  11. NestedMenuBarDropdown,
  12. } from '@/shared/components/menu-bar/menu-bar-dropdown'
  13. import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
  14. import { Fragment, useCallback, useMemo } from 'react'
  15. type CommandId = string
  16. type TaggedCommand = Command & { type: 'command' }
  17. type Entry<T> = T | GroupStructure<T>
  18. type GroupStructure<T> = {
  19. id: string
  20. title: string
  21. children: Array<Entry<T>>
  22. }
  23. export type MenuSectionStructure<T = CommandId> = {
  24. title?: string
  25. id: string
  26. children: Array<Entry<T>>
  27. }
  28. export type MenuStructure<T = CommandId> = Array<MenuSectionStructure<T>>
  29. const CommandDropdown = ({
  30. menu,
  31. title,
  32. id,
  33. }: {
  34. menu: MenuStructure<CommandId>
  35. title: string
  36. id: string
  37. }) => {
  38. const { registry } = useCommandRegistry()
  39. const populatedSections = useMemo(
  40. () =>
  41. menu
  42. .map(section => populateSectionOrGroup(section, registry))
  43. .filter(x => x.children.length > 0),
  44. [menu, registry]
  45. )
  46. if (populatedSections.length === 0) {
  47. return null
  48. }
  49. return (
  50. <MenuBarDropdown
  51. title={title}
  52. id={id}
  53. className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
  54. >
  55. {populatedSections.map((section, index) => {
  56. return (
  57. <Fragment key={section.id}>
  58. {index > 0 && <DropdownDivider />}
  59. {section.title && <DropdownHeader>{section.title}</DropdownHeader>}
  60. {section.children.map(child => (
  61. <CommandDropdownChild item={child} key={child.id} />
  62. ))}
  63. </Fragment>
  64. )
  65. })}
  66. </MenuBarDropdown>
  67. )
  68. }
  69. export const CommandSection = ({
  70. section: sectionStructure,
  71. }: {
  72. section: MenuSectionStructure<CommandId>
  73. }) => {
  74. const { registry } = useCommandRegistry()
  75. const section = populateSectionOrGroup(sectionStructure, registry)
  76. if (section.children.length === 0) {
  77. return null
  78. }
  79. return (
  80. <>
  81. {section.title && <DropdownHeader>{section.title}</DropdownHeader>}
  82. {section.children.map(child => (
  83. <CommandDropdownChild item={child} key={child.id} />
  84. ))}
  85. </>
  86. )
  87. }
  88. const CommandDropdownChild = ({ item }: { item: Entry<TaggedCommand> }) => {
  89. const onClickHandler = useCallback(() => {
  90. if (isTaggedCommand(item)) {
  91. item.handler?.({ location: 'menu-bar' })
  92. }
  93. }, [item])
  94. if (isTaggedCommand(item)) {
  95. return (
  96. <MenuBarOption
  97. eventKey={item.id}
  98. key={item.id}
  99. title={item.label}
  100. // eslint-disable-next-line react/jsx-handler-names
  101. onClick={onClickHandler}
  102. href={item.href}
  103. disabled={item.disabled}
  104. />
  105. )
  106. } else {
  107. return (
  108. <NestedMenuBarDropdown title={item.title} id={item.id} key={item.id}>
  109. {item.children.map(subChild => {
  110. return <CommandDropdownChild item={subChild} key={subChild.id} />
  111. })}
  112. </NestedMenuBarDropdown>
  113. )
  114. }
  115. }
  116. export default CommandDropdown
  117. function populateSectionOrGroup<
  118. T extends { children: Array<Entry<CommandId>> },
  119. >(
  120. section: T,
  121. registry: Map<string, Command>
  122. ): Omit<T, 'children'> & {
  123. children: Array<Entry<TaggedCommand>>
  124. } {
  125. const { children, ...rest } = section
  126. return {
  127. ...rest,
  128. children: children
  129. .map(child => {
  130. if (typeof child !== 'string') {
  131. const populatedChild = populateSectionOrGroup(child, registry)
  132. if (populatedChild.children.length === 0) {
  133. // Skip empty groups
  134. return undefined
  135. }
  136. return populatedChild
  137. }
  138. const command = registry.get(child)
  139. if (command) {
  140. return { ...command, type: 'command' as const }
  141. }
  142. return undefined
  143. })
  144. .filter(x => x !== undefined),
  145. }
  146. }
  147. function isTaggedCommand(item: Entry<TaggedCommand>): item is TaggedCommand {
  148. return 'type' in item && item.type === 'command'
  149. }