command-dropdown.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. key={item.id}
  98. title={item.label}
  99. // eslint-disable-next-line react/jsx-handler-names
  100. onClick={onClickHandler}
  101. href={item.href}
  102. disabled={item.disabled}
  103. />
  104. )
  105. } else {
  106. return (
  107. <NestedMenuBarDropdown title={item.title} id={item.id} key={item.id}>
  108. {item.children.map(subChild => {
  109. return <CommandDropdownChild item={subChild} key={subChild.id} />
  110. })}
  111. </NestedMenuBarDropdown>
  112. )
  113. }
  114. }
  115. export default CommandDropdown
  116. function populateSectionOrGroup<
  117. T extends { children: Array<Entry<CommandId>> },
  118. >(
  119. section: T,
  120. registry: Map<string, Command>
  121. ): Omit<T, 'children'> & {
  122. children: Array<Entry<TaggedCommand>>
  123. } {
  124. const { children, ...rest } = section
  125. return {
  126. ...rest,
  127. children: children
  128. .map(child => {
  129. if (typeof child !== 'string') {
  130. const populatedChild = populateSectionOrGroup(child, registry)
  131. if (populatedChild.children.length === 0) {
  132. // Skip empty groups
  133. return undefined
  134. }
  135. return populatedChild
  136. }
  137. const command = registry.get(child)
  138. if (command) {
  139. return { ...command, type: 'command' as const }
  140. }
  141. return undefined
  142. })
  143. .filter(x => x !== undefined),
  144. }
  145. }
  146. function isTaggedCommand(item: Entry<TaggedCommand>): item is TaggedCommand {
  147. return 'type' in item && item.type === 'command'
  148. }