@shopify/cli-kit 3.28.0 → 3.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/api/common.d.ts +1 -1
  2. package/dist/api/common.js +7 -4
  3. package/dist/api/common.js.map +1 -1
  4. package/dist/api/graphql/find_org.js +2 -2
  5. package/dist/api/graphql/find_org.js.map +1 -1
  6. package/dist/api/identity.js +14 -4
  7. package/dist/api/identity.js.map +1 -1
  8. package/dist/api/partners.d.ts +0 -6
  9. package/dist/api/partners.js +0 -32
  10. package/dist/api/partners.js.map +1 -1
  11. package/dist/http/fetch.js +13 -0
  12. package/dist/http/fetch.js.map +1 -1
  13. package/dist/log.d.ts +1 -0
  14. package/dist/metadata.d.ts +2 -2
  15. package/dist/output.js +1 -1
  16. package/dist/output.js.map +1 -1
  17. package/dist/path.d.ts +4 -1
  18. package/dist/path.js +9 -1
  19. package/dist/path.js.map +1 -1
  20. package/dist/private/node/ui/components/Command.js.map +1 -1
  21. package/dist/private/node/ui/components/ConcurrentOutput.js +5 -1
  22. package/dist/private/node/ui/components/ConcurrentOutput.js.map +1 -1
  23. package/dist/{testing/fixtures/render-concurrent.d.ts → private/node/ui/components/ConcurrentOutput.test.d.ts} +0 -0
  24. package/dist/private/node/ui/components/ConcurrentOutput.test.js +53 -0
  25. package/dist/private/node/ui/components/ConcurrentOutput.test.js.map +1 -0
  26. package/dist/private/node/ui/components/FullScreen.js +2 -2
  27. package/dist/private/node/ui/components/FullScreen.js.map +1 -1
  28. package/dist/private/node/ui/components/Link.js.map +1 -1
  29. package/dist/private/node/ui/components/Prompt.d.ts +10 -0
  30. package/dist/private/node/ui/components/Prompt.js +23 -0
  31. package/dist/private/node/ui/components/Prompt.js.map +1 -0
  32. package/dist/private/node/ui/components/Prompt.test.d.ts +1 -0
  33. package/dist/private/node/ui/components/Prompt.test.js +71 -0
  34. package/dist/private/node/ui/components/Prompt.test.js.map +1 -0
  35. package/dist/private/node/ui/components/SelectInput.d.ts +12 -0
  36. package/dist/private/node/ui/components/SelectInput.js +93 -0
  37. package/dist/private/node/ui/components/SelectInput.js.map +1 -0
  38. package/dist/private/node/ui/components/SelectInput.test.d.ts +1 -0
  39. package/dist/private/node/ui/components/SelectInput.test.js +200 -0
  40. package/dist/private/node/ui/components/SelectInput.test.js.map +1 -0
  41. package/dist/private/node/ui/components/Table.d.ts +8 -0
  42. package/dist/private/node/ui/components/Table.js +17 -0
  43. package/dist/private/node/ui/components/Table.js.map +1 -0
  44. package/dist/private/node/ui/components/TextAnimation.js +1 -1
  45. package/dist/private/node/ui/components/TextAnimation.js.map +1 -1
  46. package/dist/private/node/ui.d.ts +2 -0
  47. package/dist/private/node/ui.js +14 -0
  48. package/dist/private/node/ui.js.map +1 -1
  49. package/dist/public/node/base-command.d.ts +2 -2
  50. package/dist/public/node/base-command.js +2 -2
  51. package/dist/public/node/base-command.js.map +1 -1
  52. package/dist/public/node/node-package-manager.d.ts +1 -0
  53. package/dist/public/node/ruby.js +2 -2
  54. package/dist/public/node/ruby.js.map +1 -1
  55. package/dist/public/node/ui.d.ts +42 -0
  56. package/dist/public/node/ui.js +60 -6
  57. package/dist/public/node/ui.js.map +1 -1
  58. package/dist/session/exchange.js +4 -2
  59. package/dist/session/exchange.js.map +1 -1
  60. package/dist/session/validate.js +3 -13
  61. package/dist/session/validate.js.map +1 -1
  62. package/dist/session.d.ts +1 -1
  63. package/dist/session.js +3 -1
  64. package/dist/session.js.map +1 -1
  65. package/dist/string.d.ts +1 -0
  66. package/dist/string.js +3 -0
  67. package/dist/string.js.map +1 -1
  68. package/dist/system.d.ts +2 -2
  69. package/dist/system.js +17 -2
  70. package/dist/system.js.map +1 -1
  71. package/dist/testing/ui.d.ts +4 -8
  72. package/dist/testing/ui.js +17 -17
  73. package/dist/testing/ui.js.map +1 -1
  74. package/dist/tsconfig.tsbuildinfo +1 -1
  75. package/dist/ui/executor.js +7 -5
  76. package/dist/ui/executor.js.map +1 -1
  77. package/dist/ui/inquirer/autocomplete.js +13 -4
  78. package/dist/ui/inquirer/autocomplete.js.map +1 -1
  79. package/dist/ui.d.ts +6 -0
  80. package/dist/ui.js.map +1 -1
  81. package/package.json +35 -6
  82. package/dist/private/node/ui/error.d.ts +0 -2
  83. package/dist/private/node/ui/error.js +0 -8
  84. package/dist/private/node/ui/error.js.map +0 -1
  85. package/dist/testing/fixtures/render-concurrent.js +0 -26
  86. package/dist/testing/fixtures/render-concurrent.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"FullScreen.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/FullScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,GAAG,EAAC,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AAEhD;;;;GAIG;AACH,MAAM,UAAU,GAAa,CAAC,KAAmC,EAAe,EAAE;IAChF,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC;QAC/B,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;QAC/B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;KAC1B,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,QAAQ;YACf,OAAO,CAAC;gBACN,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;gBAC/B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;aAC1B,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACrC,gCAAgC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QACrC,OAAO,GAAG,EAAE;YACV,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACtC,iCAAiC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QACvC,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,oBAAC,GAAG,IAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,IACxC,KAAK,CAAC,QAAQ,CACX,CACP,CAAA;AACH,CAAC,CAAA;AAED,eAAe,UAAU,CAAA","sourcesContent":["import {Box} from 'ink'\nimport React, {useEffect, useState} from 'react'\n\n/**\n * `FullScreen` renders all output in a new buffer and makes it full screen. This is useful when:\n * - You want to preserve terminal history. `ink` [normally clears the terminal history](https://github.com/vadimdemedes/ink/issues/382) if the rendered output is taller than the terminal window. By rendering in a separate buffer history will be preserved and will be visible after pressing `Ctrl+C`.\n * - You want to respond to the resize event of the terminal. Whenever the user resizes their terminal window the output's height and width will be recalculated and re-rendered properly.\n */\nconst FullScreen: React.FC = (props: {children?: React.ReactNode}): JSX.Element => {\n const [size, setSize] = useState({\n columns: process.stdout.columns,\n rows: process.stdout.rows,\n })\n\n useEffect(() => {\n function onResize() {\n setSize({\n columns: process.stdout.columns,\n rows: process.stdout.rows,\n })\n }\n\n process.stdout.on('resize', onResize)\n // switch to an alternate buffer\n process.stdout.write('\\u001B[?1049h')\n return () => {\n process.stdout.off('resize', onResize)\n // switch back to the main buffer\n process.stdout.write('\\u001B[?1049l')\n }\n }, [])\n\n return (\n <Box width={size.columns} height={size.rows}>\n {props.children}\n </Box>\n )\n}\n\nexport default FullScreen\n"]}
1
+ {"version":3,"file":"FullScreen.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/FullScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,GAAG,EAAC,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AAEhD;;;;GAIG;AACH,MAAM,UAAU,GAAa,CAAC,EAAC,QAAQ,EAAC,EAAe,EAAE;IACvD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC;QAC/B,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;QAC/B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;KAC1B,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,QAAQ;YACf,OAAO,CAAC;gBACN,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;gBAC/B,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;aAC1B,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACrC,gCAAgC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QACrC,OAAO,GAAG,EAAE;YACV,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACtC,iCAAiC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QACvC,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,oBAAC,GAAG,IAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,IACxC,QAAQ,CACL,CACP,CAAA;AACH,CAAC,CAAA;AAED,eAAe,UAAU,CAAA","sourcesContent":["import {Box} from 'ink'\nimport React, {useEffect, useState} from 'react'\n\n/**\n * `FullScreen` renders all output in a new buffer and makes it full screen. This is useful when:\n * - You want to preserve terminal history. `ink` [normally clears the terminal history](https://github.com/vadimdemedes/ink/issues/382) if the rendered output is taller than the terminal window. By rendering in a separate buffer history will be preserved and will be visible after pressing `Ctrl+C`.\n * - You want to respond to the resize event of the terminal. Whenever the user resizes their terminal window the output's height and width will be recalculated and re-rendered properly.\n */\nconst FullScreen: React.FC = ({children}): JSX.Element => {\n const [size, setSize] = useState({\n columns: process.stdout.columns,\n rows: process.stdout.rows,\n })\n\n useEffect(() => {\n function onResize() {\n setSize({\n columns: process.stdout.columns,\n rows: process.stdout.rows,\n })\n }\n\n process.stdout.on('resize', onResize)\n // switch to an alternate buffer\n process.stdout.write('\\u001B[?1049h')\n return () => {\n process.stdout.off('resize', onResize)\n // switch back to the main buffer\n process.stdout.write('\\u001B[?1049l')\n }\n }, [])\n\n return (\n <Box width={size.columns} height={size.rows}>\n {children}\n </Box>\n )\n}\n\nexport default FullScreen\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"Link.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/Link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAC,IAAI,EAAC,MAAM,KAAK,CAAA;AACxB,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,YAAY,MAAM,eAAe,CAAA;AAOxC,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAA;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,IAAI,GAAoB,CAAC,EAAC,GAAG,EAAE,KAAK,EAAiC,EAAe,EAAE;IAC1F,OAAO,oBAAC,IAAI,QAAE,YAAY,CAAC,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,EAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAC,CAAC,CAAQ,CAAA;AAC7F,CAAC,CAAA;AAED,OAAO,EAAC,IAAI,EAAC,CAAA","sourcesContent":["import chalk from 'chalk'\nimport {Text} from 'ink'\nimport React from 'react'\nimport terminalLink from 'terminal-link'\n\ninterface Props {\n url: string\n label?: string\n}\n\nfunction fallback(text: string, url: string) {\n return `${text} ${chalk.dim(`(${url})`)}`\n}\n\n/**\n * `Link` displays a clickable link when supported by the terminal.\n */\nconst Link: React.FC<Props> = ({url, label}: React.PropsWithChildren<Props>): JSX.Element => {\n return <Text>{terminalLink(label ?? url, url, {fallback: label ? fallback : false})}</Text>\n}\n\nexport {Link}\n"]}
1
+ {"version":3,"file":"Link.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/Link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAC,IAAI,EAAC,MAAM,KAAK,CAAA;AACxB,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,YAAY,MAAM,eAAe,CAAA;AAOxC,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAW;IACzC,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAA;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,IAAI,GAAoB,CAAC,EAAC,GAAG,EAAE,KAAK,EAAC,EAAe,EAAE;IAC1D,OAAO,oBAAC,IAAI,QAAE,YAAY,CAAC,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,EAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAC,CAAC,CAAQ,CAAA;AAC7F,CAAC,CAAA;AAED,OAAO,EAAC,IAAI,EAAC,CAAA","sourcesContent":["import chalk from 'chalk'\nimport {Text} from 'ink'\nimport React from 'react'\nimport terminalLink from 'terminal-link'\n\ninterface Props {\n url: string\n label?: string\n}\n\nfunction fallback(text: string, url: string) {\n return `${text} ${chalk.dim(`(${url})`)}`\n}\n\n/**\n * `Link` displays a clickable link when supported by the terminal.\n */\nconst Link: React.FC<Props> = ({url, label}): JSX.Element => {\n return <Text>{terminalLink(label ?? url, url, {fallback: label ? fallback : false})}</Text>\n}\n\nexport {Link}\n"]}
@@ -0,0 +1,10 @@
1
+ import { Props as SelectProps } from './SelectInput.js';
2
+ import { Props as TableProps } from './Table.js';
3
+ import React from 'react';
4
+ export interface Props<T> {
5
+ message: string;
6
+ choices: SelectProps<T>['items'];
7
+ onChoose: SelectProps<T>['onSelect'];
8
+ infoTable?: TableProps['table'];
9
+ }
10
+ export default function Prompt<T>({ message, choices, infoTable, onChoose, }: React.PropsWithChildren<Props<T>>): JSX.Element | null;
@@ -0,0 +1,23 @@
1
+ import SelectInput from './SelectInput.js';
2
+ import Table from './Table.js';
3
+ import React, { useState } from 'react';
4
+ import { Box, Text } from 'ink';
5
+ import { figures } from 'listr2';
6
+ export default function Prompt({ message, choices, infoTable, onChoose, }) {
7
+ const [answer, setAnswer] = useState(null);
8
+ return (React.createElement(Box, { flexDirection: "column" },
9
+ React.createElement(Box, null,
10
+ React.createElement(Box, { marginRight: 2 },
11
+ React.createElement(Text, null, "?")),
12
+ React.createElement(Text, null, message)),
13
+ infoTable && !answer && (React.createElement(Box, { marginLeft: 7 },
14
+ React.createElement(Table, { table: infoTable }))),
15
+ answer ? (React.createElement(Box, null,
16
+ React.createElement(Box, { marginRight: 2 },
17
+ React.createElement(Text, { color: "cyan" }, figures.tick)),
18
+ React.createElement(Text, { color: "cyan" }, answer.label))) : (React.createElement(SelectInput, { items: choices, onSelect: (value) => {
19
+ setAnswer(choices.find((choice) => choice.value === value) ?? null);
20
+ onChoose(value);
21
+ } }))));
22
+ }
23
+ //# sourceMappingURL=Prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Prompt.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/Prompt.tsx"],"names":[],"mappings":"AAAA,OAAO,WAAuD,MAAM,kBAAkB,CAAA;AACtF,OAAO,KAA4B,MAAM,YAAY,CAAA;AACrD,OAAO,KAAK,EAAE,EAAC,QAAQ,EAAC,MAAM,OAAO,CAAA;AACrC,OAAO,EAAC,GAAG,EAAE,IAAI,EAAC,MAAM,KAAK,CAAA;AAC7B,OAAO,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAA;AAS9B,MAAM,CAAC,OAAO,UAAU,MAAM,CAAI,EAChC,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GAC0B;IAClC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAuB,IAAI,CAAC,CAAA;IAEhE,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ;QACzB,oBAAC,GAAG;YACF,oBAAC,GAAG,IAAC,WAAW,EAAE,CAAC;gBACjB,oBAAC,IAAI,YAAS,CACV;YACN,oBAAC,IAAI,QAAE,OAAO,CAAQ,CAClB;QACL,SAAS,IAAI,CAAC,MAAM,IAAI,CACvB,oBAAC,GAAG,IAAC,UAAU,EAAE,CAAC;YAChB,oBAAC,KAAK,IAAC,KAAK,EAAE,SAAS,GAAI,CACvB,CACP;QACA,MAAM,CAAC,CAAC,CAAC,CACR,oBAAC,GAAG;YACF,oBAAC,GAAG,IAAC,WAAW,EAAE,CAAC;gBACjB,oBAAC,IAAI,IAAC,KAAK,EAAC,MAAM,IAAE,OAAO,CAAC,IAAI,CAAQ,CACpC;YAEN,oBAAC,IAAI,IAAC,KAAK,EAAC,MAAM,IAAE,MAAM,CAAC,KAAK,CAAQ,CACpC,CACP,CAAC,CAAC,CAAC,CACF,oBAAC,WAAW,IACV,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,CAAC,KAAQ,EAAE,EAAE;gBACrB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,CAAA;gBACnE,QAAQ,CAAC,KAAK,CAAC,CAAA;YACjB,CAAC,GACD,CACH,CACG,CACP,CAAA;AACH,CAAC","sourcesContent":["import SelectInput, {Props as SelectProps, Item as SelectItem} from './SelectInput.js'\nimport Table, {Props as TableProps} from './Table.js'\nimport React, {useState} from 'react'\nimport {Box, Text} from 'ink'\nimport {figures} from 'listr2'\n\nexport interface Props<T> {\n message: string\n choices: SelectProps<T>['items']\n onChoose: SelectProps<T>['onSelect']\n infoTable?: TableProps['table']\n}\n\nexport default function Prompt<T>({\n message,\n choices,\n infoTable,\n onChoose,\n}: React.PropsWithChildren<Props<T>>): JSX.Element | null {\n const [answer, setAnswer] = useState<SelectItem<T> | null>(null)\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Box marginRight={2}>\n <Text>?</Text>\n </Box>\n <Text>{message}</Text>\n </Box>\n {infoTable && !answer && (\n <Box marginLeft={7}>\n <Table table={infoTable} />\n </Box>\n )}\n {answer ? (\n <Box>\n <Box marginRight={2}>\n <Text color=\"cyan\">{figures.tick}</Text>\n </Box>\n\n <Text color=\"cyan\">{answer.label}</Text>\n </Box>\n ) : (\n <SelectInput\n items={choices}\n onSelect={(value: T) => {\n setAnswer(choices.find((choice) => choice.value === value) ?? null)\n onChoose(value)\n }}\n />\n )}\n </Box>\n )\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import Prompt from './Prompt.js';
2
+ import { sendInput, waitForInputsToBeReady } from '../../../../testing/ui.js';
3
+ import { describe, expect, test, vi } from 'vitest';
4
+ import React from 'react';
5
+ import { render } from 'ink-testing-library';
6
+ const ARROW_DOWN = '\u001B[B';
7
+ const ENTER = '\r';
8
+ describe('Prompt', async () => {
9
+ test('choose an answer', async () => {
10
+ const onEnter = vi.fn();
11
+ const items = [
12
+ { label: 'first', value: 'first' },
13
+ { label: 'second', value: 'second' },
14
+ { label: 'third', value: 'third' },
15
+ ];
16
+ const infoTable = { Add: ['new-ext'], Remove: ['integrated-demand-ext', 'order-discount'] };
17
+ const renderInstance = render(React.createElement(Prompt, { message: "Associate your project with the org Castile Ventures?", choices: items, infoTable: infoTable, onChoose: onEnter }));
18
+ await waitForInputsToBeReady();
19
+ await sendInput(renderInstance, ARROW_DOWN);
20
+ await sendInput(renderInstance, ENTER);
21
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
22
+ "? Associate your project with the org Castile Ventures?
23
+ ✔ second"
24
+ `);
25
+ expect(onEnter).toHaveBeenCalledWith(items[1].value);
26
+ });
27
+ test('supports an info table', async () => {
28
+ const items = [
29
+ { label: 'first', value: 'first', key: 'f' },
30
+ { label: 'second', value: 'second', key: 's' },
31
+ { label: 'third', value: 'third' },
32
+ { label: 'fourth', value: 'fourth' },
33
+ { label: 'fifth', value: 'fifth', group: 'Automations' },
34
+ { label: 'sixth', value: 'sixth', group: 'Automations' },
35
+ { label: 'seventh', value: 'seventh' },
36
+ { label: 'eighth', value: 'eighth', group: 'Merchant Admin' },
37
+ { label: 'ninth', value: 'ninth', group: 'Merchant Admin' },
38
+ { label: 'tenth', value: 'tenth' },
39
+ ];
40
+ const infoTable = {
41
+ Add: ['new-ext'],
42
+ Remove: ['integrated-demand-ext', 'order-discount'],
43
+ };
44
+ const renderInstance = render(React.createElement(Prompt, { message: "Associate your project with the org Castile Ventures?", choices: items, infoTable: infoTable, onChoose: () => { } }));
45
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
46
+ "? Associate your project with the org Castile Ventures?
47
+
48
+ Add: • new-ext
49
+ Remove: • integrated-demand-ext
50
+ • order-discount
51
+
52
+ > (f) first
53
+ (s) second
54
+ (3) third
55
+ (4) fourth
56
+ (5) seventh
57
+ (6) tenth
58
+
59
+ Automations
60
+ (7) fifth
61
+ (8) sixth
62
+
63
+ Merchant Admin
64
+ (9) eighth
65
+ (10) ninth
66
+
67
+ navigate with arrows, enter to select"
68
+ `);
69
+ });
70
+ });
71
+ //# sourceMappingURL=Prompt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Prompt.test.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/Prompt.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import Prompt from './Prompt.js'\nimport {sendInput, waitForInputsToBeReady} from '../../../../testing/ui.js'\nimport {describe, expect, test, vi} from 'vitest'\nimport React from 'react'\nimport {render} from 'ink-testing-library'\n\nconst ARROW_DOWN = '\\u001B[B'\nconst ENTER = '\\r'\n\ndescribe('Prompt', async () => {\n test('choose an answer', async () => {\n const onEnter = vi.fn()\n\n const items = [\n {label: 'first', value: 'first'},\n {label: 'second', value: 'second'},\n {label: 'third', value: 'third'},\n ]\n\n const infoTable = {Add: ['new-ext'], Remove: ['integrated-demand-ext', 'order-discount']}\n\n const renderInstance = render(\n <Prompt\n message=\"Associate your project with the org Castile Ventures?\"\n choices={items}\n infoTable={infoTable}\n onChoose={onEnter}\n />,\n )\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, ARROW_DOWN)\n await sendInput(renderInstance, ENTER)\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \"? Associate your project with the org Castile Ventures?\n \u001b[36m✔\u001b[39m \u001b[36msecond\u001b[39m\"\n `)\n expect(onEnter).toHaveBeenCalledWith(items[1]!.value)\n })\n\n test('supports an info table', async () => {\n const items = [\n {label: 'first', value: 'first', key: 'f'},\n {label: 'second', value: 'second', key: 's'},\n {label: 'third', value: 'third'},\n {label: 'fourth', value: 'fourth'},\n {label: 'fifth', value: 'fifth', group: 'Automations'},\n {label: 'sixth', value: 'sixth', group: 'Automations'},\n {label: 'seventh', value: 'seventh'},\n {label: 'eighth', value: 'eighth', group: 'Merchant Admin'},\n {label: 'ninth', value: 'ninth', group: 'Merchant Admin'},\n {label: 'tenth', value: 'tenth'},\n ]\n\n const infoTable = {\n Add: ['new-ext'],\n Remove: ['integrated-demand-ext', 'order-discount'],\n }\n\n const renderInstance = render(\n <Prompt\n message=\"Associate your project with the org Castile Ventures?\"\n choices={items}\n infoTable={infoTable}\n onChoose={() => {}}\n />,\n )\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \"? Associate your project with the org Castile Ventures?\n\n Add: • new-ext\n Remove: • integrated-demand-ext\n • order-discount\n\n \u001b[36m>\u001b[39m \u001b[36m(f) first\u001b[39m\n (s) second\n (3) third\n (4) fourth\n (5) seventh\n (6) tenth\n\n \u001b[1mAutomations\u001b[22m\n (7) fifth\n (8) sixth\n\n \u001b[1mMerchant Admin\u001b[22m\n (9) eighth\n (10) ninth\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n})\n"]}
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ export interface Props<T> {
3
+ items: Item<T>[];
4
+ onSelect: (value: T) => void;
5
+ }
6
+ export interface Item<T> {
7
+ label: string;
8
+ value: T;
9
+ key?: string;
10
+ group?: string;
11
+ }
12
+ export default function SelectInput<T>({ items, onSelect }: React.PropsWithChildren<Props<T>>): JSX.Element | null;
@@ -0,0 +1,93 @@
1
+ import { isTruthy } from '../../../../environment/utilities.js';
2
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
3
+ import { Box, Text, useApp, useInput } from 'ink';
4
+ import { groupBy, isEqual, mapValues } from 'lodash-es';
5
+ function groupItems(items) {
6
+ let index = 0;
7
+ return mapValues(groupBy(items, 'group'), (groupItems) => groupItems.map((groupItem) => {
8
+ const item = { ...groupItem, key: groupItem.key ?? (index + 1).toString(), index };
9
+ index += 1;
10
+ return item;
11
+ }));
12
+ }
13
+ export default function SelectInput({ items, onSelect }) {
14
+ const [inputStack, setInputStack] = useState(null);
15
+ const [inputTimeout, setInputTimeout] = useState(null);
16
+ const [selectedIndex, setSelectedIndex] = useState(0);
17
+ const keys = useRef(new Set(items.map((item) => item.key)));
18
+ const { exit: unmountInk } = useApp();
19
+ const groupedItems = groupItems(items);
20
+ const groupTitles = Object.keys(groupedItems);
21
+ const previousItems = useRef(items);
22
+ // reset index when items change
23
+ useEffect(() => {
24
+ if (!isEqual(previousItems.current.map((item) => item.value), items.map((item) => item.value))) {
25
+ setSelectedIndex(0);
26
+ }
27
+ previousItems.current = items;
28
+ }, [items]);
29
+ const handleInput = useCallback((input, key) => {
30
+ if (input === 'c' && key.ctrl) {
31
+ // Exceptions being throw in these hooks aren't being caught by our errorHandler.
32
+ // See also how we handle exceptions in CouncurrentOutput for reference.
33
+ process.exit(1);
34
+ }
35
+ const parsedInput = parseInt(input, 10);
36
+ if (parsedInput !== 0 && parsedInput <= items.length + 1) {
37
+ setSelectedIndex(parsedInput - 1);
38
+ }
39
+ else if (keys.current.has(input)) {
40
+ const index = items.findIndex((item) => item.key === input);
41
+ if (index !== -1) {
42
+ setSelectedIndex(index);
43
+ }
44
+ }
45
+ if (key.upArrow) {
46
+ const lastIndex = items.length - 1;
47
+ setSelectedIndex(selectedIndex === 0 ? lastIndex : selectedIndex - 1);
48
+ }
49
+ else if (key.downArrow) {
50
+ setSelectedIndex(selectedIndex === items.length - 1 ? 0 : selectedIndex + 1);
51
+ }
52
+ else if (key.return) {
53
+ onSelect(items[selectedIndex].value);
54
+ // This is a workaround needed because Ink behaves differently in CI when
55
+ // unmounting. See https://github.com/vadimdemedes/ink/pull/266
56
+ if (!isTruthy(process.env.CI))
57
+ unmountInk();
58
+ }
59
+ }, [selectedIndex, items, onSelect]);
60
+ useInput((input, key) => {
61
+ if (input.length > 0 && Object.values(key).every((value) => value === false)) {
62
+ const newInputStack = inputStack === null ? input : inputStack + input;
63
+ setInputStack(newInputStack);
64
+ if (inputTimeout !== null) {
65
+ clearTimeout(inputTimeout);
66
+ }
67
+ setInputTimeout(setTimeout(() => {
68
+ handleInput(newInputStack, key);
69
+ setInputStack(null);
70
+ setInputTimeout(null);
71
+ }, 300));
72
+ }
73
+ else {
74
+ handleInput(input, key);
75
+ }
76
+ });
77
+ return (React.createElement(Box, { flexDirection: "column" },
78
+ groupTitles.map((title) => {
79
+ const hasTitle = title !== 'undefined';
80
+ return (React.createElement(Box, { key: title, flexDirection: "column", marginTop: hasTitle ? 1 : 0 },
81
+ hasTitle && (React.createElement(Box, { marginLeft: 3 },
82
+ React.createElement(Text, { bold: true }, title))),
83
+ groupedItems[title].map((item) => {
84
+ const isSelected = item.index === selectedIndex;
85
+ return (React.createElement(Box, { key: item.key },
86
+ React.createElement(Box, { marginRight: 2 }, isSelected ? React.createElement(Text, { color: "cyan" }, `>`) : React.createElement(Text, null, " ")),
87
+ React.createElement(Text, { color: isSelected ? 'cyan' : undefined }, `(${item.key}) ${item.label}`)));
88
+ })));
89
+ }),
90
+ React.createElement(Box, { marginTop: 1, marginLeft: 3 },
91
+ React.createElement(Text, { dimColor: true }, "navigate with arrows, enter to select"))));
92
+ }
93
+ //# sourceMappingURL=SelectInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectInput.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/SelectInput.tsx"],"names":[],"mappings":"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","sourcesContent":["import {isTruthy} from '../../../../environment/utilities.js'\nimport React, {useState, useEffect, useRef, useCallback} from 'react'\nimport {Box, Key, Text, useApp, useInput} from 'ink'\nimport {groupBy, isEqual, mapValues} from 'lodash-es'\n\nexport interface Props<T> {\n items: Item<T>[]\n onSelect: (value: T) => void\n}\n\nexport interface Item<T> {\n label: string\n value: T\n key?: string\n group?: string\n}\n\nfunction groupItems<T>(items: Item<T>[]) {\n let index = 0\n\n return mapValues(groupBy(items, 'group'), (groupItems) =>\n groupItems.map((groupItem) => {\n const item = {...groupItem, key: groupItem.key ?? (index + 1).toString(), index}\n index += 1\n return item\n }),\n )\n}\n\nexport default function SelectInput<T>({items, onSelect}: React.PropsWithChildren<Props<T>>): JSX.Element | null {\n const [inputStack, setInputStack] = useState<string | null>(null)\n const [inputTimeout, setInputTimeout] = useState<NodeJS.Timeout | null>(null)\n const [selectedIndex, setSelectedIndex] = useState(0)\n const keys = useRef(new Set(items.map((item) => item.key)))\n const {exit: unmountInk} = useApp()\n const groupedItems = groupItems(items)\n const groupTitles = Object.keys(groupedItems)\n\n const previousItems = useRef<Item<T>[]>(items)\n\n // reset index when items change\n useEffect(() => {\n if (\n !isEqual(\n previousItems.current.map((item) => item.value),\n items.map((item) => item.value),\n )\n ) {\n setSelectedIndex(0)\n }\n\n previousItems.current = items\n }, [items])\n\n const handleInput = useCallback(\n (input: string, key: Key) => {\n if (input === 'c' && key.ctrl) {\n // Exceptions being throw in these hooks aren't being caught by our errorHandler.\n // See also how we handle exceptions in CouncurrentOutput for reference.\n process.exit(1)\n }\n\n const parsedInput = parseInt(input, 10)\n\n if (parsedInput !== 0 && parsedInput <= items.length + 1) {\n setSelectedIndex(parsedInput - 1)\n } else if (keys.current.has(input)) {\n const index = items.findIndex((item) => item.key === input)\n if (index !== -1) {\n setSelectedIndex(index)\n }\n }\n\n if (key.upArrow) {\n const lastIndex = items.length - 1\n\n setSelectedIndex(selectedIndex === 0 ? lastIndex : selectedIndex - 1)\n } else if (key.downArrow) {\n setSelectedIndex(selectedIndex === items.length - 1 ? 0 : selectedIndex + 1)\n } else if (key.return) {\n onSelect(items[selectedIndex]!.value)\n // This is a workaround needed because Ink behaves differently in CI when\n // unmounting. See https://github.com/vadimdemedes/ink/pull/266\n if (!isTruthy(process.env.CI)) unmountInk()\n }\n },\n [selectedIndex, items, onSelect],\n )\n\n useInput((input, key) => {\n if (input.length > 0 && Object.values(key).every((value) => value === false)) {\n const newInputStack = inputStack === null ? input : inputStack + input\n\n setInputStack(newInputStack)\n\n if (inputTimeout !== null) {\n clearTimeout(inputTimeout)\n }\n\n setInputTimeout(\n setTimeout(() => {\n handleInput(newInputStack, key)\n setInputStack(null)\n setInputTimeout(null)\n }, 300),\n )\n } else {\n handleInput(input, key)\n }\n })\n\n return (\n <Box flexDirection=\"column\">\n {groupTitles.map((title) => {\n const hasTitle = title !== 'undefined'\n\n return (\n <Box key={title} flexDirection=\"column\" marginTop={hasTitle ? 1 : 0}>\n {hasTitle && (\n <Box marginLeft={3}>\n <Text bold>{title}</Text>\n </Box>\n )}\n {groupedItems[title]!.map((item) => {\n const isSelected = item.index === selectedIndex\n\n return (\n <Box key={item.key}>\n <Box marginRight={2}>{isSelected ? <Text color=\"cyan\">{`>`}</Text> : <Text> </Text>}</Box>\n\n <Text color={isSelected ? 'cyan' : undefined}>{`(${item.key}) ${item.label}`}</Text>\n </Box>\n )\n })}\n </Box>\n )\n })}\n\n <Box marginTop={1} marginLeft={3}>\n <Text dimColor>navigate with arrows, enter to select</Text>\n </Box>\n </Box>\n )\n}\n"]}
@@ -0,0 +1,200 @@
1
+ import SelectInput from './SelectInput.js';
2
+ import { waitForInputsToBeReady, waitForChange, sendInput } from '../../../../testing/ui.js';
3
+ import { describe, expect, test, vi } from 'vitest';
4
+ import React from 'react';
5
+ import { render } from 'ink-testing-library';
6
+ const ARROW_UP = '\u001B[A';
7
+ const ARROW_DOWN = '\u001B[B';
8
+ const ENTER = '\r';
9
+ describe('SelectInput', async () => {
10
+ test('move up with up arrow key', async () => {
11
+ const items = [
12
+ {
13
+ label: 'First',
14
+ value: 'first',
15
+ },
16
+ {
17
+ label: 'Second',
18
+ value: 'second',
19
+ },
20
+ {
21
+ label: 'Third',
22
+ value: 'third',
23
+ },
24
+ ];
25
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
26
+ await waitForInputsToBeReady();
27
+ await sendInput(renderInstance, ARROW_UP);
28
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
29
+ " (1) First
30
+ (2) Second
31
+ > (3) Third
32
+
33
+ navigate with arrows, enter to select"
34
+ `);
35
+ });
36
+ test('move down with down arrow key', async () => {
37
+ const items = [
38
+ {
39
+ label: 'First',
40
+ value: 'first',
41
+ },
42
+ {
43
+ label: 'Second',
44
+ value: 'second',
45
+ },
46
+ {
47
+ label: 'Third',
48
+ value: 'third',
49
+ },
50
+ ];
51
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
52
+ await waitForInputsToBeReady();
53
+ await sendInput(renderInstance, ARROW_DOWN);
54
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
55
+ " (1) First
56
+ > (2) Second
57
+ (3) Third
58
+
59
+ navigate with arrows, enter to select"
60
+ `);
61
+ });
62
+ test('select item with enter key', async () => {
63
+ const onEnter = vi.fn();
64
+ const items = [
65
+ {
66
+ label: 'First',
67
+ value: 'first',
68
+ },
69
+ {
70
+ label: 'Second',
71
+ value: 'second',
72
+ },
73
+ {
74
+ label: 'Third',
75
+ value: 'third',
76
+ },
77
+ ];
78
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: onEnter }));
79
+ await waitForInputsToBeReady();
80
+ await sendInput(renderInstance, ARROW_DOWN);
81
+ await waitForChange(() => renderInstance.stdin.write(ENTER), () => onEnter.mock.calls.length);
82
+ expect(onEnter).toHaveBeenCalledWith(items[1].value);
83
+ });
84
+ test('handles keys with multiple digits', async () => {
85
+ const items = [
86
+ {
87
+ label: 'First',
88
+ value: 'first',
89
+ },
90
+ {
91
+ label: 'Second',
92
+ value: 'second',
93
+ },
94
+ {
95
+ label: 'Tenth',
96
+ value: 'tenth',
97
+ key: '10',
98
+ },
99
+ ];
100
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
101
+ await waitForInputsToBeReady();
102
+ await sendInput(renderInstance, '1', '0');
103
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
104
+ " (1) First
105
+ (2) Second
106
+ > (10) Tenth
107
+
108
+ navigate with arrows, enter to select"
109
+ `);
110
+ });
111
+ test('handles custom keys', async () => {
112
+ const items = [
113
+ {
114
+ label: 'First',
115
+ value: 'first',
116
+ },
117
+ {
118
+ label: 'Second',
119
+ value: 'second',
120
+ },
121
+ {
122
+ label: 'Third',
123
+ value: 'third',
124
+ key: 't',
125
+ },
126
+ ];
127
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
128
+ await waitForInputsToBeReady();
129
+ await sendInput(renderInstance, 't');
130
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
131
+ " (1) First
132
+ (2) Second
133
+ > (t) Third
134
+
135
+ navigate with arrows, enter to select"
136
+ `);
137
+ });
138
+ test('rotate after reaching the end of the list', async () => {
139
+ const items = [
140
+ {
141
+ label: 'First',
142
+ value: 'first',
143
+ },
144
+ {
145
+ label: 'Second',
146
+ value: 'second',
147
+ },
148
+ {
149
+ label: 'Third',
150
+ value: 'third',
151
+ },
152
+ ];
153
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
154
+ await waitForInputsToBeReady();
155
+ await sendInput(renderInstance, ARROW_DOWN);
156
+ await sendInput(renderInstance, ARROW_DOWN);
157
+ await sendInput(renderInstance, ARROW_DOWN);
158
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
159
+ "> (1) First
160
+ (2) Second
161
+ (3) Third
162
+
163
+ navigate with arrows, enter to select"
164
+ `);
165
+ });
166
+ test('support groups', async () => {
167
+ const items = [
168
+ { label: 'first', value: 'first', key: 'f' },
169
+ { label: 'second', value: 'second', key: 's' },
170
+ { label: 'third', value: 'third' },
171
+ { label: 'fourth', value: 'fourth' },
172
+ { label: 'fifth', value: 'fifth', group: 'Automations' },
173
+ { label: 'sixth', value: 'sixth', group: 'Automations' },
174
+ { label: 'seventh', value: 'seventh' },
175
+ { label: 'eighth', value: 'eighth', group: 'Merchant Admin' },
176
+ { label: 'ninth', value: 'ninth', group: 'Merchant Admin' },
177
+ { label: 'tenth', value: 'tenth' },
178
+ ];
179
+ const renderInstance = render(React.createElement(SelectInput, { items: items, onSelect: () => { } }));
180
+ expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`
181
+ "> (f) first
182
+ (s) second
183
+ (3) third
184
+ (4) fourth
185
+ (5) seventh
186
+ (6) tenth
187
+
188
+ Automations
189
+ (7) fifth
190
+ (8) sixth
191
+
192
+ Merchant Admin
193
+ (9) eighth
194
+ (10) ninth
195
+
196
+ navigate with arrows, enter to select"
197
+ `);
198
+ });
199
+ });
200
+ //# sourceMappingURL=SelectInput.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectInput.test.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/SelectInput.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import SelectInput from './SelectInput.js'\nimport {waitForInputsToBeReady, waitForChange, sendInput} from '../../../../testing/ui.js'\nimport {describe, expect, test, vi} from 'vitest'\nimport React from 'react'\nimport {render} from 'ink-testing-library'\n\nconst ARROW_UP = '\\u001B[A'\nconst ARROW_DOWN = '\\u001B[B'\nconst ENTER = '\\r'\n\ndescribe('SelectInput', async () => {\n test('move up with up arrow key', async () => {\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Third',\n value: 'third',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, ARROW_UP)\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \" (1) First\n (2) Second\n \u001b[36m>\u001b[39m \u001b[36m(3) Third\u001b[39m\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n\n test('move down with down arrow key', async () => {\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Third',\n value: 'third',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, ARROW_DOWN)\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \" (1) First\n \u001b[36m>\u001b[39m \u001b[36m(2) Second\u001b[39m\n (3) Third\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n\n test('select item with enter key', async () => {\n const onEnter = vi.fn()\n\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Third',\n value: 'third',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={onEnter} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, ARROW_DOWN)\n await waitForChange(\n () => renderInstance.stdin.write(ENTER),\n () => onEnter.mock.calls.length,\n )\n\n expect(onEnter).toHaveBeenCalledWith(items[1]!.value)\n })\n\n test('handles keys with multiple digits', async () => {\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Tenth',\n value: 'tenth',\n key: '10',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, '1', '0')\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \" (1) First\n (2) Second\n \u001b[36m>\u001b[39m \u001b[36m(10) Tenth\u001b[39m\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n\n test('handles custom keys', async () => {\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Third',\n value: 'third',\n key: 't',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, 't')\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \" (1) First\n (2) Second\n \u001b[36m>\u001b[39m \u001b[36m(t) Third\u001b[39m\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n\n test('rotate after reaching the end of the list', async () => {\n const items = [\n {\n label: 'First',\n value: 'first',\n },\n {\n label: 'Second',\n value: 'second',\n },\n {\n label: 'Third',\n value: 'third',\n },\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n await waitForInputsToBeReady()\n await sendInput(renderInstance, ARROW_DOWN)\n await sendInput(renderInstance, ARROW_DOWN)\n await sendInput(renderInstance, ARROW_DOWN)\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \"\u001b[36m>\u001b[39m \u001b[36m(1) First\u001b[39m\n (2) Second\n (3) Third\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n\n test('support groups', async () => {\n const items = [\n {label: 'first', value: 'first', key: 'f'},\n {label: 'second', value: 'second', key: 's'},\n {label: 'third', value: 'third'},\n {label: 'fourth', value: 'fourth'},\n {label: 'fifth', value: 'fifth', group: 'Automations'},\n {label: 'sixth', value: 'sixth', group: 'Automations'},\n {label: 'seventh', value: 'seventh'},\n {label: 'eighth', value: 'eighth', group: 'Merchant Admin'},\n {label: 'ninth', value: 'ninth', group: 'Merchant Admin'},\n {label: 'tenth', value: 'tenth'},\n ]\n\n const renderInstance = render(<SelectInput items={items} onSelect={() => {}} />)\n\n expect(renderInstance.lastFrame()).toMatchInlineSnapshot(`\n \"\u001b[36m>\u001b[39m \u001b[36m(f) first\u001b[39m\n (s) second\n (3) third\n (4) fourth\n (5) seventh\n (6) tenth\n\n \u001b[1mAutomations\u001b[22m\n (7) fifth\n (8) sixth\n\n \u001b[1mMerchant Admin\u001b[22m\n (9) eighth\n (10) ninth\n\n \u001b[2mnavigate with arrows, enter to select\u001b[22m\"\n `)\n })\n})\n"]}
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export interface Props {
3
+ table: {
4
+ [header: string]: string[];
5
+ };
6
+ }
7
+ declare const Table: React.FC<Props>;
8
+ export default Table;
@@ -0,0 +1,17 @@
1
+ import { List } from './List.js';
2
+ import { Box, Text } from 'ink';
3
+ import React from 'react';
4
+ import { capitalize } from 'lodash-es';
5
+ const Table = ({ table }) => {
6
+ const headers = Object.keys(table);
7
+ const headerColumnWidth = Math.max(...headers.map((header) => header.length));
8
+ return (React.createElement(Box, { flexDirection: "column", paddingY: 1 }, headers.map((header, index) => (React.createElement(Box, { key: index },
9
+ React.createElement(Box, { width: headerColumnWidth + 1 },
10
+ React.createElement(Text, null,
11
+ capitalize(header),
12
+ ":")),
13
+ React.createElement(Box, { flexGrow: 1 },
14
+ React.createElement(List, { items: table[header] })))))));
15
+ };
16
+ export default Table;
17
+ //# sourceMappingURL=Table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Table.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/Table.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAC,GAAG,EAAE,IAAI,EAAC,MAAM,KAAK,CAAA;AAC7B,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAC,UAAU,EAAC,MAAM,WAAW,CAAA;AAQpC,MAAM,KAAK,GAAoB,CAAC,EAAC,KAAK,EAAC,EAAE,EAAE;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAE7E,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC,IACpC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAC9B,oBAAC,GAAG,IAAC,GAAG,EAAE,KAAK;QACb,oBAAC,GAAG,IAAC,KAAK,EAAE,iBAAiB,GAAG,CAAC;YAC/B,oBAAC,IAAI;gBAAE,UAAU,CAAC,MAAM,CAAC;oBAAS,CAC9B;QACN,oBAAC,GAAG,IAAC,QAAQ,EAAE,CAAC;YACd,oBAAC,IAAI,IAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAE,GAAI,CAC3B,CACF,CACP,CAAC,CACE,CACP,CAAA;AACH,CAAC,CAAA;AAED,eAAe,KAAK,CAAA","sourcesContent":["import {List} from './List.js'\nimport {Box, Text} from 'ink'\nimport React from 'react'\nimport {capitalize} from 'lodash-es'\n\nexport interface Props {\n table: {\n [header: string]: string[]\n }\n}\n\nconst Table: React.FC<Props> = ({table}) => {\n const headers = Object.keys(table)\n const headerColumnWidth = Math.max(...headers.map((header) => header.length))\n\n return (\n <Box flexDirection=\"column\" paddingY={1}>\n {headers.map((header, index) => (\n <Box key={index}>\n <Box width={headerColumnWidth + 1}>\n <Text>{capitalize(header)}:</Text>\n </Box>\n <Box flexGrow={1}>\n <List items={table[header]!} />\n </Box>\n </Box>\n ))}\n </Box>\n )\n}\n\nexport default Table\n"]}
@@ -13,7 +13,7 @@ const delays = {
13
13
  /**
14
14
  * `TextAnimation` applies animations from [chalk-animation](https://github.com/bokub/chalk-animation) to `Text` Children
15
15
  */
16
- const TextAnimation = ({ name = 'rainbow', speed = 1, children, }) => {
16
+ const TextAnimation = ({ name = 'rainbow', speed = 1, children }) => {
17
17
  const [animationTimeout, setAnimationTimeout] = useState(null);
18
18
  const animation = chalkAnimation[name]('').stop();
19
19
  const [frame, setFrame] = useState('');
@@ -1 +1 @@
1
- {"version":3,"file":"TextAnimation.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/TextAnimation.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAA;AACxC,OAAO,cAAc,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAC,IAAI,EAAC,MAAM,KAAK,CAAA;AACxB,OAAO,KAAK,EAAE,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AAShD,MAAM,MAAM,GAAqC;IAC/C,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,GAAG;IACT,OAAO,EAAE,EAAE;CACZ,CAAA;AAED;;GAEG;AACH,MAAM,aAAa,GAAoB,CAAC,EACtC,IAAI,GAAG,SAAS,EAChB,KAAK,GAAG,CAAC,EACT,QAAQ,GACuB,EAAe,EAAE;IAChD,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IACrF,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEtC,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,MAAM,EAAC,MAAM,EAAC,GAAG,YAAY,CAAC,oBAAC,IAAI,QAAE,QAAQ,CAAQ,CAAC,CAAA;QAEtD,yFAAyF;QACzF,uFAAuF;QACvF,0FAA0F;QAC1F,mBAAmB;QAEnB,MAAM,KAAK,GAAG,SAAS;aACpB,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;aACrB,KAAK,EAAE;aACP,OAAO,CAAC,mCAAmC,EAAE,EAAE,CAAC,CAAA,CAAC,uCAAuC;QAE3F,QAAQ,CAAC,KAAK,CAAC,CAAA;QAEf,mBAAmB,CACjB,UAAU,CAAC,GAAG,EAAE;YACd,KAAK,EAAE,CAAA;QACT,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CACzB,CAAA;IACH,CAAC,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,EAAE,CAAA;QAEP,OAAO,GAAG,EAAE;YACV,IAAI,gBAAgB;gBAAE,YAAY,CAAC,gBAAgB,CAAC,CAAA;YAEpD,mBAAmB,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,oBAAC,IAAI,QAAE,KAAK,CAAQ,CAAA;AAC7B,CAAC,CAAA;AAED,OAAO,EAAC,aAAa,EAAC,CAAA","sourcesContent":["import {renderString} from '../../ui.js'\nimport chalkAnimation from 'chalk-animation'\nimport {Text} from 'ink'\nimport React, {useEffect, useState} from 'react'\n\ntype AnimationName = 'rainbow' | 'pulse' | 'glitch' | 'radar' | 'neon' | 'karaoke'\n\ninterface Props {\n name?: AnimationName\n speed?: number\n}\n\nconst delays: {[key in AnimationName]: number} = {\n rainbow: 15,\n pulse: 16,\n glitch: 55,\n radar: 50,\n neon: 500,\n karaoke: 50,\n}\n\n/**\n * `TextAnimation` applies animations from [chalk-animation](https://github.com/bokub/chalk-animation) to `Text` Children\n */\nconst TextAnimation: React.FC<Props> = ({\n name = 'rainbow',\n speed = 1,\n children,\n}: React.PropsWithChildren<Props>): JSX.Element => {\n const [animationTimeout, setAnimationTimeout] = useState<NodeJS.Timeout | null>(null)\n const animation = chalkAnimation[name]('').stop()\n const [frame, setFrame] = useState('')\n\n const start = () => {\n const {output} = renderString(<Text>{children}</Text>)\n\n // There's probably some clashing between `chalk-animation` and Ink's rendering mechanism\n // (which uses `log-update`). The solution is to remove the ANSI escape sequence at the\n // start of the frame that we're getting from `chalk-animation` that tells the terminal to\n // clear the lines.\n\n const frame = animation\n .replace(output ?? '')\n .frame()\n .replace(/^\\u001B\\[(\\d)F\\u001B\\[G\\u001B\\[2K/, '') // eslint-disable-line no-control-regex\n\n setFrame(frame)\n\n setAnimationTimeout(\n setTimeout(() => {\n start()\n }, delays[name] / speed),\n )\n }\n\n useEffect(() => {\n start()\n\n return () => {\n if (animationTimeout) clearTimeout(animationTimeout)\n\n setAnimationTimeout(null)\n }\n }, [])\n\n return <Text>{frame}</Text>\n}\n\nexport {TextAnimation}\n"]}
1
+ {"version":3,"file":"TextAnimation.js","sourceRoot":"","sources":["../../../../../src/private/node/ui/components/TextAnimation.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAA;AACxC,OAAO,cAAc,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAC,IAAI,EAAC,MAAM,KAAK,CAAA;AACxB,OAAO,KAAK,EAAE,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;AAShD,MAAM,MAAM,GAAqC;IAC/C,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,GAAG;IACT,OAAO,EAAE,EAAE;CACZ,CAAA;AAED;;GAEG;AACH,MAAM,aAAa,GAAoB,CAAC,EAAC,IAAI,GAAG,SAAS,EAAE,KAAK,GAAG,CAAC,EAAE,QAAQ,EAAC,EAAe,EAAE;IAC9F,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IACrF,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEtC,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,MAAM,EAAC,MAAM,EAAC,GAAG,YAAY,CAAC,oBAAC,IAAI,QAAE,QAAQ,CAAQ,CAAC,CAAA;QAEtD,yFAAyF;QACzF,uFAAuF;QACvF,0FAA0F;QAC1F,mBAAmB;QAEnB,MAAM,KAAK,GAAG,SAAS;aACpB,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;aACrB,KAAK,EAAE;aACP,OAAO,CAAC,mCAAmC,EAAE,EAAE,CAAC,CAAA,CAAC,uCAAuC;QAE3F,QAAQ,CAAC,KAAK,CAAC,CAAA;QAEf,mBAAmB,CACjB,UAAU,CAAC,GAAG,EAAE;YACd,KAAK,EAAE,CAAA;QACT,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CACzB,CAAA;IACH,CAAC,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,EAAE,CAAA;QAEP,OAAO,GAAG,EAAE;YACV,IAAI,gBAAgB;gBAAE,YAAY,CAAC,gBAAgB,CAAC,CAAA;YAEpD,mBAAmB,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,oBAAC,IAAI,QAAE,KAAK,CAAQ,CAAA;AAC7B,CAAC,CAAA;AAED,OAAO,EAAC,aAAa,EAAC,CAAA","sourcesContent":["import {renderString} from '../../ui.js'\nimport chalkAnimation from 'chalk-animation'\nimport {Text} from 'ink'\nimport React, {useEffect, useState} from 'react'\n\ntype AnimationName = 'rainbow' | 'pulse' | 'glitch' | 'radar' | 'neon' | 'karaoke'\n\ninterface Props {\n name?: AnimationName\n speed?: number\n}\n\nconst delays: {[key in AnimationName]: number} = {\n rainbow: 15,\n pulse: 16,\n glitch: 55,\n radar: 50,\n neon: 500,\n karaoke: 50,\n}\n\n/**\n * `TextAnimation` applies animations from [chalk-animation](https://github.com/bokub/chalk-animation) to `Text` Children\n */\nconst TextAnimation: React.FC<Props> = ({name = 'rainbow', speed = 1, children}): JSX.Element => {\n const [animationTimeout, setAnimationTimeout] = useState<NodeJS.Timeout | null>(null)\n const animation = chalkAnimation[name]('').stop()\n const [frame, setFrame] = useState('')\n\n const start = () => {\n const {output} = renderString(<Text>{children}</Text>)\n\n // There's probably some clashing between `chalk-animation` and Ink's rendering mechanism\n // (which uses `log-update`). The solution is to remove the ANSI escape sequence at the\n // start of the frame that we're getting from `chalk-animation` that tells the terminal to\n // clear the lines.\n\n const frame = animation\n .replace(output ?? '')\n .frame()\n .replace(/^\\u001B\\[(\\d)F\\u001B\\[G\\u001B\\[2K/, '') // eslint-disable-line no-control-regex\n\n setFrame(frame)\n\n setAnimationTimeout(\n setTimeout(() => {\n start()\n }, delays[name] / speed),\n )\n }\n\n useEffect(() => {\n start()\n\n return () => {\n if (animationTimeout) clearTimeout(animationTimeout)\n\n setAnimationTimeout(null)\n }\n }, [])\n\n return <Text>{frame}</Text>\n}\n\nexport {TextAnimation}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { Logger, LogLevel } from '../../output.js';
2
+ import { Props as PromptProps } from '../../private/node/ui/components/Prompt.js';
2
3
  import { ReactElement } from 'react';
3
4
  import { RenderOptions } from 'ink';
4
5
  export declare function renderOnce(element: JSX.Element, logLevel?: LogLevel, logger?: Logger): void;
@@ -8,4 +9,5 @@ interface Instance {
8
9
  unmount: () => void;
9
10
  }
10
11
  export declare const renderString: (element: ReactElement) => Instance;
12
+ export declare function prompt<T>(options: Omit<PromptProps<T>, 'onChoose'>): Promise<T>;
11
13
  export {};
@@ -1,5 +1,7 @@
1
1
  import { isUnitTest } from '../../environment/local.js';
2
2
  import { collectLog, consoleLog, outputWhereAppropriate } from '../../output.js';
3
+ import Prompt from '../../private/node/ui/components/Prompt.js';
4
+ import React from 'react';
3
5
  import { render as inkRender } from 'ink';
4
6
  import { EventEmitter } from 'events';
5
7
  export function renderOnce(element, logLevel = 'info', logger = consoleLog) {
@@ -45,4 +47,16 @@ export const renderString = (element) => {
45
47
  unmount: instance.unmount,
46
48
  };
47
49
  };
50
+ export async function prompt(options) {
51
+ let onChooseResolve = () => { };
52
+ const onChoosePromise = new Promise((resolve) => {
53
+ onChooseResolve = resolve;
54
+ });
55
+ const props = {
56
+ ...options,
57
+ onChoose: onChooseResolve,
58
+ };
59
+ await render(React.createElement(Prompt, { ...props }), { exitOnCtrlC: false });
60
+ return onChoosePromise;
61
+ }
48
62
  //# sourceMappingURL=ui.js.map