redmine-context 1.0.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.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela de exportação do bundle (#33) — aberta a partir do detalhe da issue
|
|
4
|
+
* (`e`, ver `./issue-detail.tsx`), SOMENTE quando a issue já terminou de
|
|
5
|
+
* carregar (`state.status === 'loaded'` naquela tela).
|
|
6
|
+
*
|
|
7
|
+
* Reusa a issue já normalizada em memória via `./loaded-issue-context.js`
|
|
8
|
+
* (ver o JSDoc daquele módulo para a escolha de reuso vs. refetch) e chama
|
|
9
|
+
* `../hooks/use-export-bundle.js`, que empacota com `buildMarkdownBundle`/
|
|
10
|
+
* `buildJsonBundle` (fronteira do core, `../../../index.js`) e grava
|
|
11
|
+
* `<id>.md`/`<id>.json` no destino informado.
|
|
12
|
+
*
|
|
13
|
+
* Dois campos alternados por `Tab` (mesmo padrão de
|
|
14
|
+
* `./onboarding/login.tsx`): formato — lista navegável (↑/↓,
|
|
15
|
+
* `../hooks/use-list-navigation.js`) entre `md`/`json`/`both` — e destino —
|
|
16
|
+
* `../components/text-input.js`, com `process.cwd()` como valor inicial
|
|
17
|
+
* (`~` é expandido pelo hook antes de gravar). `Enter` em qualquer um dos
|
|
18
|
+
* dois campos confirma a exportação.
|
|
19
|
+
*
|
|
20
|
+
* #34 (M2-11): PRIMEIRO produtor real do registro de jobs da sessão
|
|
21
|
+
* (`../job-registry.js`, ver o JSDoc daquele módulo para o contrato) — ao
|
|
22
|
+
* confirmar (Enter), registra um job em `processing` (exportação não tem fase
|
|
23
|
+
* de fila: começa a gravar assim que confirmada) e o transiciona para
|
|
24
|
+
* `done`/`failed` conforme `useExportBundle` resolve. `useExportBundle` em si
|
|
25
|
+
* permanece intocado (fronteira do core, ADR-005) — a integração vive só
|
|
26
|
+
* aqui, na tela, mantendo o hook puro. Exportação NÃO é cancelável
|
|
27
|
+
* (`cancelable` fica `false`/omitido) — o painel (`../screens/jobs.js`)
|
|
28
|
+
* mostra "(não cancelável)" para ela.
|
|
29
|
+
*
|
|
30
|
+
* 4 estados visuais: formulário (`idle`), progresso (`Spinner` existente,
|
|
31
|
+
* igual a `./issue-detail.tsx`), sucesso (caminho(s) absoluto(s) gravado(s) +
|
|
32
|
+
* `b` para voltar) e erro (`theme.danger` + ação corretiva — diretório
|
|
33
|
+
* inexistente ou sem permissão de escrita, ver o hook). `b` (ou `Esc`, atalho
|
|
34
|
+
* global de `../app.tsx`) volta ao detalhe em qualquer estado que não seja
|
|
35
|
+
* `exporting` — a pilha já preserva a navegação (a issue é buscada de novo
|
|
36
|
+
* pelo detalhe ao remontar, mesmo comportamento já existente ao voltar da
|
|
37
|
+
* home para o detalhe).
|
|
38
|
+
*
|
|
39
|
+
* M2-16 (#39): o campo de destino usa `maxWidth` do `TextInput`
|
|
40
|
+
* (`../components/text-input.js`) — o valor exibido é truncado a partir do
|
|
41
|
+
* INÍCIO (preserva o fim, onde o usuário está digitando) quando o caminho
|
|
42
|
+
* excede o espaço restante do terminal, o mesmo `Box` em linha que sofreria
|
|
43
|
+
* sobreposição sem isso (ver o JSDoc de `../truncate.ts`). Os caminhos
|
|
44
|
+
* gravados exibidos em `success`/`error` truncam do FIM (`../truncate.js`,
|
|
45
|
+
* `truncate`) — são texto estático, não um campo em edição.
|
|
46
|
+
*/
|
|
47
|
+
import { Box, Text, useInput } from 'ink';
|
|
48
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
49
|
+
import { Spinner } from '../components/spinner.js';
|
|
50
|
+
import { TextInput } from '../components/text-input.js';
|
|
51
|
+
import { glyphs } from '../glyphs.js';
|
|
52
|
+
import { useExportBundle } from '../hooks/use-export-bundle.js';
|
|
53
|
+
import { useListNavigation } from '../hooks/use-list-navigation.js';
|
|
54
|
+
import { useJobRegistry } from '../job-registry.js';
|
|
55
|
+
import { useTerminalWidth } from '../hooks/use-terminal-width.js';
|
|
56
|
+
import { useNavigation } from '../navigation.js';
|
|
57
|
+
import { symbols } from '../symbols.js';
|
|
58
|
+
import { useTheme } from '../theme.js';
|
|
59
|
+
import { truncate } from '../truncate.js';
|
|
60
|
+
import { useLoadedIssue } from './loaded-issue-context.js';
|
|
61
|
+
/** `paddingX={1}` dos dois lados do `Box` raiz da tela. */
|
|
62
|
+
const SCREEN_PADDING_X = 2;
|
|
63
|
+
/** Recuo (`{' '}`) na frente de cada caminho gravado, na tela de sucesso. */
|
|
64
|
+
const SUCCESS_PATH_INDENT_WIDTH = 2;
|
|
65
|
+
/** Largura mínima garantida a um caminho exibido, mesmo em terminais muito estreitos. */
|
|
66
|
+
const MIN_PATH_WIDTH = 8;
|
|
67
|
+
/** Alterna entre os dois campos do formulário. */
|
|
68
|
+
function otherField(field) {
|
|
69
|
+
return field === 'format' ? 'destination' : 'format';
|
|
70
|
+
}
|
|
71
|
+
/** As 3 opções de formato exigidas pela AC: MD, JSON, ou ambos. */
|
|
72
|
+
const FORMAT_OPTIONS = [
|
|
73
|
+
{ value: 'md', label: 'Markdown (.md)' },
|
|
74
|
+
{ value: 'json', label: 'JSON (.json)' },
|
|
75
|
+
{ value: 'both', label: 'Ambos (.md + .json)' },
|
|
76
|
+
];
|
|
77
|
+
/** Uma linha da lista de formato: ponteiro quando é a linha selecionada, negrito quando o campo está focado. */
|
|
78
|
+
function FormatRow({ label, selected, focused }) {
|
|
79
|
+
const theme = useTheme();
|
|
80
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), _jsx(Text, { color: selected && focused ? theme.primary : theme.muted, children: label })] }));
|
|
81
|
+
}
|
|
82
|
+
/** Rótulo fixo à esquerda do campo de destino: ponteiro/espaço (1) + `" Destino: "` (10). */
|
|
83
|
+
const DESTINATION_LABEL_WIDTH = 11;
|
|
84
|
+
/** Largura mínima garantida ao campo de destino/caminhos exibidos, mesmo em terminais muito estreitos. */
|
|
85
|
+
const MIN_DESTINATION_WIDTH = 8;
|
|
86
|
+
/** Tela de exportação: escolhe formato + destino, grava via o core, mostra progresso/sucesso/erro. */
|
|
87
|
+
export function ExportScreen() {
|
|
88
|
+
const theme = useTheme();
|
|
89
|
+
const { pop } = useNavigation();
|
|
90
|
+
const { issue } = useLoadedIssue();
|
|
91
|
+
const { state, runExport, reset } = useExportBundle(issue);
|
|
92
|
+
// #34 (M2-11): registro de jobs da sessão — exportação é o PRIMEIRO
|
|
93
|
+
// produtor real (ver o JSDoc do módulo para o contrato completo).
|
|
94
|
+
const { registerJob, updateJobStatus } = useJobRegistry();
|
|
95
|
+
const terminalWidth = useTerminalWidth();
|
|
96
|
+
const destinationMaxWidth = Math.max(terminalWidth - SCREEN_PADDING_X - DESTINATION_LABEL_WIDTH, MIN_DESTINATION_WIDTH);
|
|
97
|
+
const pathMaxWidth = Math.max(terminalWidth - SCREEN_PADDING_X - SUCCESS_PATH_INDENT_WIDTH, MIN_PATH_WIDTH);
|
|
98
|
+
const [field, setField] = useState('format');
|
|
99
|
+
const [destination, setDestination] = useState(() => process.cwd());
|
|
100
|
+
const isFormActive = state.status === 'idle';
|
|
101
|
+
// Id do job da tentativa de exportação EM ANDAMENTO (ou da última, após
|
|
102
|
+
// terminar) — `undefined` antes da primeira confirmação. Um `ref` (não
|
|
103
|
+
// estado) porque só é lido pelo efeito abaixo, nunca pelo JSX.
|
|
104
|
+
const currentJobIdRef = useRef(undefined);
|
|
105
|
+
const handleSubmit = useCallback((formatIndex) => {
|
|
106
|
+
const format = FORMAT_OPTIONS[formatIndex]?.value ?? 'md';
|
|
107
|
+
if (issue !== undefined) {
|
|
108
|
+
// Novo `id` a cada confirmação (mesmo após um erro + "r" de retry) —
|
|
109
|
+
// cada tentativa vira uma entrada distinta no painel (histórico da
|
|
110
|
+
// sessão), nunca uma reescrita silenciosa da anterior.
|
|
111
|
+
const jobId = `export-${issue.id}-${Date.now()}`;
|
|
112
|
+
currentJobIdRef.current = jobId;
|
|
113
|
+
registerJob({
|
|
114
|
+
id: jobId,
|
|
115
|
+
label: `Exportar #${issue.id} (${format})`,
|
|
116
|
+
status: 'processing',
|
|
117
|
+
startedAt: Date.now(),
|
|
118
|
+
// Exportação não é cancelável — grava direto no disco, sem uma
|
|
119
|
+
// etapa segura para abortar no meio (ver o JSDoc do módulo).
|
|
120
|
+
cancelable: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
void runExport(format, destination);
|
|
124
|
+
}, [runExport, destination, issue, registerJob]);
|
|
125
|
+
// Espelha o resultado da exportação no job registrado acima — SEM tocar em
|
|
126
|
+
// `useExportBundle` (fronteira do core intocada, ver o JSDoc do módulo).
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const jobId = currentJobIdRef.current;
|
|
129
|
+
if (jobId === undefined) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (state.status === 'success') {
|
|
133
|
+
updateJobStatus(jobId, 'done');
|
|
134
|
+
}
|
|
135
|
+
else if (state.status === 'error') {
|
|
136
|
+
updateJobStatus(jobId, 'failed');
|
|
137
|
+
}
|
|
138
|
+
}, [state.status, updateJobStatus]);
|
|
139
|
+
const { selectedIndex: formatIndex } = useListNavigation(FORMAT_OPTIONS.length, {
|
|
140
|
+
isActive: isFormActive && field === 'format',
|
|
141
|
+
onSelect: handleSubmit,
|
|
142
|
+
});
|
|
143
|
+
// Handlers ESTÁVEIS (refs + useCallback, padrão do repo): identidade nova a
|
|
144
|
+
// cada render des/re-subscreve o useInput e pode perder uma tecla rápida.
|
|
145
|
+
const stateStatusRef = useRef(state.status);
|
|
146
|
+
stateStatusRef.current = state.status;
|
|
147
|
+
const popRef = useRef(pop);
|
|
148
|
+
popRef.current = pop;
|
|
149
|
+
const resetRef = useRef(reset);
|
|
150
|
+
resetRef.current = reset;
|
|
151
|
+
const handleControlInput = useCallback((input, key) => {
|
|
152
|
+
if (key.tab && stateStatusRef.current === 'idle') {
|
|
153
|
+
setField(otherField);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// "b" não interrompe uma exportação em andamento — evita voltar no meio
|
|
157
|
+
// de uma gravação em disco.
|
|
158
|
+
if (input === 'b' && stateStatusRef.current !== 'exporting') {
|
|
159
|
+
popRef.current();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (input === 'r' && stateStatusRef.current === 'error') {
|
|
163
|
+
resetRef.current();
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
useInput(handleControlInput);
|
|
167
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.primary, children: "Exportar bundle" }), issue === undefined ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Nenhuma issue carregada \u2014 volte ao detalhe e tente de novo." }) })) : null, issue !== undefined && state.status === 'idle' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: ["Issue #", issue.id] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: field === 'format' ? theme.primary : theme.muted, children: "Formato:" }), FORMAT_OPTIONS.map((option, index) => (_jsx(FormatRow, { label: option.label, selected: index === formatIndex, focused: field === 'format' }, option.value)))] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: field === 'destination' ? theme.primary : theme.muted, children: [field === 'destination' ? symbols.pointer : ' ', " Destino:", ' '] }), _jsx(TextInput, { value: destination, onChange: setDestination, onSubmit: () => handleSubmit(formatIndex), isActive: field === 'destination', maxWidth: destinationMaxWidth })] })] })) : null, state.status === 'exporting' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Exportando..."] }) })) : null, state.status === 'success' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.success, children: [symbols.tick, " Bundle gravado:"] }), state.files.map((file) => (_jsxs(Text, { color: theme.muted, children: [' ', truncate(file.path, pathMaxWidth)] }, file.path)))] })) : null, state.status === 'error' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " Falha ao gravar o bundle: ", state.message] }), _jsx(Text, { color: theme.muted, children: "Verifique se o diret\u00F3rio existe e se voc\u00EA tem permiss\u00E3o de escrita nele, ou informe outro destino." })] })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [state.status === 'idle' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "escolhe o formato,", ' ', _jsx(Text, { color: theme.accent, children: "Tab" }), ' ', "alterna formato/destino,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "exporta,", ' '] })) : null, state.status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: "r" }), ' ', "tenta de novo,", ' '] })) : null, _jsx(Text, { color: theme.accent, children: "b" }), ' ', "volta ao detalhe."] }) })] }));
|
|
168
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estado de seleção da home (#31), compartilhado entre `./home.tsx` e
|
|
3
|
+
* `./issue-detail.tsx` sem prop-drilling.
|
|
4
|
+
*
|
|
5
|
+
* O roteador (`../screen.ts`/`../app.tsx`) renderiza cada tela SEM props e só
|
|
6
|
+
* mantém montado o TOPO da pilha — ao empilhar `issue-detail` (Enter sobre
|
|
7
|
+
* uma issue na home), `HomeScreen` é desmontada. Isso cria dois problemas que
|
|
8
|
+
* este módulo resolve com um único contexto:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Qual issue abrir**: `issue-detail` precisa saber o `id` da issue
|
|
11
|
+
* selecionada para buscá-la via `../hooks/use-issue-detail.js` — sem
|
|
12
|
+
* props entre telas, o único jeito de repassar essa informação é um
|
|
13
|
+
* estado compartilhado escrito pela home (`setSelectedIssueId`) e lido
|
|
14
|
+
* pelo detalhe (`selectedIssueId`).
|
|
15
|
+
* 2. **Posição da lista ao voltar**: `selectedIndex` de
|
|
16
|
+
* `../hooks/use-list-navigation.js` vive num `useState` LOCAL ao
|
|
17
|
+
* componente — ele reseta para `0` a cada remount. Sem preservá-lo aqui,
|
|
18
|
+
* Esc/`b` a partir do detalhe devolveria o usuário ao topo da lista,
|
|
19
|
+
* mesmo que a issue aberta fosse a 10ª.
|
|
20
|
+
*
|
|
21
|
+
* Instanciado uma vez em `../app.tsx`, envolvendo toda a árvore de telas —
|
|
22
|
+
* mesmo padrão do `OnboardingProvider` (`./onboarding/onboarding-context.tsx`),
|
|
23
|
+
* que resolve o mesmo tipo de problema (estado compartilhado entre telas sem
|
|
24
|
+
* props) para o fluxo de onboarding.
|
|
25
|
+
*
|
|
26
|
+
* ## Alternativas descartadas
|
|
27
|
+
*
|
|
28
|
+
* - **Adicionar parâmetros às telas** (`SCREENS` passaria a aceitar props):
|
|
29
|
+
* afetaria as 12 telas do registro (`../screen.ts`) e o contrato do
|
|
30
|
+
* roteador inteiro por uma necessidade de só duas telas — desproporcional.
|
|
31
|
+
* - **Singleton de módulo** (uma variável fora de qualquer componente React,
|
|
32
|
+
* sem `Context`): funciona em produção, mas vaza estado entre execuções de
|
|
33
|
+
* teste (`render()` de um teste não reresetaria o singleton do teste
|
|
34
|
+
* seguinte) — um `Context` reinstanciado a cada `render()` evita esse
|
|
35
|
+
* acoplamento oculto.
|
|
36
|
+
* - **Guardar no próprio `NavigationValue`** (`../navigation.tsx`): esse
|
|
37
|
+
* contrato é sobre QUAIS telas existem na pilha, não sobre o estado interno
|
|
38
|
+
* de uma tela específica — misturar os dois tornaria `navigation.tsx`
|
|
39
|
+
* acoplado a detalhes da home/detalhe de issue.
|
|
40
|
+
*/
|
|
41
|
+
import { type ReactNode } from 'react';
|
|
42
|
+
/** Valor exposto pelo contexto de seleção da home. */
|
|
43
|
+
export interface HomeSelectionValue {
|
|
44
|
+
/**
|
|
45
|
+
* Índice da linha destacada na lista da home — sobrevive ao push/pop de
|
|
46
|
+
* `issue-detail` (ver o JSDoc do módulo). Semente lida por `initialIndex`
|
|
47
|
+
* de `../hooks/use-list-navigation.js`.
|
|
48
|
+
*/
|
|
49
|
+
selectedIndex: number;
|
|
50
|
+
/** Atualiza o índice destacado — chamado pela home a cada mudança do cursor. */
|
|
51
|
+
setSelectedIndex: (index: number) => void;
|
|
52
|
+
/** Id da issue aberta em `issue-detail`, ou `undefined` antes de qualquer seleção. */
|
|
53
|
+
selectedIssueId: number | undefined;
|
|
54
|
+
/** Registra a issue selecionada — chamado pela home ao empilhar `issue-detail` (Enter). */
|
|
55
|
+
setSelectedIssueId: (id: number) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Provider do contexto de seleção da home — instanciado uma vez em
|
|
59
|
+
* `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
|
|
60
|
+
*/
|
|
61
|
+
export declare function HomeSelectionProvider({ children }: {
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}): import("react").JSX.Element;
|
|
64
|
+
/**
|
|
65
|
+
* Hook de seleção da home consumido por `./home.tsx`/`./issue-detail.tsx`.
|
|
66
|
+
*
|
|
67
|
+
* @throws {Error} Se chamado fora de um `<HomeSelectionProvider>` — indica
|
|
68
|
+
* erro de composição (tela renderizada sem passar pelo roteador).
|
|
69
|
+
*/
|
|
70
|
+
export declare function useHomeSelection(): HomeSelectionValue;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Estado de seleção da home (#31), compartilhado entre `./home.tsx` e
|
|
4
|
+
* `./issue-detail.tsx` sem prop-drilling.
|
|
5
|
+
*
|
|
6
|
+
* O roteador (`../screen.ts`/`../app.tsx`) renderiza cada tela SEM props e só
|
|
7
|
+
* mantém montado o TOPO da pilha — ao empilhar `issue-detail` (Enter sobre
|
|
8
|
+
* uma issue na home), `HomeScreen` é desmontada. Isso cria dois problemas que
|
|
9
|
+
* este módulo resolve com um único contexto:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Qual issue abrir**: `issue-detail` precisa saber o `id` da issue
|
|
12
|
+
* selecionada para buscá-la via `../hooks/use-issue-detail.js` — sem
|
|
13
|
+
* props entre telas, o único jeito de repassar essa informação é um
|
|
14
|
+
* estado compartilhado escrito pela home (`setSelectedIssueId`) e lido
|
|
15
|
+
* pelo detalhe (`selectedIssueId`).
|
|
16
|
+
* 2. **Posição da lista ao voltar**: `selectedIndex` de
|
|
17
|
+
* `../hooks/use-list-navigation.js` vive num `useState` LOCAL ao
|
|
18
|
+
* componente — ele reseta para `0` a cada remount. Sem preservá-lo aqui,
|
|
19
|
+
* Esc/`b` a partir do detalhe devolveria o usuário ao topo da lista,
|
|
20
|
+
* mesmo que a issue aberta fosse a 10ª.
|
|
21
|
+
*
|
|
22
|
+
* Instanciado uma vez em `../app.tsx`, envolvendo toda a árvore de telas —
|
|
23
|
+
* mesmo padrão do `OnboardingProvider` (`./onboarding/onboarding-context.tsx`),
|
|
24
|
+
* que resolve o mesmo tipo de problema (estado compartilhado entre telas sem
|
|
25
|
+
* props) para o fluxo de onboarding.
|
|
26
|
+
*
|
|
27
|
+
* ## Alternativas descartadas
|
|
28
|
+
*
|
|
29
|
+
* - **Adicionar parâmetros às telas** (`SCREENS` passaria a aceitar props):
|
|
30
|
+
* afetaria as 12 telas do registro (`../screen.ts`) e o contrato do
|
|
31
|
+
* roteador inteiro por uma necessidade de só duas telas — desproporcional.
|
|
32
|
+
* - **Singleton de módulo** (uma variável fora de qualquer componente React,
|
|
33
|
+
* sem `Context`): funciona em produção, mas vaza estado entre execuções de
|
|
34
|
+
* teste (`render()` de um teste não reresetaria o singleton do teste
|
|
35
|
+
* seguinte) — um `Context` reinstanciado a cada `render()` evita esse
|
|
36
|
+
* acoplamento oculto.
|
|
37
|
+
* - **Guardar no próprio `NavigationValue`** (`../navigation.tsx`): esse
|
|
38
|
+
* contrato é sobre QUAIS telas existem na pilha, não sobre o estado interno
|
|
39
|
+
* de uma tela específica — misturar os dois tornaria `navigation.tsx`
|
|
40
|
+
* acoplado a detalhes da home/detalhe de issue.
|
|
41
|
+
*/
|
|
42
|
+
import { createContext, useCallback, useContext, useState } from 'react';
|
|
43
|
+
const HomeSelectionContext = createContext(undefined);
|
|
44
|
+
/**
|
|
45
|
+
* Provider do contexto de seleção da home — instanciado uma vez em
|
|
46
|
+
* `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
|
|
47
|
+
*/
|
|
48
|
+
export function HomeSelectionProvider({ children }) {
|
|
49
|
+
const [selectedIndex, setSelectedIndexState] = useState(0);
|
|
50
|
+
const [selectedIssueId, setSelectedIssueIdState] = useState(undefined);
|
|
51
|
+
// Identidades ESTÁVEIS (useCallback sem deps, padrão do repo) — evita
|
|
52
|
+
// resubscrições desnecessárias em quem os usa como dependência de efeito
|
|
53
|
+
// (ex.: `../screens/home.tsx`).
|
|
54
|
+
const setSelectedIndex = useCallback((index) => {
|
|
55
|
+
setSelectedIndexState(index);
|
|
56
|
+
}, []);
|
|
57
|
+
const setSelectedIssueId = useCallback((id) => {
|
|
58
|
+
setSelectedIssueIdState(id);
|
|
59
|
+
}, []);
|
|
60
|
+
const value = {
|
|
61
|
+
selectedIndex,
|
|
62
|
+
setSelectedIndex,
|
|
63
|
+
selectedIssueId,
|
|
64
|
+
setSelectedIssueId,
|
|
65
|
+
};
|
|
66
|
+
return _jsx(HomeSelectionContext.Provider, { value: value, children: children });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Hook de seleção da home consumido por `./home.tsx`/`./issue-detail.tsx`.
|
|
70
|
+
*
|
|
71
|
+
* @throws {Error} Se chamado fora de um `<HomeSelectionProvider>` — indica
|
|
72
|
+
* erro de composição (tela renderizada sem passar pelo roteador).
|
|
73
|
+
*/
|
|
74
|
+
export function useHomeSelection() {
|
|
75
|
+
const value = useContext(HomeSelectionContext);
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
throw new Error('useHomeSelection() usado fora de <HomeSelectionProvider>.');
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela home da TUI (M2-06, #29): "minhas issues" (`assigned_to_id=me`) via o
|
|
4
|
+
* core, com seleção por teclado (`../hooks/use-list-navigation.js`) e os 4
|
|
5
|
+
* estados visuais exigidos pela AC — carregando (`Spinner`), vazio, erro de
|
|
6
|
+
* rede (banner `theme.danger` + tecla `r` para tentar de novo) e 403
|
|
7
|
+
* (mensagem específica de permissão). Vira o destino pós-onboarding (ver
|
|
8
|
+
* `onboarding/success.tsx`, que faz `resetTo('home')`) e o Enter da
|
|
9
|
+
* `welcome.tsx` quando já há credencial salva na cascata.
|
|
10
|
+
*
|
|
11
|
+
* A busca em si vive em `../hooks/use-my-issues.js` (deps injetáveis,
|
|
12
|
+
* fronteira do core via `../../../index.js`) — esta tela só formata os
|
|
13
|
+
* estados. `Enter` sobre a issue selecionada empilha `issue-detail`
|
|
14
|
+
* (`./issue-detail.js`, detalhe real desde a #31).
|
|
15
|
+
*
|
|
16
|
+
* Fix do review do PR #120: `useMyIssues` agora envolve a busca com
|
|
17
|
+
* `useAuthGuard` (#36) — 401 relogina e retoma a busca sozinho (o estado
|
|
18
|
+
* fica `loading` até lá, nenhuma tela nova aqui). Se o re-login for
|
|
19
|
+
* abandonado (Esc), o hook expõe o 5º estado `auth-aborted` (neutro, não um
|
|
20
|
+
* erro) — renderizado aqui igual aos banners de erro, com o mesmo `r` de
|
|
21
|
+
* retry.
|
|
22
|
+
*
|
|
23
|
+
* M2-07 (#30) acrescenta a busca/filtros inline: `/` abre um `TextInput`
|
|
24
|
+
* QUANDO a home está ativa; `f` cicla o filtro com a busca FECHADA (badge no
|
|
25
|
+
* cabeçalho); Esc fecha a busca sem refetch, interceptado via
|
|
26
|
+
* `../hooks/use-escape-interceptor.ts` para não desempilhar a home.
|
|
27
|
+
*
|
|
28
|
+
* #31 (detalhe) acrescenta `./home-selection.js`: a tela é DESMONTADA ao
|
|
29
|
+
* empilhar `issue-detail`, então seleção (`selectedIndex` + issue escolhida)
|
|
30
|
+
* vive num contexto acima da pilha (padrão do `OnboardingProvider`) — ver o
|
|
31
|
+
* JSDoc de `./home-selection.js`.
|
|
32
|
+
*
|
|
33
|
+
* M2-16 (#39): o subject de cada linha é truncado (`../truncate.js`) com um
|
|
34
|
+
* orçamento de largura CALCULADO por linha (`../hooks/use-terminal-width.js`
|
|
35
|
+
* menos o espaço fixo de ponteiro/`#id`/badge de status, ver
|
|
36
|
+
* `fixedRowOverhead` abaixo) — sem isso, um subject comprido sobrepõe o badge
|
|
37
|
+
* de status da mesma linha em terminais estreitos (o Ink não reflui `<Text>`
|
|
38
|
+
* IRMÃS dentro de um `<Box>` em linha; ver o JSDoc de `../truncate.ts`).
|
|
39
|
+
*/
|
|
40
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
41
|
+
import { Box, Text, useInput } from 'ink';
|
|
42
|
+
import { Spinner } from '../components/spinner.js';
|
|
43
|
+
import { TextInput } from '../components/text-input.js';
|
|
44
|
+
import { glyphs } from '../glyphs.js';
|
|
45
|
+
import { useEscapeInterceptor } from '../hooks/use-escape-interceptor.js';
|
|
46
|
+
import { useIssueSearch } from '../hooks/use-issue-search.js';
|
|
47
|
+
import { useListNavigation } from '../hooks/use-list-navigation.js';
|
|
48
|
+
import { useMyIssues } from '../hooks/use-my-issues.js';
|
|
49
|
+
import { useTerminalWidth } from '../hooks/use-terminal-width.js';
|
|
50
|
+
import { useNavigation } from '../navigation.js';
|
|
51
|
+
import { statusColor } from '../status-color.js';
|
|
52
|
+
import { symbols } from '../symbols.js';
|
|
53
|
+
import { useTheme } from '../theme.js';
|
|
54
|
+
import { truncate } from '../truncate.js';
|
|
55
|
+
import { useHomeSelection } from './home-selection.js';
|
|
56
|
+
/** Largura mínima garantida ao subject, mesmo em terminais muito estreitos. */
|
|
57
|
+
const MIN_SUBJECT_WIDTH = 8;
|
|
58
|
+
/** `paddingX={1}` dos dois lados do `Box` raiz da tela (ver o JSX abaixo). */
|
|
59
|
+
const SCREEN_PADDING_X = 2;
|
|
60
|
+
// Nit do review #120: referência ESTÁVEL para os estados sem lista — um
|
|
61
|
+
// literal `[]` inline em cada render seria recriado a cada chamada,
|
|
62
|
+
// invalidando memoizações a jusante (`useListNavigation`) sem necessidade.
|
|
63
|
+
const EMPTY_ISSUES = [];
|
|
64
|
+
/** Rótulo do badge de cada filtro rápido de status (tecla `f` cicla, M2-07/#30). */
|
|
65
|
+
const STATUS_FILTER_LABELS = {
|
|
66
|
+
open: 'aberta',
|
|
67
|
+
closed: 'fechada',
|
|
68
|
+
all: 'todas',
|
|
69
|
+
};
|
|
70
|
+
/** Próximo filtro no ciclo `aberta → fechada → todas → aberta` (tecla `f`). */
|
|
71
|
+
function nextStatusFilter(current) {
|
|
72
|
+
if (current === 'open')
|
|
73
|
+
return 'closed';
|
|
74
|
+
if (current === 'closed')
|
|
75
|
+
return 'all';
|
|
76
|
+
return 'open';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Espaço fixo ocupado pela linha FORA do subject (M2-16, #39): ponteiro (2),
|
|
80
|
+
* `#id ` (id + `#`/espaço), 1 espaço final após o subject, `[status]`
|
|
81
|
+
* (colchetes + nome) e o `paddingX` da tela. O que sobrar da largura do
|
|
82
|
+
* terminal é o orçamento do subject.
|
|
83
|
+
*/
|
|
84
|
+
function fixedRowOverhead(idLength, statusLength) {
|
|
85
|
+
const POINTER_WIDTH = 2;
|
|
86
|
+
const ID_PREFIX_WIDTH = 1 + idLength + 1; // "#" + dígitos + espaço
|
|
87
|
+
const SUBJECT_TRAILING_SPACE = 1;
|
|
88
|
+
const STATUS_BRACKETS_WIDTH = 2 + statusLength; // "[" + nome + "]"
|
|
89
|
+
return POINTER_WIDTH + ID_PREFIX_WIDTH + SUBJECT_TRAILING_SPACE + STATUS_BRACKETS_WIDTH + SCREEN_PADDING_X;
|
|
90
|
+
}
|
|
91
|
+
/** Uma linha da lista: `#id` em `theme.muted`, subject truncado ao orçamento de largura, badge de status. */
|
|
92
|
+
function IssueRow({ issue, selected }) {
|
|
93
|
+
const theme = useTheme();
|
|
94
|
+
const terminalWidth = useTerminalWidth();
|
|
95
|
+
const overhead = fixedRowOverhead(String(issue.id).length, issue.statusName.length);
|
|
96
|
+
const subjectBudget = Math.max(terminalWidth - overhead, MIN_SUBJECT_WIDTH);
|
|
97
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), _jsxs(Text, { color: theme.muted, children: ["#", issue.id, " "] }), _jsxs(Text, { ...(selected ? { color: theme.primary } : {}), children: [truncate(issue.subject, subjectBudget), ' '] }), _jsxs(Text, { color: statusColor(theme, issue.statusName), children: ["[", issue.statusName, "]"] })] }));
|
|
98
|
+
}
|
|
99
|
+
/** Tela home: lista "minhas issues", `↑/↓`/`j`/`k` navegam, `Enter` abre o detalhe. */
|
|
100
|
+
export function HomeScreen() {
|
|
101
|
+
const theme = useTheme();
|
|
102
|
+
const { push } = useNavigation();
|
|
103
|
+
const { state, retry } = useMyIssues();
|
|
104
|
+
// #31: índice preservado entre remounts + registro de qual issue foi aberta
|
|
105
|
+
// (ver o JSDoc do módulo e de `./home-selection.js`).
|
|
106
|
+
const { selectedIndex: persistedIndex, setSelectedIndex: persistIndex, setSelectedIssueId } = useHomeSelection();
|
|
107
|
+
// --- Busca/filtros inline (M2-07, #30) ---
|
|
108
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
109
|
+
const [query, setQuery] = useState('');
|
|
110
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
111
|
+
const search = useIssueSearch(query, statusFilter);
|
|
112
|
+
// Handler ESTÁVEL: `search.clear` é a única dependência mutável (mas já é
|
|
113
|
+
// estável por construção, ver `use-issue-search.ts`) — fecha a busca e
|
|
114
|
+
// restaura os 3 estados neutros de uma vez (query/filtro/hook de busca).
|
|
115
|
+
const closeSearch = useCallback(() => {
|
|
116
|
+
setIsSearching(false);
|
|
117
|
+
setQuery('');
|
|
118
|
+
setStatusFilter('all');
|
|
119
|
+
search.clear();
|
|
120
|
+
}, [search.clear]);
|
|
121
|
+
// Desvia o Esc GLOBAL (`../app.tsx`) enquanto a busca está aberta — sem
|
|
122
|
+
// isso, Esc desempilharia a home inteira em vez de só fechar a busca.
|
|
123
|
+
useEscapeInterceptor(isSearching, closeSearch);
|
|
124
|
+
const issues = state.status === 'loaded' ? state.issues : EMPTY_ISSUES;
|
|
125
|
+
// Handlers ESTÁVEIS (useCallback + refs, padrão do repo): identidade nova a
|
|
126
|
+
// cada render des/re-subscreve o useInput e pode perder uma tecla rápida.
|
|
127
|
+
const pushRef = useRef(push);
|
|
128
|
+
pushRef.current = push;
|
|
129
|
+
const setSelectedIssueIdRef = useRef(setSelectedIssueId);
|
|
130
|
+
setSelectedIssueIdRef.current = setSelectedIssueId;
|
|
131
|
+
const handleSelect = useCallback((index) => {
|
|
132
|
+
const issue = issues[index];
|
|
133
|
+
if (issue !== undefined) {
|
|
134
|
+
setSelectedIssueIdRef.current(issue.id);
|
|
135
|
+
pushRef.current('issue-detail');
|
|
136
|
+
}
|
|
137
|
+
}, [issues]);
|
|
138
|
+
// #30: navegação desligada com a busca aberta; #31: cursor semeado com a
|
|
139
|
+
// última posição persistida (sobrevive ao unmount via home-selection).
|
|
140
|
+
const { selectedIndex } = useListNavigation(issues.length, {
|
|
141
|
+
onSelect: handleSelect,
|
|
142
|
+
isActive: !isSearching,
|
|
143
|
+
initialIndex: persistedIndex,
|
|
144
|
+
});
|
|
145
|
+
// Espelha `selectedIndex` ao contexto — a cópia externa sobrevive ao
|
|
146
|
+
// unmount desta tela (ver ./home-selection.js).
|
|
147
|
+
const persistIndexRef = useRef(persistIndex);
|
|
148
|
+
persistIndexRef.current = persistIndex;
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
persistIndexRef.current(selectedIndex);
|
|
151
|
+
}, [selectedIndex]);
|
|
152
|
+
const retryRef = useRef(retry);
|
|
153
|
+
retryRef.current = retry;
|
|
154
|
+
const statusRef = useRef(state.status);
|
|
155
|
+
statusRef.current = state.status;
|
|
156
|
+
const isSearchingRef = useRef(isSearching);
|
|
157
|
+
isSearchingRef.current = isSearching;
|
|
158
|
+
const handleRetryInput = useCallback((input) => {
|
|
159
|
+
// M2-07 (#30): "r" digitado como texto de busca não deve disparar retry.
|
|
160
|
+
if (isSearchingRef.current)
|
|
161
|
+
return;
|
|
162
|
+
if (input === 'r' &&
|
|
163
|
+
(statusRef.current === 'error-network' ||
|
|
164
|
+
statusRef.current === 'error-forbidden' ||
|
|
165
|
+
// Fix do review #120: abandono do re-login (Esc) também tem retry.
|
|
166
|
+
statusRef.current === 'auth-aborted')) {
|
|
167
|
+
retryRef.current();
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
useInput(handleRetryInput);
|
|
171
|
+
// "/" abre a busca; "f" cicla o filtro rápido de status (só com a busca já
|
|
172
|
+
// aberta). Reason: dentro do campo de texto, "f" digitado NÃO vira
|
|
173
|
+
// caractere da query — é a tecla de controle do filtro, trade-off
|
|
174
|
+
// Com a busca ABERTA, toda letra pertence à query (buscar "workflow" exige
|
|
175
|
+
// digitar "f") — o ciclo de filtro por "f" só vale com a busca fechada.
|
|
176
|
+
const handleSearchControlInput = useCallback((input) => {
|
|
177
|
+
if (input === '/' && !isSearchingRef.current) {
|
|
178
|
+
setIsSearching(true);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (input === 'f' && !isSearchingRef.current) {
|
|
182
|
+
setStatusFilter((current) => nextStatusFilter(current));
|
|
183
|
+
}
|
|
184
|
+
}, []);
|
|
185
|
+
useInput(handleSearchControlInput);
|
|
186
|
+
// #34 (M2-11): "t" abre o painel de jobs da sessão (`./jobs.js`) — só fora
|
|
187
|
+
// da busca (mesma guarda de "/"/"f" acima: dentro do campo, "t" é texto da
|
|
188
|
+
// query, não um atalho).
|
|
189
|
+
const handleJobsShortcut = useCallback((input) => {
|
|
190
|
+
if (input === 't' && !isSearchingRef.current) {
|
|
191
|
+
pushRef.current('jobs');
|
|
192
|
+
}
|
|
193
|
+
}, []);
|
|
194
|
+
useInput(handleJobsShortcut);
|
|
195
|
+
const searchState = search.state;
|
|
196
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "Minhas issues" }), _jsxs(Text, { color: theme.muted, children: [" [", STATUS_FILTER_LABELS[statusFilter], "]"] }), !isSearching ? (_jsx(Text, { color: theme.muted, children: ` / busca ${glyphs.middleDot} f filtro ${glyphs.middleDot} t jobs` })) : null] }), isSearching ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "Buscar: " }), _jsx(TextInput, { value: query, onChange: setQuery, placeholder: `digite para buscar${glyphs.ellipsis}`, isActive: isSearching }), _jsxs(Text, { color: theme.muted, children: [" [", STATUS_FILTER_LABELS[statusFilter], "]"] })] }), searchState.status === 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Digite para buscar ou pressione f para ciclar o filtro de status." }) })) : null, searchState.status === 'loading' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Buscando..."] }) })) : null, searchState.status === 'loaded' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [searchState.degraded ? (_jsxs(Text, { color: theme.warning, children: [symbols.warning, " ", searchState.warnings.join(' ')] })) : null, _jsx(Text, { children: searchState.content })] })) : null, searchState.status === 'error-network' || searchState.status === 'error-forbidden' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", searchState.message] }) })) : null, searchState.status === 'auth-aborted' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: searchState.message }) })) : null] })) : (_jsxs(_Fragment, { children: [state.status === 'loading' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Carregando issues..."] }) })) : null, state.status === 'empty' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "nenhuma issue atribu\u00EDda" }) })) : null, state.status === 'error-network' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " Falha ao carregar suas issues: ", state.message] }), _jsxs(Text, { color: theme.muted, children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "r" }), ' ', "para tentar de novo."] })] })) : null, state.status === 'error-forbidden' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", state.message] }), _jsxs(Text, { color: theme.muted, children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "r" }), ' ', "para tentar de novo."] })] })) : null, state.status === 'auth-aborted' ? (
|
|
197
|
+
// Fix do review #120: estado NEUTRO (abandono consciente do re-login,
|
|
198
|
+
// Esc) — `theme.muted`, não `theme.danger` (não é uma falha).
|
|
199
|
+
_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: theme.muted, children: state.message }) })) : null, state.status === 'loaded' ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: state.issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.id))) })) : null] })), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: isSearching ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "fecha a busca,", ' ', _jsx(Text, { color: theme.accent, children: "f" }), ' ', "cicla o filtro."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "navega,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "abre a issue,", ' ', _jsx(Text, { color: theme.accent, children: "/" }), ' ', "busca,", ' ', _jsx(Text, { color: theme.accent, children: "t" }), ' ', "jobs,", ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "volta."] })) }) })] }));
|
|
200
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tela de detalhe: metadados fixos + descrição/journals em viewport rolável.
|
|
3
|
+
* `b` (ou `Esc`, atalho global) volta para a home — `selectedIndex` da lista
|
|
4
|
+
* é preservado via `./home-selection.js`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function IssueDetailScreen(): import("react").JSX.Element;
|