redmine-context 1.0.0 → 1.2.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/README.md +20 -5
- package/dist/bundle/journal-detail.d.ts +89 -0
- package/dist/bundle/journal-detail.js +260 -0
- package/dist/bundle/markdown.d.ts +7 -0
- package/dist/bundle/markdown.js +21 -12
- package/dist/client/enumerations.d.ts +39 -0
- package/dist/client/enumerations.js +76 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/fetch-issue-bundle.js +17 -2
- package/dist/fetch-issue-search.d.ts +10 -0
- package/dist/fetch-issue-search.js +1 -1
- package/dist/fetch-last-issues.d.ts +96 -0
- package/dist/fetch-last-issues.js +120 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +22 -2
- package/dist/normalize/issue.js +32 -3
- package/dist/surfaces/cli/commands.d.ts +17 -0
- package/dist/surfaces/cli/commands.js +105 -12
- package/dist/surfaces/cli/main.js +22 -3
- package/dist/surfaces/mcp/server.d.ts +22 -45
- package/dist/surfaces/mcp/server.js +67 -56
- package/dist/surfaces/mcp/tools.d.ts +109 -0
- package/dist/surfaces/mcp/tools.js +93 -0
- package/dist/surfaces/tui/app.d.ts +14 -0
- package/dist/surfaces/tui/app.js +40 -3
- package/dist/surfaces/tui/banner.d.ts +43 -0
- package/dist/surfaces/tui/banner.js +93 -0
- package/dist/surfaces/tui/components/gauge.d.ts +27 -0
- package/dist/surfaces/tui/components/gauge.js +53 -0
- package/dist/surfaces/tui/components/gradient-banner.d.ts +13 -0
- package/dist/surfaces/tui/components/gradient-banner.js +41 -0
- package/dist/surfaces/tui/components/text-input.js +23 -4
- package/dist/surfaces/tui/glyphs.d.ts +8 -0
- package/dist/surfaces/tui/glyphs.js +8 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +7 -1
- package/dist/surfaces/tui/hooks/use-issue-detail.js +15 -2
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +25 -2
- package/dist/surfaces/tui/hooks/use-issue-search.js +14 -2
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +8 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +12 -1
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +9 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +6 -2
- package/dist/surfaces/tui/hooks/use-status-options.d.ts +38 -0
- package/dist/surfaces/tui/hooks/use-status-options.js +86 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +16 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +15 -0
- package/dist/surfaces/tui/hooks/use-typing-guard.d.ts +19 -0
- package/dist/surfaces/tui/hooks/use-typing-guard.js +58 -0
- package/dist/surfaces/tui/list-window.d.ts +39 -0
- package/dist/surfaces/tui/list-window.js +41 -0
- package/dist/surfaces/tui/screens/export.js +6 -2
- package/dist/surfaces/tui/screens/home.js +161 -33
- package/dist/surfaces/tui/screens/issue-detail.js +55 -16
- package/dist/surfaces/tui/screens/jobs.js +3 -2
- package/dist/surfaces/tui/screens/welcome.js +9 -1
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +33 -0
- package/dist/surfaces/tui/wrap.d.ts +38 -0
- package/dist/surfaces/tui/wrap.js +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opções do filtro de status da home (#30, revisto).
|
|
3
|
+
*
|
|
4
|
+
* O ciclo `todas → abertas → fechadas` não servia a uma instância real: todos os
|
|
5
|
+
* status de trabalho (Nova, Fila, Estimativa, Atribuída, Em Andamento,
|
|
6
|
+
* Validação…) são ABERTOS, então alternar entre "todas" e "abertas" devolvia a
|
|
7
|
+
* mesma lista e o filtro parecia quebrado.
|
|
8
|
+
*
|
|
9
|
+
* Este hook carrega os status DA INSTÂNCIA (`/issue_statuses.json`, já memoizado
|
|
10
|
+
* por `fetchEnumerations`) e os oferece como opções, com os dois agregados úteis
|
|
11
|
+
* no topo. Degrada em silêncio: sem permissão ou sem rede, sobram os agregados —
|
|
12
|
+
* o mesmo comportamento de antes, nunca um erro na tela.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
15
|
+
import { createHttpClient, fetchEnumerations, resolveApiKey, } from '../../../index.js';
|
|
16
|
+
import { useEnvFallbackAllowed } from '../instance.js';
|
|
17
|
+
/** Agregados sempre disponíveis, mesmo sem as enumerações da instância. */
|
|
18
|
+
const BASE_OPTIONS = [
|
|
19
|
+
{ value: 'all', label: 'Todas' },
|
|
20
|
+
{ value: 'open', label: 'Abertas' },
|
|
21
|
+
{ value: 'closed', label: 'Fechadas' },
|
|
22
|
+
];
|
|
23
|
+
/** Lê `REDMINE_URL` do ambiente, tratando string vazia como ausente. */
|
|
24
|
+
function instanceFromEnv(env) {
|
|
25
|
+
const value = env.REDMINE_URL;
|
|
26
|
+
return value !== undefined && value.length > 0 ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Opções do filtro de status: agregados + os status reais da instância.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Dependências injetáveis (ver {@link UseStatusOptionsOptions}).
|
|
32
|
+
* @returns A lista de opções; só os agregados enquanto carrega ou se falhar.
|
|
33
|
+
* @example
|
|
34
|
+
* const options = useStatusOptions();
|
|
35
|
+
* // [{ value: 'all', label: 'Todas' }, ..., { value: 7, label: 'Atribuída' }]
|
|
36
|
+
*/
|
|
37
|
+
export function useStatusOptions(options = {}) {
|
|
38
|
+
const env = options.env ?? process.env;
|
|
39
|
+
const resolve = options.resolveApiKey ?? resolveApiKey;
|
|
40
|
+
const buildClient = options.createHttpClient ?? createHttpClient;
|
|
41
|
+
const loadEnumerations = options.fetchEnumerations ?? fetchEnumerations;
|
|
42
|
+
const allowEnvFallback = useEnvFallbackAllowed();
|
|
43
|
+
const [statuses, setStatuses] = useState([]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
const instanceUrl = instanceFromEnv(env);
|
|
47
|
+
if (instanceUrl === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
void (async () => {
|
|
50
|
+
try {
|
|
51
|
+
const cascade = { env, allowEnvFallback };
|
|
52
|
+
const apiKey = await resolve(instanceUrl, cascade);
|
|
53
|
+
if (apiKey === undefined || cancelled)
|
|
54
|
+
return;
|
|
55
|
+
const enums = await loadEnumerations(buildClient({ baseUrl: instanceUrl, apiKey }), instanceUrl);
|
|
56
|
+
if (cancelled)
|
|
57
|
+
return;
|
|
58
|
+
// Ordem estável por id: a lista não pode dançar entre renders.
|
|
59
|
+
setStatuses([...enums.status.entries()]
|
|
60
|
+
.sort(([a], [b]) => a - b)
|
|
61
|
+
.map(([id, label]) => ({ value: id, label })));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Enriquecimento, não dado essencial: sem os status da instância o
|
|
65
|
+
// seletor segue com os agregados.
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
};
|
|
71
|
+
}, [env, allowEnvFallback, resolve, buildClient, loadEnumerations]);
|
|
72
|
+
return statuses.length === 0 ? BASE_OPTIONS : [...BASE_OPTIONS, ...statuses];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Rótulo de um filtro, resolvido contra as opções carregadas.
|
|
76
|
+
*
|
|
77
|
+
* @param options - Opções disponíveis.
|
|
78
|
+
* @param filter - Filtro corrente.
|
|
79
|
+
* @returns O rótulo da opção, ou `#id` se o status não constar (degradação).
|
|
80
|
+
*/
|
|
81
|
+
export function statusFilterLabel(options, filter) {
|
|
82
|
+
const found = options.find((option) => option.value === filter);
|
|
83
|
+
if (found !== undefined)
|
|
84
|
+
return found.label;
|
|
85
|
+
return typeof filter === 'number' ? `#${filter}` : filter;
|
|
86
|
+
}
|
|
@@ -34,6 +34,22 @@ export declare function TerminalWidthProvider({ width, children }: {
|
|
|
34
34
|
* consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
|
|
35
35
|
* estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* Sobrescreve a ALTURA disponível para as telas abaixo.
|
|
39
|
+
*
|
|
40
|
+
* Usado pelo shell (`../app.tsx`) para descontar as linhas que a moldura da
|
|
41
|
+
* aplicação consome: as telas pedem `useTerminalHeight()` e devem receber o
|
|
42
|
+
* espaço que sobra, sem precisar saber que existe uma moldura — do contrário
|
|
43
|
+
* cada tela carregaria uma constante acoplada ao desenho do shell (e os testes,
|
|
44
|
+
* que montam telas sem moldura, veriam um valor errado).
|
|
45
|
+
*
|
|
46
|
+
* @param props.height - Altura disponível, em linhas.
|
|
47
|
+
* @param props.children - Subárvore que passa a enxergar essa altura.
|
|
48
|
+
*/
|
|
49
|
+
export declare function TerminalHeightProvider({ height, children }: {
|
|
50
|
+
height: number;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
37
53
|
export declare function TerminalSizeProvider({ children }: {
|
|
38
54
|
children: ReactNode;
|
|
39
55
|
}): import("react").JSX.Element;
|
|
@@ -47,6 +47,21 @@ export function TerminalWidthProvider({ width, children }) {
|
|
|
47
47
|
* consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
|
|
48
48
|
* estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
|
|
49
49
|
*/
|
|
50
|
+
/**
|
|
51
|
+
* Sobrescreve a ALTURA disponível para as telas abaixo.
|
|
52
|
+
*
|
|
53
|
+
* Usado pelo shell (`../app.tsx`) para descontar as linhas que a moldura da
|
|
54
|
+
* aplicação consome: as telas pedem `useTerminalHeight()` e devem receber o
|
|
55
|
+
* espaço que sobra, sem precisar saber que existe uma moldura — do contrário
|
|
56
|
+
* cada tela carregaria uma constante acoplada ao desenho do shell (e os testes,
|
|
57
|
+
* que montam telas sem moldura, veriam um valor errado).
|
|
58
|
+
*
|
|
59
|
+
* @param props.height - Altura disponível, em linhas.
|
|
60
|
+
* @param props.children - Subárvore que passa a enxergar essa altura.
|
|
61
|
+
*/
|
|
62
|
+
export function TerminalHeightProvider({ height, children }) {
|
|
63
|
+
return _jsx(TerminalHeightContext.Provider, { value: height, children: children });
|
|
64
|
+
}
|
|
50
65
|
export function TerminalSizeProvider({ children }) {
|
|
51
66
|
const [size, setSize] = useState(() => ({ width: readProcessColumns(), height: readProcessRows() }));
|
|
52
67
|
useEffect(() => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Há algum campo de texto ativo agora?
|
|
3
|
+
*
|
|
4
|
+
* Chamado pelos handlers de atalho ANTES de reagir a uma tecla de letra.
|
|
5
|
+
*
|
|
6
|
+
* @returns `true` enquanto qualquer `TextInput` ativo estiver montado.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isTyping(): boolean;
|
|
9
|
+
/** Zera o contador — para os testes não vazarem estado entre casos. */
|
|
10
|
+
export declare function resetTypingGuard(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Registra um campo de texto como ativo enquanto `active` for `true`.
|
|
13
|
+
*
|
|
14
|
+
* @param active - `true` enquanto o campo captura teclado.
|
|
15
|
+
* @example
|
|
16
|
+
* // dentro de um componente de input:
|
|
17
|
+
* useTypingGuard(isActive);
|
|
18
|
+
*/
|
|
19
|
+
export declare function useTypingGuard(active: boolean): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarda de DIGITAÇÃO para os atalhos de letra.
|
|
3
|
+
*
|
|
4
|
+
* O Ink entrega cada tecla a TODOS os `useInput` registrados na árvore — um
|
|
5
|
+
* campo de texto ativo não "consome" a tecla. Sem esta guarda, qualquer atalho
|
|
6
|
+
* global de letra dispara no meio da digitação: `q` (sair) fecha a TUI enquanto
|
|
7
|
+
* se digita a URL da instância no onboarding (`https://redmine.qualquer...`) ou
|
|
8
|
+
* uma senha que contenha a letra.
|
|
9
|
+
*
|
|
10
|
+
* As telas que já sabem quando estão em modo de texto (a busca da home) checam
|
|
11
|
+
* o próprio estado; este módulo cobre o caso GERAL — qualquer `TextInput` ativo
|
|
12
|
+
* em qualquer tela suspende os atalhos de letra enquanto durar.
|
|
13
|
+
*
|
|
14
|
+
* Mesmo desenho de `./use-escape-interceptor.ts`: estado mutável em nível de
|
|
15
|
+
* módulo, não Context/Provider — a TUI sustenta uma tela por vez e um Provider
|
|
16
|
+
* seria complexidade sem benefício. Aqui é um CONTADOR (não um booleano) porque
|
|
17
|
+
* uma tela pode montar mais de um campo (ex.: usuário + senha) e a ordem de
|
|
18
|
+
* montagem/desmontagem entre eles não é garantida.
|
|
19
|
+
*
|
|
20
|
+
* Atalhos com modificador (`Ctrl+C`) continuam livres: eles não colidem com
|
|
21
|
+
* texto. `Command+<letra>` não é alternativa em TUI — no macOS o terminal
|
|
22
|
+
* intercepta antes de a tecla chegar à aplicação.
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect } from 'react';
|
|
25
|
+
/** Quantidade de campos de texto ATIVOS no momento. */
|
|
26
|
+
let activeInputs = 0;
|
|
27
|
+
/**
|
|
28
|
+
* Há algum campo de texto ativo agora?
|
|
29
|
+
*
|
|
30
|
+
* Chamado pelos handlers de atalho ANTES de reagir a uma tecla de letra.
|
|
31
|
+
*
|
|
32
|
+
* @returns `true` enquanto qualquer `TextInput` ativo estiver montado.
|
|
33
|
+
*/
|
|
34
|
+
export function isTyping() {
|
|
35
|
+
return activeInputs > 0;
|
|
36
|
+
}
|
|
37
|
+
/** Zera o contador — para os testes não vazarem estado entre casos. */
|
|
38
|
+
export function resetTypingGuard() {
|
|
39
|
+
activeInputs = 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Registra um campo de texto como ativo enquanto `active` for `true`.
|
|
43
|
+
*
|
|
44
|
+
* @param active - `true` enquanto o campo captura teclado.
|
|
45
|
+
* @example
|
|
46
|
+
* // dentro de um componente de input:
|
|
47
|
+
* useTypingGuard(isActive);
|
|
48
|
+
*/
|
|
49
|
+
export function useTypingGuard(active) {
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!active)
|
|
52
|
+
return undefined;
|
|
53
|
+
activeInputs += 1;
|
|
54
|
+
return () => {
|
|
55
|
+
activeInputs = Math.max(0, activeInputs - 1);
|
|
56
|
+
};
|
|
57
|
+
}, [active]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Janela visível de uma lista longa, seguindo o cursor.
|
|
3
|
+
*
|
|
4
|
+
* A home renderizava TODAS as issues de uma vez. Enquanto a lista era curta
|
|
5
|
+
* (as abertas de uma pessoa) isso passava; filtrando por um status com centenas
|
|
6
|
+
* de itens, a lista estoura a altura do terminal, o rodapé é empurrado para fora
|
|
7
|
+
* e o cursor — que começa no topo — some da tela: "cadê meu cursor".
|
|
8
|
+
*
|
|
9
|
+
* Esta é a contraparte de `./components/scroll-view.tsx` para listas
|
|
10
|
+
* SELECIONÁVEIS: lá o deslocamento é dirigido pelo teclado sobre conteúdo
|
|
11
|
+
* estático; aqui ele acompanha o índice selecionado, que é quem manda.
|
|
12
|
+
*
|
|
13
|
+
* Sem estado próprio de propósito: a janela é função do (total, selecionado,
|
|
14
|
+
* altura). Guardar um offset exigiria sincronizá-lo com a seleção, com a troca
|
|
15
|
+
* de filtro e com o redimensionamento do terminal — três fontes de divergência.
|
|
16
|
+
*/
|
|
17
|
+
/** Intervalo `[start, end)` de itens a renderizar. */
|
|
18
|
+
export interface ListWindow {
|
|
19
|
+
/** Índice do primeiro item visível. */
|
|
20
|
+
readonly start: number;
|
|
21
|
+
/** Índice logo APÓS o último visível (exclusivo, pronto para `slice`). */
|
|
22
|
+
readonly end: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Calcula a fatia visível mantendo o item selecionado dentro dela.
|
|
26
|
+
*
|
|
27
|
+
* O cursor é mantido ao MEIO da janela sempre que possível; nas pontas a janela
|
|
28
|
+
* gruda no início ou no fim, para não sobrar espaço vazio.
|
|
29
|
+
*
|
|
30
|
+
* @param total - Quantidade de itens da lista.
|
|
31
|
+
* @param selected - Índice selecionado (fora da faixa é fixado nela).
|
|
32
|
+
* @param height - Linhas disponíveis; `<= 0` devolve uma janela vazia.
|
|
33
|
+
* @returns O intervalo `[start, end)` para `slice`.
|
|
34
|
+
* @example
|
|
35
|
+
* listWindow(347, 0, 20); // { start: 0, end: 20 }
|
|
36
|
+
* listWindow(347, 200, 20); // { start: 190, end: 210 } — cursor centrado
|
|
37
|
+
* listWindow(347, 346, 20); // { start: 327, end: 347 } — grudado no fim
|
|
38
|
+
*/
|
|
39
|
+
export declare function listWindow(total: number, selected: number, height: number): ListWindow;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Janela visível de uma lista longa, seguindo o cursor.
|
|
3
|
+
*
|
|
4
|
+
* A home renderizava TODAS as issues de uma vez. Enquanto a lista era curta
|
|
5
|
+
* (as abertas de uma pessoa) isso passava; filtrando por um status com centenas
|
|
6
|
+
* de itens, a lista estoura a altura do terminal, o rodapé é empurrado para fora
|
|
7
|
+
* e o cursor — que começa no topo — some da tela: "cadê meu cursor".
|
|
8
|
+
*
|
|
9
|
+
* Esta é a contraparte de `./components/scroll-view.tsx` para listas
|
|
10
|
+
* SELECIONÁVEIS: lá o deslocamento é dirigido pelo teclado sobre conteúdo
|
|
11
|
+
* estático; aqui ele acompanha o índice selecionado, que é quem manda.
|
|
12
|
+
*
|
|
13
|
+
* Sem estado próprio de propósito: a janela é função do (total, selecionado,
|
|
14
|
+
* altura). Guardar um offset exigiria sincronizá-lo com a seleção, com a troca
|
|
15
|
+
* de filtro e com o redimensionamento do terminal — três fontes de divergência.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Calcula a fatia visível mantendo o item selecionado dentro dela.
|
|
19
|
+
*
|
|
20
|
+
* O cursor é mantido ao MEIO da janela sempre que possível; nas pontas a janela
|
|
21
|
+
* gruda no início ou no fim, para não sobrar espaço vazio.
|
|
22
|
+
*
|
|
23
|
+
* @param total - Quantidade de itens da lista.
|
|
24
|
+
* @param selected - Índice selecionado (fora da faixa é fixado nela).
|
|
25
|
+
* @param height - Linhas disponíveis; `<= 0` devolve uma janela vazia.
|
|
26
|
+
* @returns O intervalo `[start, end)` para `slice`.
|
|
27
|
+
* @example
|
|
28
|
+
* listWindow(347, 0, 20); // { start: 0, end: 20 }
|
|
29
|
+
* listWindow(347, 200, 20); // { start: 190, end: 210 } — cursor centrado
|
|
30
|
+
* listWindow(347, 346, 20); // { start: 327, end: 347 } — grudado no fim
|
|
31
|
+
*/
|
|
32
|
+
export function listWindow(total, selected, height) {
|
|
33
|
+
if (height <= 0 || total <= 0)
|
|
34
|
+
return { start: 0, end: 0 };
|
|
35
|
+
if (total <= height)
|
|
36
|
+
return { start: 0, end: total };
|
|
37
|
+
const cursor = Math.min(Math.max(selected, 0), total - 1);
|
|
38
|
+
const half = Math.floor(height / 2);
|
|
39
|
+
const start = Math.min(Math.max(0, cursor - half), total - height);
|
|
40
|
+
return { start, end: start + height };
|
|
41
|
+
}
|
|
@@ -53,6 +53,7 @@ import { useExportBundle } from '../hooks/use-export-bundle.js';
|
|
|
53
53
|
import { useListNavigation } from '../hooks/use-list-navigation.js';
|
|
54
54
|
import { useJobRegistry } from '../job-registry.js';
|
|
55
55
|
import { useTerminalWidth } from '../hooks/use-terminal-width.js';
|
|
56
|
+
import { isTyping } from '../hooks/use-typing-guard.js';
|
|
56
57
|
import { useNavigation } from '../navigation.js';
|
|
57
58
|
import { symbols } from '../symbols.js';
|
|
58
59
|
import { useTheme } from '../theme.js';
|
|
@@ -154,8 +155,11 @@ export function ExportScreen() {
|
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
156
157
|
// "b" não interrompe uma exportação em andamento — evita voltar no meio
|
|
157
|
-
// de uma gravação em disco
|
|
158
|
-
|
|
158
|
+
// de uma gravação em disco — nem dispara enquanto o campo "Destino" está
|
|
159
|
+
// capturando teclado: o Ink entrega a tecla aos dois handlers, então um
|
|
160
|
+
// caminho como `~/backup/` ou `bundle.json` fecharia a tela no meio da
|
|
161
|
+
// digitação (ver ../hooks/use-typing-guard.ts).
|
|
162
|
+
if (input === 'b' && !isTyping() && stateStatusRef.current !== 'exporting') {
|
|
159
163
|
popRef.current();
|
|
160
164
|
return;
|
|
161
165
|
}
|
|
@@ -21,7 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
21
21
|
* retry.
|
|
22
22
|
*
|
|
23
23
|
* M2-07 (#30) acrescenta a busca/filtros inline: `/` abre um `TextInput`
|
|
24
|
-
* QUANDO a home está ativa; `f`
|
|
24
|
+
* QUANDO a home está ativa; `f` abre o seletor de status com a busca FECHADA (badge no
|
|
25
25
|
* cabeçalho); Esc fecha a busca sem refetch, interceptado via
|
|
26
26
|
* `../hooks/use-escape-interceptor.ts` para não desempilhar a home.
|
|
27
27
|
*
|
|
@@ -43,12 +43,15 @@ import { Spinner } from '../components/spinner.js';
|
|
|
43
43
|
import { TextInput } from '../components/text-input.js';
|
|
44
44
|
import { glyphs } from '../glyphs.js';
|
|
45
45
|
import { useEscapeInterceptor } from '../hooks/use-escape-interceptor.js';
|
|
46
|
+
import { isTyping } from '../hooks/use-typing-guard.js';
|
|
47
|
+
import { listWindow } from '../list-window.js';
|
|
48
|
+
import { statusFilterLabel, useStatusOptions } from '../hooks/use-status-options.js';
|
|
46
49
|
import { useIssueSearch } from '../hooks/use-issue-search.js';
|
|
47
50
|
import { useListNavigation } from '../hooks/use-list-navigation.js';
|
|
48
51
|
import { useMyIssues } from '../hooks/use-my-issues.js';
|
|
49
|
-
import { useTerminalWidth } from '../hooks/use-terminal-width.js';
|
|
52
|
+
import { useTerminalHeight, useTerminalWidth } from '../hooks/use-terminal-width.js';
|
|
50
53
|
import { useNavigation } from '../navigation.js';
|
|
51
|
-
import { statusColor } from '../status-color.js';
|
|
54
|
+
import { statusColor, statusFilterColor } from '../status-color.js';
|
|
52
55
|
import { symbols } from '../symbols.js';
|
|
53
56
|
import { useTheme } from '../theme.js';
|
|
54
57
|
import { truncate } from '../truncate.js';
|
|
@@ -61,20 +64,14 @@ const SCREEN_PADDING_X = 2;
|
|
|
61
64
|
// literal `[]` inline em cada render seria recriado a cada chamada,
|
|
62
65
|
// invalidando memoizações a jusante (`useListNavigation`) sem necessidade.
|
|
63
66
|
const EMPTY_ISSUES = [];
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
if (current === 'open')
|
|
73
|
-
return 'closed';
|
|
74
|
-
if (current === 'closed')
|
|
75
|
-
return 'all';
|
|
76
|
-
return 'open';
|
|
77
|
-
}
|
|
67
|
+
/**
|
|
68
|
+
* Linhas ocupadas fora da lista NESTA tela (breadcrumb, cabeçalho, contador,
|
|
69
|
+
* rodapé, paddings). A moldura da aplicação não entra: o shell já entrega a
|
|
70
|
+
* altura sem ela (ver `TerminalHeightProvider` em `../app.tsx`).
|
|
71
|
+
*/
|
|
72
|
+
const LIST_OVERHEAD_ROWS = 9;
|
|
73
|
+
/** Piso da janela da lista (terminais muito baixos). */
|
|
74
|
+
const LIST_MIN_HEIGHT = 5;
|
|
78
75
|
/**
|
|
79
76
|
* Espaço fixo ocupado pela linha FORA do subject (M2-16, #39): ponteiro (2),
|
|
80
77
|
* `#id ` (id + `#`/espaço), 1 espaço final após o subject, `[status]`
|
|
@@ -89,6 +86,20 @@ function fixedRowOverhead(idLength, statusLength) {
|
|
|
89
86
|
return POINTER_WIDTH + ID_PREFIX_WIDTH + SUBJECT_TRAILING_SPACE + STATUS_BRACKETS_WIDTH + SCREEN_PADDING_X;
|
|
90
87
|
}
|
|
91
88
|
/** Uma linha da lista: `#id` em `theme.muted`, subject truncado ao orçamento de largura, badge de status. */
|
|
89
|
+
/**
|
|
90
|
+
* Uma linha de RESULTADO DE BUSCA.
|
|
91
|
+
*
|
|
92
|
+
* Espelha o layout do {@link IssueRow} (id · assunto · status), mas a partir do
|
|
93
|
+
* item estruturado da busca — sem passar pelo Markdown do bundle, que traria as
|
|
94
|
+
* fences `<untrusted-content>` para a tela.
|
|
95
|
+
*/
|
|
96
|
+
function SearchResultRow({ item, selected }) {
|
|
97
|
+
const theme = useTheme();
|
|
98
|
+
const terminalWidth = useTerminalWidth();
|
|
99
|
+
const overhead = fixedRowOverhead(String(item.id).length, item.status.length);
|
|
100
|
+
const subjectBudget = Math.max(terminalWidth - overhead, MIN_SUBJECT_WIDTH);
|
|
101
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), _jsxs(Text, { color: theme.muted, children: ["#", item.id, " "] }), _jsx(Text, { ...(selected ? { color: theme.primary } : {}), children: truncate(item.subject ?? '(sem assunto)', subjectBudget) }), _jsxs(Text, { color: statusColor(theme, item.status), children: [" [", item.status, "]"] }), _jsxs(Text, { color: theme.muted, children: [" ", item.assignee] })] }));
|
|
102
|
+
}
|
|
92
103
|
function IssueRow({ issue, selected }) {
|
|
93
104
|
const theme = useTheme();
|
|
94
105
|
const terminalWidth = useTerminalWidth();
|
|
@@ -100,28 +111,64 @@ function IssueRow({ issue, selected }) {
|
|
|
100
111
|
export function HomeScreen() {
|
|
101
112
|
const theme = useTheme();
|
|
102
113
|
const { push } = useNavigation();
|
|
103
|
-
const { state, retry } = useMyIssues();
|
|
104
114
|
// #31: índice preservado entre remounts + registro de qual issue foi aberta
|
|
105
115
|
// (ver o JSDoc do módulo e de `./home-selection.js`).
|
|
106
116
|
const { selectedIndex: persistedIndex, setSelectedIndex: persistIndex, setSelectedIssueId } = useHomeSelection();
|
|
107
117
|
// --- Busca/filtros inline (M2-07, #30) ---
|
|
108
118
|
const [isSearching, setIsSearching] = useState(false);
|
|
109
119
|
const [query, setQuery] = useState('');
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// Default ABERTAS, não "todas": a home é a lista de trabalho. Com `all` o
|
|
121
|
+
// Redmine devolve o histórico inteiro (centenas de fechadas), que estoura a
|
|
122
|
+
// tela e enterra o que importa — era o default implícito antes do filtro
|
|
123
|
+
// chegar à lista (a API omite fechadas quando `status_id` não é enviado).
|
|
124
|
+
const [statusFilter, setStatusFilter] = useState('open');
|
|
125
|
+
// Seletor de status (#30, revisto): `f` abre uma LISTA em vez de ciclar às
|
|
126
|
+
// cegas — a instância tem uma dezena de status e o usuário não tem como
|
|
127
|
+
// adivinhar qual vem a seguir num ciclo.
|
|
128
|
+
const [isPickingStatus, setIsPickingStatus] = useState(false);
|
|
129
|
+
const statusOptions = useStatusOptions();
|
|
130
|
+
const [statusIndex, setStatusIndex] = useState(0);
|
|
131
|
+
// Seleção dentro dos RESULTADOS da busca: achar o chamado e não conseguir
|
|
132
|
+
// abrir é o mesmo que não ter achado. As setas não são texto, então navegam
|
|
133
|
+
// os resultados enquanto o campo continua recebendo letras.
|
|
134
|
+
const [searchIndex, setSearchIndex] = useState(0);
|
|
135
|
+
const searchIndexRef = useRef(searchIndex);
|
|
136
|
+
searchIndexRef.current = searchIndex;
|
|
137
|
+
// O filtro só vai para a BUSCA quando ela está aberta: com ela fechada, quem
|
|
138
|
+
// aplica o status é a lista (abaixo), e passar o filtro aqui dispararia um
|
|
139
|
+
// request cujo resultado nunca é renderizado — dois GETs por `f` em vez de um.
|
|
140
|
+
const search = useIssueSearch(query, isSearching ? statusFilter : 'all');
|
|
141
|
+
// O filtro rápido (`f`) precisa valer para a LISTA visível — antes ele só
|
|
142
|
+
// alimentava a busca, cujos resultados só aparecem com a busca ABERTA, então
|
|
143
|
+
// trocar o status não mudava nada na tela.
|
|
144
|
+
const { state, retry } = useMyIssues({ statusFilter });
|
|
112
145
|
// Handler ESTÁVEL: `search.clear` é a única dependência mutável (mas já é
|
|
113
146
|
// estável por construção, ver `use-issue-search.ts`) — fecha a busca e
|
|
114
147
|
// restaura os 3 estados neutros de uma vez (query/filtro/hook de busca).
|
|
115
148
|
const closeSearch = useCallback(() => {
|
|
116
149
|
setIsSearching(false);
|
|
117
150
|
setQuery('');
|
|
118
|
-
|
|
151
|
+
// O filtro de status NÃO é resetado: desde que ele governa a LISTA (e não
|
|
152
|
+
// só a busca), zerá-lo aqui desfazia uma escolha que o usuário fez ANTES de
|
|
153
|
+
// abrir a busca — `f`, depois `/`, depois `Esc` devolvia [Todas].
|
|
119
154
|
search.clear();
|
|
120
155
|
}, [search.clear]);
|
|
121
156
|
// Desvia o Esc GLOBAL (`../app.tsx`) enquanto a busca está aberta — sem
|
|
122
157
|
// isso, Esc desempilharia a home inteira em vez de só fechar a busca.
|
|
158
|
+
// Resultado novo, cursor no topo: manter o índice de uma busca anterior faria
|
|
159
|
+
// o Enter abrir uma issue que não é a que está sob o cursor.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
setSearchIndex(0);
|
|
162
|
+
searchIndexRef.current = 0;
|
|
163
|
+
}, [query, statusFilter]);
|
|
123
164
|
useEscapeInterceptor(isSearching, closeSearch);
|
|
165
|
+
const closeStatusPicker = useCallback(() => setIsPickingStatus(false), []);
|
|
166
|
+
useEscapeInterceptor(isPickingStatus, closeStatusPicker);
|
|
124
167
|
const issues = state.status === 'loaded' ? state.issues : EMPTY_ISSUES;
|
|
168
|
+
// Altura da lista: o terminal menos a moldura, breadcrumb, cabeçalho, contador
|
|
169
|
+
// e rodapé. A lista rola DENTRO dessa janela em vez de empurrar o resto da
|
|
170
|
+
// tela para fora — e a mesma altura é o salto de uma página.
|
|
171
|
+
const listHeight = Math.max(LIST_MIN_HEIGHT, useTerminalHeight() - LIST_OVERHEAD_ROWS);
|
|
125
172
|
// Handlers ESTÁVEIS (useCallback + refs, padrão do repo): identidade nova a
|
|
126
173
|
// cada render des/re-subscreve o useInput e pode perder uma tecla rápida.
|
|
127
174
|
const pushRef = useRef(push);
|
|
@@ -139,8 +186,13 @@ export function HomeScreen() {
|
|
|
139
186
|
// última posição persistida (sobrevive ao unmount via home-selection).
|
|
140
187
|
const { selectedIndex } = useListNavigation(issues.length, {
|
|
141
188
|
onSelect: handleSelect,
|
|
142
|
-
|
|
189
|
+
// Também desligada com o SELETOR DE STATUS aberto: o Ink entrega a tecla a
|
|
190
|
+
// todos os handlers, então o Enter que aplica o filtro abria a issue
|
|
191
|
+
// selecionada por baixo, ao mesmo tempo.
|
|
192
|
+
isActive: !isSearching && !isPickingStatus,
|
|
143
193
|
initialIndex: persistedIndex,
|
|
194
|
+
// Uma página = uma tela da janela visível (ver ../list-window.ts).
|
|
195
|
+
pageSize: listHeight,
|
|
144
196
|
});
|
|
145
197
|
// Espelha `selectedIndex` ao contexto — a cópia externa sobrevive ao
|
|
146
198
|
// unmount desta tela (ver ./home-selection.js).
|
|
@@ -155,6 +207,16 @@ export function HomeScreen() {
|
|
|
155
207
|
statusRef.current = state.status;
|
|
156
208
|
const isSearchingRef = useRef(isSearching);
|
|
157
209
|
isSearchingRef.current = isSearching;
|
|
210
|
+
const isPickingStatusRef = useRef(isPickingStatus);
|
|
211
|
+
isPickingStatusRef.current = isPickingStatus;
|
|
212
|
+
const optionsRef = useRef(statusOptions);
|
|
213
|
+
optionsRef.current = statusOptions;
|
|
214
|
+
const statusIndexRef = useRef(statusIndex);
|
|
215
|
+
statusIndexRef.current = statusIndex;
|
|
216
|
+
const filterRef = useRef(statusFilter);
|
|
217
|
+
filterRef.current = statusFilter;
|
|
218
|
+
const searchItemsRef = useRef([]);
|
|
219
|
+
searchItemsRef.current = search.state.status === 'loaded' ? search.state.items : [];
|
|
158
220
|
const handleRetryInput = useCallback((input) => {
|
|
159
221
|
// M2-07 (#30): "r" digitado como texto de busca não deve disparar retry.
|
|
160
222
|
if (isSearchingRef.current)
|
|
@@ -168,33 +230,99 @@ export function HomeScreen() {
|
|
|
168
230
|
}
|
|
169
231
|
}, []);
|
|
170
232
|
useInput(handleRetryInput);
|
|
171
|
-
// "/" abre a busca; "f"
|
|
172
|
-
// aberta
|
|
173
|
-
//
|
|
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.
|
|
233
|
+
// "/" abre a busca; "f" abre o seletor de status. Ambos só FORA de campo de
|
|
234
|
+
// texto: com a busca aberta toda letra pertence à query (buscar "workflow"
|
|
235
|
+
// exige digitar "f") — ver ../hooks/use-typing-guard.ts.
|
|
176
236
|
const handleSearchControlInput = useCallback((input) => {
|
|
177
|
-
if (
|
|
237
|
+
if (isPickingStatusRef.current)
|
|
238
|
+
return;
|
|
239
|
+
if (input === '/' && !isTyping()) {
|
|
178
240
|
setIsSearching(true);
|
|
179
241
|
return;
|
|
180
242
|
}
|
|
181
|
-
if (input === 'f' && !
|
|
182
|
-
|
|
243
|
+
if (input === 'f' && !isTyping()) {
|
|
244
|
+
// Abre já posicionado no filtro atual, para o usuário ver onde está.
|
|
245
|
+
const start = Math.max(0, optionsRef.current.findIndex((o) => o.value === filterRef.current));
|
|
246
|
+
statusIndexRef.current = start;
|
|
247
|
+
setStatusIndex(start);
|
|
248
|
+
setIsPickingStatus(true);
|
|
183
249
|
}
|
|
184
250
|
}, []);
|
|
185
251
|
useInput(handleSearchControlInput);
|
|
252
|
+
// Navegação do seletor de status.
|
|
253
|
+
const handleStatusPickerInput = useCallback((input, key) => {
|
|
254
|
+
if (!isPickingStatusRef.current)
|
|
255
|
+
return;
|
|
256
|
+
const total = optionsRef.current.length;
|
|
257
|
+
// A ref avança JUNTO com o estado: sincronizá-la só no render faz o
|
|
258
|
+
// `Enter` logo após um `j`/`k` ler o índice ANTIGO (o React ainda não
|
|
259
|
+
// commitou) e aplicar o filtro errado — mesmo defeito que o TextInput
|
|
260
|
+
// tinha ao perder teclas digitadas rápido.
|
|
261
|
+
const move = (delta) => {
|
|
262
|
+
const next = (statusIndexRef.current + delta + total) % total;
|
|
263
|
+
statusIndexRef.current = next;
|
|
264
|
+
setStatusIndex(next);
|
|
265
|
+
};
|
|
266
|
+
if (key.upArrow || input === 'k') {
|
|
267
|
+
move(-1);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.downArrow || input === 'j') {
|
|
271
|
+
move(1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key.return) {
|
|
275
|
+
const picked = optionsRef.current[statusIndexRef.current];
|
|
276
|
+
if (picked !== undefined)
|
|
277
|
+
setStatusFilter(picked.value);
|
|
278
|
+
setIsPickingStatus(false);
|
|
279
|
+
}
|
|
280
|
+
}, []);
|
|
281
|
+
useInput(handleStatusPickerInput);
|
|
282
|
+
// Navegação dos RESULTADOS da busca (setas + Enter). O campo de texto ignora
|
|
283
|
+
// setas e Enter, então não há disputa: as letras seguem indo para a query.
|
|
284
|
+
const handleSearchResultsInput = useCallback((_input, key) => {
|
|
285
|
+
const items = searchItemsRef.current;
|
|
286
|
+
if (!isSearchingRef.current || items.length === 0)
|
|
287
|
+
return;
|
|
288
|
+
if (key.upArrow) {
|
|
289
|
+
setSearchIndex((i) => (i - 1 + items.length) % items.length);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (key.downArrow) {
|
|
293
|
+
setSearchIndex((i) => (i + 1) % items.length);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (key.return) {
|
|
297
|
+
const picked = items[searchIndexRef.current];
|
|
298
|
+
if (picked !== undefined) {
|
|
299
|
+
setSelectedIssueIdRef.current(picked.id);
|
|
300
|
+
pushRef.current('issue-detail');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}, []);
|
|
304
|
+
useInput(handleSearchResultsInput);
|
|
186
305
|
// #34 (M2-11): "t" abre o painel de jobs da sessão (`./jobs.js`) — só fora
|
|
187
306
|
// da busca (mesma guarda de "/"/"f" acima: dentro do campo, "t" é texto da
|
|
188
307
|
// query, não um atalho).
|
|
189
308
|
const handleJobsShortcut = useCallback((input) => {
|
|
190
|
-
if (input === 't' && !
|
|
309
|
+
if (input === 't' && !isTyping()) {
|
|
191
310
|
pushRef.current('jobs');
|
|
192
311
|
}
|
|
193
312
|
}, []);
|
|
194
313
|
useInput(handleJobsShortcut);
|
|
314
|
+
const listWindow_ = listWindow(issues.length, selectedIndex, listHeight);
|
|
195
315
|
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
|
|
316
|
+
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: statusFilterColor(theme, statusFilter, statusFilterLabel(statusOptions, statusFilter)), children: [' ', "[", statusFilterLabel(statusOptions, statusFilter), "]"] }), state.status === 'loaded' ? (_jsx(Text, { color: theme.muted, children: ` ${glyphs.middleDot} ${issues.length} ${issues.length === 1 ? 'issue' : 'issues'}` })) : null, !isSearching && !isPickingStatus ? (_jsx(Text, { color: theme.muted, children: ` / busca ${glyphs.middleDot} f filtro ${glyphs.middleDot} t jobs` })) : null] }), isPickingStatus ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.primary, children: "Filtrar por status" }), statusOptions.map((option, i) => {
|
|
317
|
+
const selected = i === statusIndex;
|
|
318
|
+
const current = option.value === statusFilter;
|
|
319
|
+
// Divisor entre os AGREGADOS do Redmine e os status da instância:
|
|
320
|
+
// sem ele, "Fechadas" (agregado) e "Fechada" (status) ficam coladas
|
|
321
|
+
// e parecem duplicata.
|
|
322
|
+
const firstConcrete = typeof option.value === 'number' && typeof statusOptions[i - 1]?.value !== 'number';
|
|
323
|
+
return (_jsxs(Box, { flexDirection: "column", children: [firstConcrete ? (_jsx(Text, { color: theme.border, children: ` ${'─'.repeat(18)}` })) : null, _jsxs(Box, { children: [_jsxs(Text, { color: selected ? theme.primary : theme.muted, children: [selected ? symbols.pointer : ' ', ' '] }), _jsx(Text, { color: statusFilterColor(theme, option.value, option.label), children: option.label }), current ? _jsxs(Text, { color: theme.muted, children: [" ", symbols.tick, " atual"] }) : null] })] }, String(option.value)));
|
|
324
|
+
}), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), " escolhe", ' ', glyphs.middleDot, " ", _jsx(Text, { color: theme.accent, children: "Enter" }), " aplica ", glyphs.middleDot, ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), " cancela"] }) })] })) : 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: statusFilterColor(theme, statusFilter, statusFilterLabel(statusOptions, statusFilter)), children: [' ', "[", statusFilterLabel(statusOptions, statusFilter), "]"] })] }), searchState.status === 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Digite para buscar. Esc fecha a busca (o filtro de status \u00E9 o f com a busca fechada)." }) })) : 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, searchState.items.length === 0 ? (_jsx(Text, { color: theme.muted, children: "nenhuma issue encontrada" })) : (searchState.items.map((item, index) => (_jsx(SearchResultRow, { item: item, selected: index === searchIndex }, item.id))))] })) : 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
325
|
// Fix do review #120: estado NEUTRO (abandono consciente do re-login,
|
|
198
326
|
// 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' ? (
|
|
327
|
+
_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: theme.muted, children: state.message }) })) : null, state.status === 'loaded' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.issues.slice(listWindow_.start, listWindow_.end).map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: listWindow_.start + index === selectedIndex }, issue.id))), state.issues.length > listWindow_.end - listWindow_.start ? (_jsxs(Text, { color: theme.muted, children: [` ${listWindow_.start + 1}-${listWindow_.end} de ${state.issues.length}`, listWindow_.start > 0 ? ` ${glyphs.arrowUp}` : '', listWindow_.end < state.issues.length ? ` ${glyphs.arrowDown}` : ''] })) : null] })) : 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: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), " navega os resultados,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), " abre. O filtro \u00E9 o", ' ', _jsx(Text, { color: theme.accent, children: "f" }), " com a busca fechada."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "navega,", ' ', _jsx(Text, { color: theme.accent, children: `${glyphs.arrowLeft}/${glyphs.arrowRight}` }), ' ', "p\u00E1gina,", ' ', _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
328
|
}
|