command-dropdown.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. type MenuSectionStructure<T> = {
  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. const CommandDropdownChild = ({ item }: { item: Entry<TaggedCommand> }) => {
  63. const onClickHandler = useCallback(() => {
  64. if (isTaggedCommand(item)) {
  65. item.handler?.({ location: 'menu-bar' })
  66. }
  67. }, [item])
  68. if (isTaggedCommand(item)) {
  69. return (
  70. <MenuBarOption
  71. key={item.id}
  72. title={item.label}
  73. // eslint-disable-next-line react/jsx-handler-names
  74. onClick={onClickHandler}
  75. href={item.href}
  76. disabled={item.disabled}
  77. />
  78. )
  79. } else {
  80. return (
  81. <NestedMenuBarDropdown title={item.title} id={item.id} key={item.id}>
  82. {item.children.map(subChild => {
  83. return <CommandDropdownChild item={subChild} key={subChild.id} />
  84. })}
  85. </NestedMenuBarDropdown>
  86. )
  87. }
  88. }
  89. export default CommandDropdown
  90. function populateSectionOrGroup<
  91. T extends { children: Array<Entry<CommandId>> },
  92. >(
  93. section: T,
  94. registry: Map<string, Command>
  95. ): Omit<T, 'children'> & {
  96. children: Array<Entry<TaggedCommand>>
  97. } {
  98. const { children, ...rest } = section
  99. return {
  100. ...rest,
  101. children: children
  102. .map(child => {
  103. if (typeof child !== 'string') {
  104. return populateSectionOrGroup(child, registry)
  105. }
  106. const command = registry.get(child)
  107. if (command) {
  108. return { ...command, type: 'command' as const }
  109. }
  110. return undefined
  111. })
  112. .filter(x => x !== undefined),
  113. }
  114. }
  115. function isTaggedCommand(item: Entry<TaggedCommand>): item is TaggedCommand {
  116. return 'type' in item && item.type === 'command'
  117. }