command-dropdown.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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 } 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 = menu
  40. .map(section => populateSectionOrGroup(section, registry))
  41. .filter(x => x.children.length > 0)
  42. return (
  43. <MenuBarDropdown
  44. title={title}
  45. id={id}
  46. className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
  47. >
  48. {populatedSections.map((section, index) => {
  49. return (
  50. <Fragment key={section.id}>
  51. {index > 0 && <DropdownDivider />}
  52. {section.title && <DropdownHeader>{section.title}</DropdownHeader>}
  53. {section.children.map(child => (
  54. <CommandDropdownChild item={child} key={child.id} />
  55. ))}
  56. </Fragment>
  57. )
  58. })}
  59. </MenuBarDropdown>
  60. )
  61. }
  62. export const CommandSection = ({
  63. section: sectionStructure,
  64. }: {
  65. section: MenuSectionStructure<CommandId>
  66. }) => {
  67. const { registry } = useCommandRegistry()
  68. const section = populateSectionOrGroup(sectionStructure, registry)
  69. if (section.children.length === 0) {
  70. return null
  71. }
  72. return (
  73. <>
  74. {section.title && <DropdownHeader>{section.title}</DropdownHeader>}
  75. {section.children.map(child => (
  76. <CommandDropdownChild item={child} key={child.id} />
  77. ))}
  78. </>
  79. )
  80. }
  81. const CommandDropdownChild = ({ item }: { item: Entry<TaggedCommand> }) => {
  82. const onClickHandler = useCallback(() => {
  83. if (isTaggedCommand(item)) {
  84. item.handler?.({ location: 'menu-bar' })
  85. }
  86. }, [item])
  87. if (isTaggedCommand(item)) {
  88. return (
  89. <MenuBarOption
  90. key={item.id}
  91. title={item.label}
  92. // eslint-disable-next-line react/jsx-handler-names
  93. onClick={onClickHandler}
  94. href={item.href}
  95. disabled={item.disabled}
  96. />
  97. )
  98. } else {
  99. return (
  100. <NestedMenuBarDropdown title={item.title} id={item.id} key={item.id}>
  101. {item.children.map(subChild => {
  102. return <CommandDropdownChild item={subChild} key={subChild.id} />
  103. })}
  104. </NestedMenuBarDropdown>
  105. )
  106. }
  107. }
  108. export default CommandDropdown
  109. function populateSectionOrGroup<
  110. T extends { children: Array<Entry<CommandId>> },
  111. >(
  112. section: T,
  113. registry: Map<string, Command>
  114. ): Omit<T, 'children'> & {
  115. children: Array<Entry<TaggedCommand>>
  116. } {
  117. const { children, ...rest } = section
  118. return {
  119. ...rest,
  120. children: children
  121. .map(child => {
  122. if (typeof child !== 'string') {
  123. const populatedChild = populateSectionOrGroup(child, registry)
  124. if (populatedChild.children.length === 0) {
  125. // Skip empty groups
  126. return undefined
  127. }
  128. return populatedChild
  129. }
  130. const command = registry.get(child)
  131. if (command) {
  132. return { ...command, type: 'command' as const }
  133. }
  134. return undefined
  135. })
  136. .filter(x => x !== undefined),
  137. }
  138. }
  139. function isTaggedCommand(item: Entry<TaggedCommand>): item is TaggedCommand {
  140. return 'type' in item && item.type === 'command'
  141. }