@mosaicoo/kanban 0.1.0 → 0.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.
|
@@ -0,0 +1,2570 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, Injectable, inject, ChangeDetectionStrategy, Component, input, output, effect, untracked } from '@angular/core';
|
|
3
|
+
import * as i7 from '@angular/material/button';
|
|
4
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
5
|
+
import * as i1$1 from '@angular/material/tabs';
|
|
6
|
+
import { MatTabsModule } from '@angular/material/tabs';
|
|
7
|
+
import { MosKanbanBoard } from '@mosaicoo/kanban/ui';
|
|
8
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
9
|
+
import * as i1 from '@angular/forms';
|
|
10
|
+
import { FormsModule } from '@angular/forms';
|
|
11
|
+
import * as i6 from '@angular/material/checkbox';
|
|
12
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
13
|
+
import * as i2 from '@angular/material/form-field';
|
|
14
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
15
|
+
import * as i3 from '@angular/material/input';
|
|
16
|
+
import { MatInputModule } from '@angular/material/input';
|
|
17
|
+
import * as i4 from '@angular/material/select';
|
|
18
|
+
import { MatSelectModule } from '@angular/material/select';
|
|
19
|
+
import * as i5 from '@angular/material/slide-toggle';
|
|
20
|
+
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
21
|
+
import { validateConfig, createBoard } from '@mosaicoo/kanban/core';
|
|
22
|
+
import * as i7$1 from '@angular/material/expansion';
|
|
23
|
+
import { MatExpansionModule } from '@angular/material/expansion';
|
|
24
|
+
|
|
25
|
+
/** Espelho de styles/kanban-tokens.css (defaults de fábrica). */
|
|
26
|
+
const TOKEN_GROUPS = [
|
|
27
|
+
{
|
|
28
|
+
label: 'Espaçamento',
|
|
29
|
+
tokens: [
|
|
30
|
+
{ token: '--mkb-column-gap', label: 'Entre colunas', kind: 'text', default: '12px' },
|
|
31
|
+
{ token: '--mkb-lane-gap', label: 'Entre lanes', kind: 'text', default: '12px' },
|
|
32
|
+
{ token: '--mkb-card-gap', label: 'Entre cartões', kind: 'text', default: '8px' },
|
|
33
|
+
{ token: '--mkb-entry-gap', label: 'Entre itens do cartão', kind: 'text', default: '4px' },
|
|
34
|
+
{ token: '--mkb-block-gap', label: 'Entre blocos do cartão', kind: 'text', default: '8px' },
|
|
35
|
+
{ token: '--mkb-fact-gap', label: 'Entre fatos (facts)', kind: 'text', default: '10px' },
|
|
36
|
+
{ token: '--mkb-card-padding', label: 'Interno do cartão', kind: 'text', default: '10px 12px' },
|
|
37
|
+
{ token: '--mkb-cell-padding', label: 'Interno da célula', kind: 'text', default: '4px 8px 10px' },
|
|
38
|
+
{ token: '--mkb-header-padding', label: 'Interno dos cabeçalhos', kind: 'text', default: '10px 12px' },
|
|
39
|
+
{ token: '--mkb-column-width', label: 'Largura da coluna', kind: 'text', default: 'clamp(220px, 26vw, 320px)' },
|
|
40
|
+
{ token: '--mkb-grid-column-width', label: 'Largura da coluna (grade de lanes)', kind: 'text', default: 'var(--mkb-column-width)' },
|
|
41
|
+
{ token: '--mkb-lane-label-width', label: 'Largura do rótulo da lane', kind: 'text', default: '140px' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Cores',
|
|
46
|
+
tokens: [
|
|
47
|
+
{ token: '--mkb-surface', label: 'Fundo do board', kind: 'color', default: '#f6f5f8' },
|
|
48
|
+
{ token: '--mkb-column-bg', label: 'Fundo da coluna', kind: 'color', default: '#efedf3' },
|
|
49
|
+
{ token: '--mkb-card-bg', label: 'Fundo do cartão', kind: 'color', default: '#ffffff' },
|
|
50
|
+
{ token: '--mkb-border', label: 'Borda', kind: 'color', default: '#e3e0ea' },
|
|
51
|
+
{ token: '--mkb-border-strong', label: 'Borda forte', kind: 'color', default: '#c9c4d4' },
|
|
52
|
+
{ token: '--mkb-foreground', label: 'Texto', kind: 'color', default: '#241f33' },
|
|
53
|
+
{ token: '--mkb-muted', label: 'Texto secundário', kind: 'color', default: '#6f6a7c' },
|
|
54
|
+
{ token: '--mkb-accent', label: 'Realce (accent)', kind: 'color', default: '#5e35b1' },
|
|
55
|
+
{ token: '--mkb-accent-contrast', label: 'Contraste do realce', kind: 'color', default: '#ffffff' },
|
|
56
|
+
{ token: '--mkb-danger', label: 'Perigo', kind: 'color', default: '#b3261e' },
|
|
57
|
+
{ token: '--mkb-warning', label: 'Alerta', kind: 'color', default: '#9a6b00' },
|
|
58
|
+
{ token: '--mkb-success', label: 'Sucesso', kind: 'color', default: '#1b6e3c' },
|
|
59
|
+
{ token: '--mkb-info', label: 'Informação', kind: 'color', default: '#2264c4' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Forma e movimento',
|
|
64
|
+
tokens: [
|
|
65
|
+
{ token: '--mkb-radius', label: 'Raio (containers)', kind: 'text', default: '10px' },
|
|
66
|
+
{ token: '--mkb-radius-sm', label: 'Raio (cartões/controles)', kind: 'text', default: '6px' },
|
|
67
|
+
{ token: '--mkb-dur', label: 'Duração (hover/foco)', kind: 'text', default: '150ms' },
|
|
68
|
+
{ token: '--mkb-dur-move', label: 'Duração (movimento/settle)', kind: 'text', default: '180ms' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
const COMMON = [
|
|
73
|
+
{ key: 'label', label: 'Rótulo (label)', kind: 'text' },
|
|
74
|
+
{ key: 'class', label: 'Classes CSS (separadas por espaço)', kind: 'csv' },
|
|
75
|
+
];
|
|
76
|
+
const FIELD = {
|
|
77
|
+
key: 'field',
|
|
78
|
+
label: 'Campo do item',
|
|
79
|
+
kind: 'text',
|
|
80
|
+
hint: 'Nome do campo lido no card (ex.: titulo)',
|
|
81
|
+
};
|
|
82
|
+
const TONES = ['neutral', 'info', 'success', 'warning', 'danger'];
|
|
83
|
+
/** Descritores dos campos editáveis de cada tipo de bloco. */
|
|
84
|
+
const BLOCK_FIELDS = {
|
|
85
|
+
text: [
|
|
86
|
+
FIELD,
|
|
87
|
+
{ key: 'variant', label: 'Variante', kind: 'select', options: ['', 'title', 'subtitle', 'body', 'caption', 'highlight'] },
|
|
88
|
+
{ key: 'prefix', label: 'Prefixo', kind: 'text' },
|
|
89
|
+
{ key: 'suffix', label: 'Sufixo', kind: 'text' },
|
|
90
|
+
{ key: 'format', label: 'Formatador (format:*)', kind: 'text', hint: 'Nome no registry do host' },
|
|
91
|
+
{ key: 'clamp', label: 'Máx. de linhas', kind: 'number' },
|
|
92
|
+
...COMMON,
|
|
93
|
+
],
|
|
94
|
+
number: [
|
|
95
|
+
FIELD,
|
|
96
|
+
{ key: 'variant', label: 'Variante', kind: 'select', options: ['', 'body', 'highlight'] },
|
|
97
|
+
{ key: 'style', label: 'Estilo', kind: 'select', options: ['', 'decimal', 'currency', 'percent'] },
|
|
98
|
+
{ key: 'currency', label: 'Moeda (ISO)', kind: 'text', hint: 'ex.: BRL' },
|
|
99
|
+
{ key: 'decimals', label: 'Casas decimais', kind: 'number' },
|
|
100
|
+
{ key: 'prefix', label: 'Prefixo', kind: 'text' },
|
|
101
|
+
{ key: 'suffix', label: 'Sufixo', kind: 'text' },
|
|
102
|
+
...COMMON,
|
|
103
|
+
],
|
|
104
|
+
date: [
|
|
105
|
+
FIELD,
|
|
106
|
+
{ key: 'dateStyle', label: 'Formato', kind: 'select', options: ['', 'short', 'medium', 'long', 'full'] },
|
|
107
|
+
{ key: 'relative', label: 'Relativo (há 2 dias)', kind: 'toggle' },
|
|
108
|
+
...COMMON,
|
|
109
|
+
],
|
|
110
|
+
time: [
|
|
111
|
+
FIELD,
|
|
112
|
+
{ key: 'timeStyle', label: 'Formato', kind: 'select', options: ['', 'short', 'medium'] },
|
|
113
|
+
...COMMON,
|
|
114
|
+
],
|
|
115
|
+
datetime: [
|
|
116
|
+
FIELD,
|
|
117
|
+
{ key: 'dateStyle', label: 'Formato (data)', kind: 'select', options: ['', 'short', 'medium', 'long', 'full'] },
|
|
118
|
+
{ key: 'timeStyle', label: 'Formato (hora)', kind: 'select', options: ['', 'short', 'medium'] },
|
|
119
|
+
{ key: 'relative', label: 'Relativo', kind: 'toggle' },
|
|
120
|
+
...COMMON,
|
|
121
|
+
],
|
|
122
|
+
image: [
|
|
123
|
+
FIELD,
|
|
124
|
+
{ key: 'alt', label: 'Texto alternativo', kind: 'text' },
|
|
125
|
+
{ key: 'rounded', label: 'Arredondada', kind: 'toggle' },
|
|
126
|
+
...COMMON,
|
|
127
|
+
],
|
|
128
|
+
tag: [
|
|
129
|
+
FIELD,
|
|
130
|
+
{ key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
|
|
131
|
+
{ key: 'map', label: 'Mapa valor → estilo (JSON)', kind: 'json', hint: '{"ok": {"label": "OK", "tone": "success", "icon": "check"}}' },
|
|
132
|
+
...COMMON,
|
|
133
|
+
],
|
|
134
|
+
badge: [
|
|
135
|
+
FIELD,
|
|
136
|
+
{ key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
|
|
137
|
+
{ key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
|
|
138
|
+
{ key: 'showText', label: 'Mostrar texto', kind: 'toggle' },
|
|
139
|
+
{ key: 'map', label: 'Mapa valor → estilo (JSON)', kind: 'json' },
|
|
140
|
+
...COMMON,
|
|
141
|
+
],
|
|
142
|
+
icon: [
|
|
143
|
+
{ key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
|
|
144
|
+
{ key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
|
|
145
|
+
...COMMON,
|
|
146
|
+
],
|
|
147
|
+
avatar: [
|
|
148
|
+
{ key: 'nameField', label: 'Campo do nome', kind: 'text' },
|
|
149
|
+
{ key: 'photoField', label: 'Campo da foto (URL)', kind: 'text' },
|
|
150
|
+
...COMMON,
|
|
151
|
+
],
|
|
152
|
+
facts: [{ key: 'items', label: 'Fatos', kind: 'facts' }, ...COMMON],
|
|
153
|
+
items: [
|
|
154
|
+
FIELD,
|
|
155
|
+
{ key: 'labelField', label: 'Campo do rótulo', kind: 'text', hint: 'default: label' },
|
|
156
|
+
{ key: 'checkField', label: 'Campo de conclusão', kind: 'text', hint: 'ex.: done' },
|
|
157
|
+
{ key: 'capabilities', label: 'Capacidades', kind: 'capabilities' },
|
|
158
|
+
{ key: 'actions', label: 'Ações por item', kind: 'card-actions' },
|
|
159
|
+
{ key: 'showProgress', label: 'Mostrar progresso (N/M)', kind: 'toggle' },
|
|
160
|
+
{ key: 'maxVisible', label: 'Máx. visíveis', kind: 'number' },
|
|
161
|
+
...COMMON,
|
|
162
|
+
],
|
|
163
|
+
alert: [
|
|
164
|
+
FIELD,
|
|
165
|
+
{ key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
|
|
166
|
+
{ key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
|
|
167
|
+
...COMMON,
|
|
168
|
+
],
|
|
169
|
+
progress: [
|
|
170
|
+
FIELD,
|
|
171
|
+
{ key: 'max', label: 'Máximo', kind: 'number' },
|
|
172
|
+
{ key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
|
|
173
|
+
...COMMON,
|
|
174
|
+
],
|
|
175
|
+
divider: [...COMMON],
|
|
176
|
+
actions: [{ key: 'items', label: 'Ações', kind: 'card-actions' }, ...COMMON],
|
|
177
|
+
row: [
|
|
178
|
+
{ key: 'align', label: 'Alinhamento', kind: 'select', options: ['', 'start', 'center', 'end', 'between'] },
|
|
179
|
+
{ key: 'wrap', label: 'Quebrar linha', kind: 'toggle' },
|
|
180
|
+
...COMMON,
|
|
181
|
+
],
|
|
182
|
+
custom: [
|
|
183
|
+
{ key: 'renderer', label: 'Renderer (renderer:*)', kind: 'text', hint: 'Nome no registry do host' },
|
|
184
|
+
...COMMON,
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
/** Tipos de bloco oferecidos no "adicionar bloco" (ordem de exibição). */
|
|
188
|
+
const BLOCK_TYPE_LABELS = [
|
|
189
|
+
{ type: 'text', label: 'Texto' },
|
|
190
|
+
{ type: 'number', label: 'Número' },
|
|
191
|
+
{ type: 'date', label: 'Data' },
|
|
192
|
+
{ type: 'time', label: 'Hora' },
|
|
193
|
+
{ type: 'datetime', label: 'Data e hora' },
|
|
194
|
+
{ type: 'image', label: 'Imagem' },
|
|
195
|
+
{ type: 'tag', label: 'Tag' },
|
|
196
|
+
{ type: 'badge', label: 'Selo (badge)' },
|
|
197
|
+
{ type: 'icon', label: 'Ícone' },
|
|
198
|
+
{ type: 'avatar', label: 'Avatar' },
|
|
199
|
+
{ type: 'facts', label: 'Fatos (ícone + valor)' },
|
|
200
|
+
{ type: 'items', label: 'Itens internos (checklist/ações)' },
|
|
201
|
+
{ type: 'alert', label: 'Alerta' },
|
|
202
|
+
{ type: 'progress', label: 'Progresso' },
|
|
203
|
+
{ type: 'divider', label: 'Divisor' },
|
|
204
|
+
{ type: 'actions', label: 'Ações do cartão' },
|
|
205
|
+
{ type: 'row', label: 'Linha (composição horizontal)' },
|
|
206
|
+
{ type: 'custom', label: 'Custom (renderer do host)' },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const DEFAULT_CONFIG = {
|
|
210
|
+
columns: {
|
|
211
|
+
groups: [
|
|
212
|
+
{ id: 'todo', title: 'A Fazer' },
|
|
213
|
+
{ id: 'doing', title: 'Fazendo' },
|
|
214
|
+
{ id: 'done', title: 'Feito' },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
card: { blocks: [{ type: 'text', field: 'title', variant: 'title' }] },
|
|
218
|
+
items: [
|
|
219
|
+
{ id: 'item-1', column: 'todo', title: 'Primeiro cartão' },
|
|
220
|
+
{ id: 'item-2', column: 'doing', title: 'Segundo cartão' },
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Estado do Designer — instância POR componente (provider do
|
|
225
|
+
* MosKanbanDesigner; nunca singleton de módulo). Fonte da verdade é um
|
|
226
|
+
* BoardConfig serializável em forma canônica de edição (eixos como objeto);
|
|
227
|
+
* toda mutação passa por `update()` (clone → muta → valida → re-preview).
|
|
228
|
+
*/
|
|
229
|
+
class DesignerStore {
|
|
230
|
+
config = signal(structuredClone(DEFAULT_CONFIG), ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
|
|
231
|
+
theme = signal({}, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
232
|
+
registry;
|
|
233
|
+
/** Validação contínua da config em edição. */
|
|
234
|
+
validation = computed(() => validateConfig(this.config()), ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
|
|
235
|
+
errors = computed(() => this.validation().errors, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
236
|
+
/** Board do preview — recriado a cada config VÁLIDA (a inválida mantém o
|
|
237
|
+
* último preview bom, com os erros listados ao lado). */
|
|
238
|
+
lastGood = null;
|
|
239
|
+
previewBoard = computed(() => {
|
|
240
|
+
const cfg = this.config();
|
|
241
|
+
if (!validateConfig(cfg).valid)
|
|
242
|
+
return this.lastGood;
|
|
243
|
+
try {
|
|
244
|
+
this.lastGood?.destroy();
|
|
245
|
+
this.lastGood = createBoard(structuredClone(cfg), { registry: this.registry });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* estrutural já validado; regra de runtime inesperada mantém o último */
|
|
249
|
+
}
|
|
250
|
+
return this.lastGood;
|
|
251
|
+
}, ...(ngDevMode ? [{ debugName: "previewBoard" }] : /* istanbul ignore next */ []));
|
|
252
|
+
/** String de estilo com os tokens alterados (aplicada no preview). */
|
|
253
|
+
themeStyle = computed(() => Object.entries(this.theme())
|
|
254
|
+
.map(([token, value]) => `${token}: ${value}`)
|
|
255
|
+
.join('; '), ...(ngDevMode ? [{ debugName: "themeStyle" }] : /* istanbul ignore next */ []));
|
|
256
|
+
// ---- mutações ---------------------------------------------------------------
|
|
257
|
+
/** Toda mutação: clona, aplica, publica (imutável p/ OnPush). */
|
|
258
|
+
update(mutator) {
|
|
259
|
+
const draft = structuredClone(this.config());
|
|
260
|
+
mutator(draft);
|
|
261
|
+
this.config.set(draft);
|
|
262
|
+
}
|
|
263
|
+
load(config, theme) {
|
|
264
|
+
this.config.set(this.canonicalize(structuredClone(config)));
|
|
265
|
+
if (theme)
|
|
266
|
+
this.theme.set({ ...theme });
|
|
267
|
+
}
|
|
268
|
+
setToken(token, value) {
|
|
269
|
+
const def = TOKEN_GROUPS.flatMap((g) => g.tokens).find((t) => t.token === token);
|
|
270
|
+
const next = { ...this.theme() };
|
|
271
|
+
if (!value || value === def?.default)
|
|
272
|
+
delete next[token];
|
|
273
|
+
else
|
|
274
|
+
next[token] = value;
|
|
275
|
+
this.theme.set(next);
|
|
276
|
+
}
|
|
277
|
+
resetTheme() {
|
|
278
|
+
this.theme.set({});
|
|
279
|
+
}
|
|
280
|
+
// ---- eixos em forma canônica --------------------------------------------------
|
|
281
|
+
/** Eixo de colunas SEMPRE como objeto (açúcar de array normalizado). */
|
|
282
|
+
columnsAxis() {
|
|
283
|
+
return this.axisOf(this.config().columns);
|
|
284
|
+
}
|
|
285
|
+
lanesAxis() {
|
|
286
|
+
const lanes = this.config().lanes;
|
|
287
|
+
return lanes ? this.axisOf(lanes) : null;
|
|
288
|
+
}
|
|
289
|
+
axisOf(axis) {
|
|
290
|
+
if (Array.isArray(axis)) {
|
|
291
|
+
return {
|
|
292
|
+
groups: axis.map((g) => (typeof g === 'string' ? { id: g } : { ...g })),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
...axis,
|
|
297
|
+
groups: axis.groups?.map((g) => (typeof g === 'string' ? { id: g } : { ...g })),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Importa qualquer config para a forma canônica de edição. */
|
|
301
|
+
canonicalize(config) {
|
|
302
|
+
config.columns = this.axisOf(config.columns);
|
|
303
|
+
if (config.lanes)
|
|
304
|
+
config.lanes = this.axisOf(config.lanes);
|
|
305
|
+
return config;
|
|
306
|
+
}
|
|
307
|
+
// ---- import / export ----------------------------------------------------------
|
|
308
|
+
/** Aceita BoardConfig puro OU KanbanDesignFile; retorna erros sem aplicar
|
|
309
|
+
* quando a config é inválida. */
|
|
310
|
+
import(json) {
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(json);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
return { ok: false, errors: [{ path: '', message: `JSON inválido: ${e.message}` }] };
|
|
317
|
+
}
|
|
318
|
+
const asFile = parsed;
|
|
319
|
+
const isDesignFile = !!asFile.config;
|
|
320
|
+
const config = (asFile.config ?? parsed);
|
|
321
|
+
const result = validateConfig(config);
|
|
322
|
+
if (!result.valid)
|
|
323
|
+
return { ok: false, errors: result.errors };
|
|
324
|
+
// Design file descreve o design INTEIRO: sem `theme` = tema no padrão, o
|
|
325
|
+
// que ZERA os tokens em edição. BoardConfig puro só traz estrutura, então
|
|
326
|
+
// preserva o tema atual.
|
|
327
|
+
this.load(config, isDesignFile ? (asFile.theme ?? {}) : undefined);
|
|
328
|
+
return { ok: true, errors: [] };
|
|
329
|
+
}
|
|
330
|
+
exportConfigJson() {
|
|
331
|
+
return JSON.stringify(this.config(), null, 2);
|
|
332
|
+
}
|
|
333
|
+
exportDesignJson() {
|
|
334
|
+
const file = { version: 1, config: this.config() };
|
|
335
|
+
if (Object.keys(this.theme()).length)
|
|
336
|
+
file.theme = this.theme();
|
|
337
|
+
return JSON.stringify(file, null, 2);
|
|
338
|
+
}
|
|
339
|
+
/** Snippet CSS só com os tokens alterados (nada = string vazia). */
|
|
340
|
+
exportThemeCss() {
|
|
341
|
+
const entries = Object.entries(this.theme());
|
|
342
|
+
if (!entries.length)
|
|
343
|
+
return '';
|
|
344
|
+
const lines = entries.map(([token, value]) => ` ${token}: ${value};`);
|
|
345
|
+
return `/* Tema @mosaicoo/kanban — gerado pelo Designer */\n:root {\n${lines.join('\n')}\n}\n`;
|
|
346
|
+
}
|
|
347
|
+
// ---- rascunho local -------------------------------------------------------------
|
|
348
|
+
saveDraft(storageKey) {
|
|
349
|
+
try {
|
|
350
|
+
localStorage.setItem(storageKey, this.exportDesignJson());
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* storage cheio/indisponível: rascunho é conveniência, não requisito */
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
loadDraft(storageKey) {
|
|
357
|
+
try {
|
|
358
|
+
const raw = localStorage.getItem(storageKey);
|
|
359
|
+
return raw ? this.import(raw).ok : false;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
clearDraft(storageKey) {
|
|
366
|
+
try {
|
|
367
|
+
localStorage.removeItem(storageKey);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
/* idem */
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
destroy() {
|
|
374
|
+
this.lastGood?.destroy();
|
|
375
|
+
this.lastGood = null;
|
|
376
|
+
}
|
|
377
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
378
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore });
|
|
379
|
+
}
|
|
380
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore, decorators: [{
|
|
381
|
+
type: Injectable
|
|
382
|
+
}] });
|
|
383
|
+
|
|
384
|
+
const TYPE_LABEL = new Map(BLOCK_TYPE_LABELS.map((t) => [t.type, t.label]));
|
|
385
|
+
/** Bloco novo mínimo VÁLIDO por tipo (campos obrigatórios preenchidos). */
|
|
386
|
+
function blankBlock(type) {
|
|
387
|
+
switch (type) {
|
|
388
|
+
case 'icon':
|
|
389
|
+
return { type, icon: 'star' };
|
|
390
|
+
case 'avatar':
|
|
391
|
+
return { type, nameField: 'owner' };
|
|
392
|
+
case 'facts':
|
|
393
|
+
return { type, items: [{ field: 'campo' }] };
|
|
394
|
+
case 'actions':
|
|
395
|
+
return { type, items: [{ id: 'acao-1', label: 'Ação' }] };
|
|
396
|
+
case 'divider':
|
|
397
|
+
return { type };
|
|
398
|
+
case 'row':
|
|
399
|
+
return { type, blocks: [] };
|
|
400
|
+
case 'custom':
|
|
401
|
+
return { type, renderer: 'renderer:custom' };
|
|
402
|
+
default:
|
|
403
|
+
return { type, field: 'campo' };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Editor do CARTÃO: árvore de blocos (com `row` de um nível) + painel de
|
|
408
|
+
* propriedades GERADO a partir de BLOCK_FIELDS — novos campos entram na
|
|
409
|
+
* tabela, não em template escrito à mão.
|
|
410
|
+
*/
|
|
411
|
+
class MkbCardEditor {
|
|
412
|
+
store = inject(DesignerStore);
|
|
413
|
+
config = this.store.config;
|
|
414
|
+
blockTypes = BLOCK_TYPE_LABELS;
|
|
415
|
+
tones = ['', 'neutral', 'info', 'success', 'warning', 'danger'];
|
|
416
|
+
allCapabilities = ['check', 'reorder', 'move-across', 'action'];
|
|
417
|
+
newType = 'text';
|
|
418
|
+
selectedPath = signal(null, ...(ngDevMode ? [{ debugName: "selectedPath" }] : /* istanbul ignore next */ []));
|
|
419
|
+
jsonError = signal(null, ...(ngDevMode ? [{ debugName: "jsonError" }] : /* istanbul ignore next */ []));
|
|
420
|
+
selectedBlock = computed(() => {
|
|
421
|
+
const path = this.selectedPath();
|
|
422
|
+
return path ? this.blockAt(this.config().card?.blocks ?? [], path) : null;
|
|
423
|
+
}, ...(ngDevMode ? [{ debugName: "selectedBlock" }] : /* istanbul ignore next */ []));
|
|
424
|
+
blocks() {
|
|
425
|
+
return this.config().card?.blocks ?? [];
|
|
426
|
+
}
|
|
427
|
+
rowChildren(block) {
|
|
428
|
+
return block.type === 'row' ? block.blocks : [];
|
|
429
|
+
}
|
|
430
|
+
typeLabel(type) {
|
|
431
|
+
return TYPE_LABEL.get(type) ?? type;
|
|
432
|
+
}
|
|
433
|
+
summary(block) {
|
|
434
|
+
const b = block;
|
|
435
|
+
return String(b['field'] ?? b['icon'] ?? b['renderer'] ?? b['nameField'] ?? '');
|
|
436
|
+
}
|
|
437
|
+
fieldsFor(type) {
|
|
438
|
+
return BLOCK_FIELDS[type];
|
|
439
|
+
}
|
|
440
|
+
// ---- seleção / árvore --------------------------------------------------------
|
|
441
|
+
select(path) {
|
|
442
|
+
this.selectedPath.set(path);
|
|
443
|
+
this.jsonError.set(null);
|
|
444
|
+
}
|
|
445
|
+
isSelected(path) {
|
|
446
|
+
const sel = this.selectedPath();
|
|
447
|
+
return !!sel && sel.length === path.length && sel.every((v, i) => v === path[i]);
|
|
448
|
+
}
|
|
449
|
+
selectedIsRow() {
|
|
450
|
+
return this.selectedBlock()?.type === 'row';
|
|
451
|
+
}
|
|
452
|
+
blockAt(blocks, path) {
|
|
453
|
+
const top = blocks[path[0]];
|
|
454
|
+
if (!top)
|
|
455
|
+
return null;
|
|
456
|
+
if (path.length === 1)
|
|
457
|
+
return top;
|
|
458
|
+
return top.type === 'row' ? (top.blocks[path[1]] ?? null) : null;
|
|
459
|
+
}
|
|
460
|
+
/** Lista IRMÃ do caminho (raiz ou blocos de uma row) dentro do draft. */
|
|
461
|
+
siblingsAt(draft, path) {
|
|
462
|
+
const blocks = draft.card?.blocks;
|
|
463
|
+
if (!blocks)
|
|
464
|
+
return null;
|
|
465
|
+
if (path.length === 1)
|
|
466
|
+
return blocks;
|
|
467
|
+
const parent = blocks[path[0]];
|
|
468
|
+
return parent?.type === 'row' ? parent.blocks : null;
|
|
469
|
+
}
|
|
470
|
+
add(intoSelectedRow) {
|
|
471
|
+
const block = blankBlock(this.newType);
|
|
472
|
+
const rowPath = intoSelectedRow ? this.selectedPath() : null;
|
|
473
|
+
this.store.update((d) => {
|
|
474
|
+
d.card ??= { blocks: [] };
|
|
475
|
+
if (rowPath) {
|
|
476
|
+
const row = this.blockAt(d.card.blocks, rowPath);
|
|
477
|
+
if (row?.type === 'row') {
|
|
478
|
+
row.blocks.push(block);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
d.card.blocks.push(block);
|
|
483
|
+
});
|
|
484
|
+
if (rowPath) {
|
|
485
|
+
const row = this.blockAt(this.blocks(), rowPath);
|
|
486
|
+
this.select([...rowPath, (row?.type === 'row' ? row.blocks.length : 1) - 1]);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
this.select([this.blocks().length - 1]);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
remove(path) {
|
|
493
|
+
this.store.update((d) => {
|
|
494
|
+
this.siblingsAt(d, path)?.splice(path[path.length - 1], 1);
|
|
495
|
+
});
|
|
496
|
+
this.selectedPath.set(null);
|
|
497
|
+
}
|
|
498
|
+
move(path, delta) {
|
|
499
|
+
const index = path[path.length - 1];
|
|
500
|
+
const target = index + delta;
|
|
501
|
+
let moved = false;
|
|
502
|
+
this.store.update((d) => {
|
|
503
|
+
const siblings = this.siblingsAt(d, path);
|
|
504
|
+
if (!siblings || target < 0 || target >= siblings.length)
|
|
505
|
+
return;
|
|
506
|
+
const [b] = siblings.splice(index, 1);
|
|
507
|
+
siblings.splice(target, 0, b);
|
|
508
|
+
moved = true;
|
|
509
|
+
});
|
|
510
|
+
if (moved && this.isSelected(path)) {
|
|
511
|
+
this.select([...path.slice(0, -1), target]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ---- leitura de propriedades ----------------------------------------------------
|
|
515
|
+
str(block, key) {
|
|
516
|
+
const v = block[key];
|
|
517
|
+
return typeof v === 'string' ? v : '';
|
|
518
|
+
}
|
|
519
|
+
num(block, key) {
|
|
520
|
+
const v = block[key];
|
|
521
|
+
return typeof v === 'number' ? v : null;
|
|
522
|
+
}
|
|
523
|
+
bool(block, key) {
|
|
524
|
+
return block[key] === true;
|
|
525
|
+
}
|
|
526
|
+
csv(block, key) {
|
|
527
|
+
const v = block[key];
|
|
528
|
+
return Array.isArray(v) ? v.join(' ') : typeof v === 'string' ? v : '';
|
|
529
|
+
}
|
|
530
|
+
json(block, key) {
|
|
531
|
+
const v = block[key];
|
|
532
|
+
return v === undefined ? '' : JSON.stringify(v, null, 2);
|
|
533
|
+
}
|
|
534
|
+
facts(block) {
|
|
535
|
+
return block.type === 'facts' ? block.items : [];
|
|
536
|
+
}
|
|
537
|
+
actions(block, key) {
|
|
538
|
+
const v = block[key];
|
|
539
|
+
return Array.isArray(v) ? v : [];
|
|
540
|
+
}
|
|
541
|
+
hasCapability(block, cap) {
|
|
542
|
+
return block.type === 'items' && !!block.capabilities?.includes(cap);
|
|
543
|
+
}
|
|
544
|
+
whenColumns(action) {
|
|
545
|
+
const col = action.when?.column;
|
|
546
|
+
return Array.isArray(col) ? col.join(', ') : (col ?? '');
|
|
547
|
+
}
|
|
548
|
+
// ---- escrita de propriedades ------------------------------------------------------
|
|
549
|
+
/** Escreve no bloco selecionado; undefined REMOVE a chave. */
|
|
550
|
+
setProp(key, value) {
|
|
551
|
+
const path = this.selectedPath();
|
|
552
|
+
if (!path)
|
|
553
|
+
return;
|
|
554
|
+
this.store.update((d) => {
|
|
555
|
+
const block = this.blockAt(d.card?.blocks ?? [], path);
|
|
556
|
+
if (!block)
|
|
557
|
+
return;
|
|
558
|
+
if (value === undefined)
|
|
559
|
+
delete block[key];
|
|
560
|
+
else
|
|
561
|
+
block[key] = value;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
setCsv(key, raw) {
|
|
565
|
+
const list = raw.split(/[\s,]+/).filter(Boolean);
|
|
566
|
+
this.setProp(key, list.length ? list : undefined);
|
|
567
|
+
}
|
|
568
|
+
setJson(key, event) {
|
|
569
|
+
const raw = event.target.value.trim();
|
|
570
|
+
if (!raw) {
|
|
571
|
+
this.jsonError.set(null);
|
|
572
|
+
this.setProp(key, undefined);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
this.setProp(key, JSON.parse(raw));
|
|
577
|
+
this.jsonError.set(null);
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
this.jsonError.set(`JSON inválido: ${e.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
toggleCapability(cap, enabled) {
|
|
584
|
+
const block = this.selectedBlock();
|
|
585
|
+
if (block?.type !== 'items')
|
|
586
|
+
return;
|
|
587
|
+
const caps = new Set(block.capabilities ?? []);
|
|
588
|
+
if (enabled)
|
|
589
|
+
caps.add(cap);
|
|
590
|
+
else
|
|
591
|
+
caps.delete(cap);
|
|
592
|
+
this.setProp('capabilities', caps.size ? [...caps] : undefined);
|
|
593
|
+
}
|
|
594
|
+
// ---- listas internas (facts / actions) --------------------------------------------
|
|
595
|
+
addFact(key) {
|
|
596
|
+
this.mutateList(key, (list) => list.push({ field: 'campo' }));
|
|
597
|
+
}
|
|
598
|
+
setFact(index, prop, value) {
|
|
599
|
+
this.mutateList('items', (list) => {
|
|
600
|
+
const fact = list[index];
|
|
601
|
+
if (!fact)
|
|
602
|
+
return;
|
|
603
|
+
if (value === undefined)
|
|
604
|
+
delete fact[prop];
|
|
605
|
+
else
|
|
606
|
+
fact[prop] = value;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
addAction(key) {
|
|
610
|
+
this.mutateList(key, (list) => list.push({ id: `acao-${list.length + 1}`, label: 'Ação' }));
|
|
611
|
+
}
|
|
612
|
+
setAction(key, index, prop, value) {
|
|
613
|
+
this.mutateList(key, (list) => {
|
|
614
|
+
const action = list[index];
|
|
615
|
+
if (!action)
|
|
616
|
+
return;
|
|
617
|
+
if (value === undefined)
|
|
618
|
+
delete action[prop];
|
|
619
|
+
else
|
|
620
|
+
action[prop] = value;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
setActionWhen(key, index, raw) {
|
|
624
|
+
const columns = raw.split(/[\s,]+/).filter(Boolean);
|
|
625
|
+
this.mutateList(key, (list) => {
|
|
626
|
+
const action = list[index];
|
|
627
|
+
if (!action)
|
|
628
|
+
return;
|
|
629
|
+
if (!columns.length) {
|
|
630
|
+
if (action.when)
|
|
631
|
+
delete action.when.column;
|
|
632
|
+
if (action.when && !action.when.lane)
|
|
633
|
+
delete action.when;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
action.when = { ...(action.when ?? {}), column: columns.length === 1 ? columns[0] : columns };
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
removeListItem(key, index) {
|
|
641
|
+
this.mutateList(key, (list) => list.splice(index, 1));
|
|
642
|
+
}
|
|
643
|
+
mutateList(key, mutator) {
|
|
644
|
+
const path = this.selectedPath();
|
|
645
|
+
if (!path)
|
|
646
|
+
return;
|
|
647
|
+
this.store.update((d) => {
|
|
648
|
+
const block = this.blockAt(d.card?.blocks ?? [], path);
|
|
649
|
+
if (!block)
|
|
650
|
+
return;
|
|
651
|
+
block[key] ??= [];
|
|
652
|
+
mutator(block[key]);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// ---- geral -------------------------------------------------------------------------
|
|
656
|
+
setDragHandle(value) {
|
|
657
|
+
this.store.update((d) => {
|
|
658
|
+
d.card ??= { blocks: [] };
|
|
659
|
+
if (value)
|
|
660
|
+
d.card.dragHandle = true;
|
|
661
|
+
else
|
|
662
|
+
delete d.card.dragHandle;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbCardEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
666
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbCardEditor, isStandalone: true, selector: "mkb-card-editor", ngImport: i0, template: `
|
|
667
|
+
<div class="mkbd-row">
|
|
668
|
+
<mat-slide-toggle
|
|
669
|
+
[ngModel]="config().card?.dragHandle ?? false"
|
|
670
|
+
(ngModelChange)="setDragHandle($event)"
|
|
671
|
+
>
|
|
672
|
+
Arrastar só pela alça ⠿
|
|
673
|
+
</mat-slide-toggle>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<h3 class="mkbd-h">Blocos do cartão</h3>
|
|
677
|
+
<div class="mkbd-tree" role="tree" aria-label="Blocos do cartão">
|
|
678
|
+
@for (block of blocks(); track $index; let i = $index) {
|
|
679
|
+
<ng-container
|
|
680
|
+
*ngTemplateOutlet="blockRow; context: { block, path: [i], siblings: blocks().length }"
|
|
681
|
+
/>
|
|
682
|
+
@if (block.type === 'row') {
|
|
683
|
+
@for (child of rowChildren(block); track $index; let j = $index) {
|
|
684
|
+
<ng-container
|
|
685
|
+
*ngTemplateOutlet="
|
|
686
|
+
blockRow;
|
|
687
|
+
context: { block: child, path: [i, j], siblings: rowChildren(block).length }
|
|
688
|
+
"
|
|
689
|
+
/>
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} @empty {
|
|
693
|
+
<p class="mkbd-hint">Nenhum bloco — o cartão renderiza vazio. Adicione abaixo.</p>
|
|
694
|
+
}
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<ng-template #blockRow let-block="block" let-path="path" let-siblings="siblings">
|
|
698
|
+
<div
|
|
699
|
+
class="mkbd-node"
|
|
700
|
+
role="treeitem"
|
|
701
|
+
tabindex="0"
|
|
702
|
+
[class.mkbd-node-child]="path.length > 1"
|
|
703
|
+
[class.mkbd-node-selected]="isSelected(path)"
|
|
704
|
+
[attr.aria-selected]="isSelected(path)"
|
|
705
|
+
(click)="select(path)"
|
|
706
|
+
(keydown.enter)="select(path)"
|
|
707
|
+
>
|
|
708
|
+
<span class="mkbd-node-type">{{ typeLabel(block.type) }}</span>
|
|
709
|
+
<span class="mkbd-node-summary">{{ summary(block) }}</span>
|
|
710
|
+
<span class="mkbd-spacer"></span>
|
|
711
|
+
<button matIconButton type="button" (click)="move(path, -1); $event.stopPropagation()" [disabled]="path[path.length - 1] === 0" aria-label="Subir bloco">↑</button>
|
|
712
|
+
<button matIconButton type="button" (click)="move(path, 1); $event.stopPropagation()" [disabled]="path[path.length - 1] === siblings - 1" aria-label="Descer bloco">↓</button>
|
|
713
|
+
<button matIconButton type="button" (click)="remove(path); $event.stopPropagation()" aria-label="Remover bloco">✕</button>
|
|
714
|
+
</div>
|
|
715
|
+
</ng-template>
|
|
716
|
+
|
|
717
|
+
<div class="mkbd-row mkbd-mt">
|
|
718
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
719
|
+
<mat-label>Tipo do novo bloco</mat-label>
|
|
720
|
+
<mat-select [(ngModel)]="newType">
|
|
721
|
+
@for (t of blockTypes; track t.type) {
|
|
722
|
+
<mat-option [value]="t.type">{{ t.label }}</mat-option>
|
|
723
|
+
}
|
|
724
|
+
</mat-select>
|
|
725
|
+
</mat-form-field>
|
|
726
|
+
<button matButton type="button" (click)="add(false)">+ Adicionar</button>
|
|
727
|
+
@if (selectedIsRow()) {
|
|
728
|
+
<button matButton type="button" (click)="add(true)">+ Adicionar DENTRO da linha</button>
|
|
729
|
+
}
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
@if (selectedBlock(); as block) {
|
|
733
|
+
<h3 class="mkbd-h">Propriedades — {{ typeLabel(block.type) }}</h3>
|
|
734
|
+
<div class="mkbd-props">
|
|
735
|
+
@for (def of fieldsFor(block.type); track def.key) {
|
|
736
|
+
@switch (def.kind) {
|
|
737
|
+
@case ('text') {
|
|
738
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
739
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
740
|
+
<input
|
|
741
|
+
matInput
|
|
742
|
+
[ngModel]="str(block, def.key)"
|
|
743
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
744
|
+
/>
|
|
745
|
+
@if (def.hint) { <mat-hint>{{ def.hint }}</mat-hint> }
|
|
746
|
+
</mat-form-field>
|
|
747
|
+
}
|
|
748
|
+
@case ('number') {
|
|
749
|
+
<mat-form-field appearance="outline" class="mkbd-tiny">
|
|
750
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
751
|
+
<input
|
|
752
|
+
matInput
|
|
753
|
+
type="number"
|
|
754
|
+
[ngModel]="num(block, def.key)"
|
|
755
|
+
(ngModelChange)="setProp(def.key, $event === null || $event === '' ? undefined : +$event)"
|
|
756
|
+
/>
|
|
757
|
+
</mat-form-field>
|
|
758
|
+
}
|
|
759
|
+
@case ('toggle') {
|
|
760
|
+
<mat-slide-toggle
|
|
761
|
+
[ngModel]="bool(block, def.key)"
|
|
762
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
763
|
+
>
|
|
764
|
+
{{ def.label }}
|
|
765
|
+
</mat-slide-toggle>
|
|
766
|
+
}
|
|
767
|
+
@case ('select') {
|
|
768
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
769
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
770
|
+
<mat-select
|
|
771
|
+
[ngModel]="str(block, def.key)"
|
|
772
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
773
|
+
>
|
|
774
|
+
@for (opt of def.options; track opt) {
|
|
775
|
+
<mat-option [value]="opt">{{ opt || '—' }}</mat-option>
|
|
776
|
+
}
|
|
777
|
+
</mat-select>
|
|
778
|
+
</mat-form-field>
|
|
779
|
+
}
|
|
780
|
+
@case ('csv') {
|
|
781
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
782
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
783
|
+
<input
|
|
784
|
+
matInput
|
|
785
|
+
[ngModel]="csv(block, def.key)"
|
|
786
|
+
(ngModelChange)="setCsv(def.key, $event)"
|
|
787
|
+
/>
|
|
788
|
+
</mat-form-field>
|
|
789
|
+
}
|
|
790
|
+
@case ('json') {
|
|
791
|
+
<mat-form-field appearance="outline" class="mkbd-full">
|
|
792
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
793
|
+
<textarea
|
|
794
|
+
matInput
|
|
795
|
+
rows="4"
|
|
796
|
+
[value]="json(block, def.key)"
|
|
797
|
+
(change)="setJson(def.key, $event)"
|
|
798
|
+
></textarea>
|
|
799
|
+
<mat-hint [class.mkbd-error]="jsonError()">
|
|
800
|
+
{{ jsonError() || def.hint || 'Objeto JSON; vazio remove.' }}
|
|
801
|
+
</mat-hint>
|
|
802
|
+
</mat-form-field>
|
|
803
|
+
}
|
|
804
|
+
@case ('capabilities') {
|
|
805
|
+
<div class="mkbd-caps">
|
|
806
|
+
<span class="mkbd-caps-label">{{ def.label }}:</span>
|
|
807
|
+
@for (cap of allCapabilities; track cap) {
|
|
808
|
+
<mat-checkbox
|
|
809
|
+
[ngModel]="hasCapability(block, cap)"
|
|
810
|
+
(ngModelChange)="toggleCapability(cap, $event)"
|
|
811
|
+
>
|
|
812
|
+
{{ cap }}
|
|
813
|
+
</mat-checkbox>
|
|
814
|
+
}
|
|
815
|
+
</div>
|
|
816
|
+
}
|
|
817
|
+
@case ('facts') {
|
|
818
|
+
<div class="mkbd-sub">
|
|
819
|
+
<span class="mkbd-caps-label">{{ def.label }}</span>
|
|
820
|
+
@for (fact of facts(block); track $index; let fi = $index) {
|
|
821
|
+
<div class="mkbd-subrow">
|
|
822
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
823
|
+
<mat-label>Campo</mat-label>
|
|
824
|
+
<input matInput [ngModel]="fact.field" (ngModelChange)="setFact(fi, 'field', $event)" />
|
|
825
|
+
</mat-form-field>
|
|
826
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
827
|
+
<mat-label>Ícone</mat-label>
|
|
828
|
+
<input matInput [ngModel]="fact.icon ?? ''" (ngModelChange)="setFact(fi, 'icon', $event || undefined)" />
|
|
829
|
+
</mat-form-field>
|
|
830
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
831
|
+
<mat-label>Rótulo</mat-label>
|
|
832
|
+
<input matInput [ngModel]="fact.label ?? ''" (ngModelChange)="setFact(fi, 'label', $event || undefined)" />
|
|
833
|
+
</mat-form-field>
|
|
834
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
835
|
+
<mat-label>Prefixo</mat-label>
|
|
836
|
+
<input matInput [ngModel]="fact.prefix ?? ''" (ngModelChange)="setFact(fi, 'prefix', $event || undefined)" />
|
|
837
|
+
</mat-form-field>
|
|
838
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
839
|
+
<mat-label>Sufixo</mat-label>
|
|
840
|
+
<input matInput [ngModel]="fact.suffix ?? ''" (ngModelChange)="setFact(fi, 'suffix', $event || undefined)" />
|
|
841
|
+
</mat-form-field>
|
|
842
|
+
<button matIconButton type="button" (click)="removeListItem(def.key, fi)" aria-label="Remover fato">✕</button>
|
|
843
|
+
</div>
|
|
844
|
+
}
|
|
845
|
+
<button matButton type="button" (click)="addFact(def.key)">+ Fato</button>
|
|
846
|
+
</div>
|
|
847
|
+
}
|
|
848
|
+
@case ('card-actions') {
|
|
849
|
+
<div class="mkbd-sub">
|
|
850
|
+
<span class="mkbd-caps-label">{{ def.label }}</span>
|
|
851
|
+
@for (action of actions(block, def.key); track $index; let ai = $index) {
|
|
852
|
+
<div class="mkbd-subrow">
|
|
853
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
854
|
+
<mat-label>Id</mat-label>
|
|
855
|
+
<input matInput [ngModel]="action.id" (ngModelChange)="setAction(def.key, ai, 'id', $event)" />
|
|
856
|
+
</mat-form-field>
|
|
857
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
858
|
+
<mat-label>Rótulo</mat-label>
|
|
859
|
+
<input matInput [ngModel]="action.label" (ngModelChange)="setAction(def.key, ai, 'label', $event)" />
|
|
860
|
+
</mat-form-field>
|
|
861
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
862
|
+
<mat-label>Ícone</mat-label>
|
|
863
|
+
<input matInput [ngModel]="action.icon ?? ''" (ngModelChange)="setAction(def.key, ai, 'icon', $event || undefined)" />
|
|
864
|
+
</mat-form-field>
|
|
865
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
866
|
+
<mat-label>Tom</mat-label>
|
|
867
|
+
<mat-select [ngModel]="action.tone ?? ''" (ngModelChange)="setAction(def.key, ai, 'tone', $event || undefined)">
|
|
868
|
+
@for (tone of tones; track tone) {
|
|
869
|
+
<mat-option [value]="tone">{{ tone || '—' }}</mat-option>
|
|
870
|
+
}
|
|
871
|
+
</mat-select>
|
|
872
|
+
</mat-form-field>
|
|
873
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
874
|
+
<mat-label>Só nas colunas</mat-label>
|
|
875
|
+
<input
|
|
876
|
+
matInput
|
|
877
|
+
placeholder="col-a, col-b"
|
|
878
|
+
[ngModel]="whenColumns(action)"
|
|
879
|
+
(ngModelChange)="setActionWhen(def.key, ai, $event)"
|
|
880
|
+
/>
|
|
881
|
+
</mat-form-field>
|
|
882
|
+
<button matIconButton type="button" (click)="removeListItem(def.key, ai)" aria-label="Remover ação">✕</button>
|
|
883
|
+
</div>
|
|
884
|
+
}
|
|
885
|
+
<button matButton type="button" (click)="addAction(def.key)">+ Ação</button>
|
|
886
|
+
</div>
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
</div>
|
|
891
|
+
} @else {
|
|
892
|
+
<p class="mkbd-hint mkbd-mt">Selecione um bloco para editar as propriedades.</p>
|
|
893
|
+
}
|
|
894
|
+
`, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:12px}.mkbd-tree{display:flex;flex-direction:column;gap:3px}.mkbd-node{display:flex;align-items:center;gap:8px;padding:3px 10px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;cursor:pointer;background:var(--mkb-card-bg, #fff)}.mkbd-node:focus-visible{outline:2px solid var(--mkb-accent, #5e35b1);outline-offset:1px}.mkbd-node-child{margin-left:26px}.mkbd-node-selected{border-color:var(--mkb-accent, #5e35b1);box-shadow:inset 2px 0 0 var(--mkb-accent, #5e35b1)}.mkbd-node-type{font-size:12px;font-weight:600}.mkbd-node-summary{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mkbd-spacer{flex:1}.mkbd-props{display:flex;flex-wrap:wrap;gap:18px 10px;align-items:center}.mkbd-slim{width:200px}.mkbd-tiny{width:110px}.mkbd-tiny2{width:130px}.mkbd-grow{flex:1;min-width:220px}.mkbd-full{width:100%}.mkbd-caps,.mkbd-sub{display:flex;flex-wrap:wrap;gap:10px;align-items:center;width:100%}.mkbd-sub{flex-direction:column;align-items:stretch;gap:4px}.mkbd-subrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.mkbd-caps-label{font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i5.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
895
|
+
}
|
|
896
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbCardEditor, decorators: [{
|
|
897
|
+
type: Component,
|
|
898
|
+
args: [{ selector: 'mkb-card-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
899
|
+
NgTemplateOutlet,
|
|
900
|
+
FormsModule,
|
|
901
|
+
MatFormFieldModule,
|
|
902
|
+
MatInputModule,
|
|
903
|
+
MatSelectModule,
|
|
904
|
+
MatSlideToggleModule,
|
|
905
|
+
MatCheckboxModule,
|
|
906
|
+
MatButtonModule,
|
|
907
|
+
], template: `
|
|
908
|
+
<div class="mkbd-row">
|
|
909
|
+
<mat-slide-toggle
|
|
910
|
+
[ngModel]="config().card?.dragHandle ?? false"
|
|
911
|
+
(ngModelChange)="setDragHandle($event)"
|
|
912
|
+
>
|
|
913
|
+
Arrastar só pela alça ⠿
|
|
914
|
+
</mat-slide-toggle>
|
|
915
|
+
</div>
|
|
916
|
+
|
|
917
|
+
<h3 class="mkbd-h">Blocos do cartão</h3>
|
|
918
|
+
<div class="mkbd-tree" role="tree" aria-label="Blocos do cartão">
|
|
919
|
+
@for (block of blocks(); track $index; let i = $index) {
|
|
920
|
+
<ng-container
|
|
921
|
+
*ngTemplateOutlet="blockRow; context: { block, path: [i], siblings: blocks().length }"
|
|
922
|
+
/>
|
|
923
|
+
@if (block.type === 'row') {
|
|
924
|
+
@for (child of rowChildren(block); track $index; let j = $index) {
|
|
925
|
+
<ng-container
|
|
926
|
+
*ngTemplateOutlet="
|
|
927
|
+
blockRow;
|
|
928
|
+
context: { block: child, path: [i, j], siblings: rowChildren(block).length }
|
|
929
|
+
"
|
|
930
|
+
/>
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} @empty {
|
|
934
|
+
<p class="mkbd-hint">Nenhum bloco — o cartão renderiza vazio. Adicione abaixo.</p>
|
|
935
|
+
}
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<ng-template #blockRow let-block="block" let-path="path" let-siblings="siblings">
|
|
939
|
+
<div
|
|
940
|
+
class="mkbd-node"
|
|
941
|
+
role="treeitem"
|
|
942
|
+
tabindex="0"
|
|
943
|
+
[class.mkbd-node-child]="path.length > 1"
|
|
944
|
+
[class.mkbd-node-selected]="isSelected(path)"
|
|
945
|
+
[attr.aria-selected]="isSelected(path)"
|
|
946
|
+
(click)="select(path)"
|
|
947
|
+
(keydown.enter)="select(path)"
|
|
948
|
+
>
|
|
949
|
+
<span class="mkbd-node-type">{{ typeLabel(block.type) }}</span>
|
|
950
|
+
<span class="mkbd-node-summary">{{ summary(block) }}</span>
|
|
951
|
+
<span class="mkbd-spacer"></span>
|
|
952
|
+
<button matIconButton type="button" (click)="move(path, -1); $event.stopPropagation()" [disabled]="path[path.length - 1] === 0" aria-label="Subir bloco">↑</button>
|
|
953
|
+
<button matIconButton type="button" (click)="move(path, 1); $event.stopPropagation()" [disabled]="path[path.length - 1] === siblings - 1" aria-label="Descer bloco">↓</button>
|
|
954
|
+
<button matIconButton type="button" (click)="remove(path); $event.stopPropagation()" aria-label="Remover bloco">✕</button>
|
|
955
|
+
</div>
|
|
956
|
+
</ng-template>
|
|
957
|
+
|
|
958
|
+
<div class="mkbd-row mkbd-mt">
|
|
959
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
960
|
+
<mat-label>Tipo do novo bloco</mat-label>
|
|
961
|
+
<mat-select [(ngModel)]="newType">
|
|
962
|
+
@for (t of blockTypes; track t.type) {
|
|
963
|
+
<mat-option [value]="t.type">{{ t.label }}</mat-option>
|
|
964
|
+
}
|
|
965
|
+
</mat-select>
|
|
966
|
+
</mat-form-field>
|
|
967
|
+
<button matButton type="button" (click)="add(false)">+ Adicionar</button>
|
|
968
|
+
@if (selectedIsRow()) {
|
|
969
|
+
<button matButton type="button" (click)="add(true)">+ Adicionar DENTRO da linha</button>
|
|
970
|
+
}
|
|
971
|
+
</div>
|
|
972
|
+
|
|
973
|
+
@if (selectedBlock(); as block) {
|
|
974
|
+
<h3 class="mkbd-h">Propriedades — {{ typeLabel(block.type) }}</h3>
|
|
975
|
+
<div class="mkbd-props">
|
|
976
|
+
@for (def of fieldsFor(block.type); track def.key) {
|
|
977
|
+
@switch (def.kind) {
|
|
978
|
+
@case ('text') {
|
|
979
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
980
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
981
|
+
<input
|
|
982
|
+
matInput
|
|
983
|
+
[ngModel]="str(block, def.key)"
|
|
984
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
985
|
+
/>
|
|
986
|
+
@if (def.hint) { <mat-hint>{{ def.hint }}</mat-hint> }
|
|
987
|
+
</mat-form-field>
|
|
988
|
+
}
|
|
989
|
+
@case ('number') {
|
|
990
|
+
<mat-form-field appearance="outline" class="mkbd-tiny">
|
|
991
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
992
|
+
<input
|
|
993
|
+
matInput
|
|
994
|
+
type="number"
|
|
995
|
+
[ngModel]="num(block, def.key)"
|
|
996
|
+
(ngModelChange)="setProp(def.key, $event === null || $event === '' ? undefined : +$event)"
|
|
997
|
+
/>
|
|
998
|
+
</mat-form-field>
|
|
999
|
+
}
|
|
1000
|
+
@case ('toggle') {
|
|
1001
|
+
<mat-slide-toggle
|
|
1002
|
+
[ngModel]="bool(block, def.key)"
|
|
1003
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
1004
|
+
>
|
|
1005
|
+
{{ def.label }}
|
|
1006
|
+
</mat-slide-toggle>
|
|
1007
|
+
}
|
|
1008
|
+
@case ('select') {
|
|
1009
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1010
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
1011
|
+
<mat-select
|
|
1012
|
+
[ngModel]="str(block, def.key)"
|
|
1013
|
+
(ngModelChange)="setProp(def.key, $event || undefined)"
|
|
1014
|
+
>
|
|
1015
|
+
@for (opt of def.options; track opt) {
|
|
1016
|
+
<mat-option [value]="opt">{{ opt || '—' }}</mat-option>
|
|
1017
|
+
}
|
|
1018
|
+
</mat-select>
|
|
1019
|
+
</mat-form-field>
|
|
1020
|
+
}
|
|
1021
|
+
@case ('csv') {
|
|
1022
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1023
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
1024
|
+
<input
|
|
1025
|
+
matInput
|
|
1026
|
+
[ngModel]="csv(block, def.key)"
|
|
1027
|
+
(ngModelChange)="setCsv(def.key, $event)"
|
|
1028
|
+
/>
|
|
1029
|
+
</mat-form-field>
|
|
1030
|
+
}
|
|
1031
|
+
@case ('json') {
|
|
1032
|
+
<mat-form-field appearance="outline" class="mkbd-full">
|
|
1033
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
1034
|
+
<textarea
|
|
1035
|
+
matInput
|
|
1036
|
+
rows="4"
|
|
1037
|
+
[value]="json(block, def.key)"
|
|
1038
|
+
(change)="setJson(def.key, $event)"
|
|
1039
|
+
></textarea>
|
|
1040
|
+
<mat-hint [class.mkbd-error]="jsonError()">
|
|
1041
|
+
{{ jsonError() || def.hint || 'Objeto JSON; vazio remove.' }}
|
|
1042
|
+
</mat-hint>
|
|
1043
|
+
</mat-form-field>
|
|
1044
|
+
}
|
|
1045
|
+
@case ('capabilities') {
|
|
1046
|
+
<div class="mkbd-caps">
|
|
1047
|
+
<span class="mkbd-caps-label">{{ def.label }}:</span>
|
|
1048
|
+
@for (cap of allCapabilities; track cap) {
|
|
1049
|
+
<mat-checkbox
|
|
1050
|
+
[ngModel]="hasCapability(block, cap)"
|
|
1051
|
+
(ngModelChange)="toggleCapability(cap, $event)"
|
|
1052
|
+
>
|
|
1053
|
+
{{ cap }}
|
|
1054
|
+
</mat-checkbox>
|
|
1055
|
+
}
|
|
1056
|
+
</div>
|
|
1057
|
+
}
|
|
1058
|
+
@case ('facts') {
|
|
1059
|
+
<div class="mkbd-sub">
|
|
1060
|
+
<span class="mkbd-caps-label">{{ def.label }}</span>
|
|
1061
|
+
@for (fact of facts(block); track $index; let fi = $index) {
|
|
1062
|
+
<div class="mkbd-subrow">
|
|
1063
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1064
|
+
<mat-label>Campo</mat-label>
|
|
1065
|
+
<input matInput [ngModel]="fact.field" (ngModelChange)="setFact(fi, 'field', $event)" />
|
|
1066
|
+
</mat-form-field>
|
|
1067
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1068
|
+
<mat-label>Ícone</mat-label>
|
|
1069
|
+
<input matInput [ngModel]="fact.icon ?? ''" (ngModelChange)="setFact(fi, 'icon', $event || undefined)" />
|
|
1070
|
+
</mat-form-field>
|
|
1071
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1072
|
+
<mat-label>Rótulo</mat-label>
|
|
1073
|
+
<input matInput [ngModel]="fact.label ?? ''" (ngModelChange)="setFact(fi, 'label', $event || undefined)" />
|
|
1074
|
+
</mat-form-field>
|
|
1075
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1076
|
+
<mat-label>Prefixo</mat-label>
|
|
1077
|
+
<input matInput [ngModel]="fact.prefix ?? ''" (ngModelChange)="setFact(fi, 'prefix', $event || undefined)" />
|
|
1078
|
+
</mat-form-field>
|
|
1079
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1080
|
+
<mat-label>Sufixo</mat-label>
|
|
1081
|
+
<input matInput [ngModel]="fact.suffix ?? ''" (ngModelChange)="setFact(fi, 'suffix', $event || undefined)" />
|
|
1082
|
+
</mat-form-field>
|
|
1083
|
+
<button matIconButton type="button" (click)="removeListItem(def.key, fi)" aria-label="Remover fato">✕</button>
|
|
1084
|
+
</div>
|
|
1085
|
+
}
|
|
1086
|
+
<button matButton type="button" (click)="addFact(def.key)">+ Fato</button>
|
|
1087
|
+
</div>
|
|
1088
|
+
}
|
|
1089
|
+
@case ('card-actions') {
|
|
1090
|
+
<div class="mkbd-sub">
|
|
1091
|
+
<span class="mkbd-caps-label">{{ def.label }}</span>
|
|
1092
|
+
@for (action of actions(block, def.key); track $index; let ai = $index) {
|
|
1093
|
+
<div class="mkbd-subrow">
|
|
1094
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1095
|
+
<mat-label>Id</mat-label>
|
|
1096
|
+
<input matInput [ngModel]="action.id" (ngModelChange)="setAction(def.key, ai, 'id', $event)" />
|
|
1097
|
+
</mat-form-field>
|
|
1098
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1099
|
+
<mat-label>Rótulo</mat-label>
|
|
1100
|
+
<input matInput [ngModel]="action.label" (ngModelChange)="setAction(def.key, ai, 'label', $event)" />
|
|
1101
|
+
</mat-form-field>
|
|
1102
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1103
|
+
<mat-label>Ícone</mat-label>
|
|
1104
|
+
<input matInput [ngModel]="action.icon ?? ''" (ngModelChange)="setAction(def.key, ai, 'icon', $event || undefined)" />
|
|
1105
|
+
</mat-form-field>
|
|
1106
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1107
|
+
<mat-label>Tom</mat-label>
|
|
1108
|
+
<mat-select [ngModel]="action.tone ?? ''" (ngModelChange)="setAction(def.key, ai, 'tone', $event || undefined)">
|
|
1109
|
+
@for (tone of tones; track tone) {
|
|
1110
|
+
<mat-option [value]="tone">{{ tone || '—' }}</mat-option>
|
|
1111
|
+
}
|
|
1112
|
+
</mat-select>
|
|
1113
|
+
</mat-form-field>
|
|
1114
|
+
<mat-form-field appearance="outline" class="mkbd-tiny2">
|
|
1115
|
+
<mat-label>Só nas colunas</mat-label>
|
|
1116
|
+
<input
|
|
1117
|
+
matInput
|
|
1118
|
+
placeholder="col-a, col-b"
|
|
1119
|
+
[ngModel]="whenColumns(action)"
|
|
1120
|
+
(ngModelChange)="setActionWhen(def.key, ai, $event)"
|
|
1121
|
+
/>
|
|
1122
|
+
</mat-form-field>
|
|
1123
|
+
<button matIconButton type="button" (click)="removeListItem(def.key, ai)" aria-label="Remover ação">✕</button>
|
|
1124
|
+
</div>
|
|
1125
|
+
}
|
|
1126
|
+
<button matButton type="button" (click)="addAction(def.key)">+ Ação</button>
|
|
1127
|
+
</div>
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
</div>
|
|
1132
|
+
} @else {
|
|
1133
|
+
<p class="mkbd-hint mkbd-mt">Selecione um bloco para editar as propriedades.</p>
|
|
1134
|
+
}
|
|
1135
|
+
`, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:12px}.mkbd-tree{display:flex;flex-direction:column;gap:3px}.mkbd-node{display:flex;align-items:center;gap:8px;padding:3px 10px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;cursor:pointer;background:var(--mkb-card-bg, #fff)}.mkbd-node:focus-visible{outline:2px solid var(--mkb-accent, #5e35b1);outline-offset:1px}.mkbd-node-child{margin-left:26px}.mkbd-node-selected{border-color:var(--mkb-accent, #5e35b1);box-shadow:inset 2px 0 0 var(--mkb-accent, #5e35b1)}.mkbd-node-type{font-size:12px;font-weight:600}.mkbd-node-summary{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mkbd-spacer{flex:1}.mkbd-props{display:flex;flex-wrap:wrap;gap:18px 10px;align-items:center}.mkbd-slim{width:200px}.mkbd-tiny{width:110px}.mkbd-tiny2{width:130px}.mkbd-grow{flex:1;min-width:220px}.mkbd-full{width:100%}.mkbd-caps,.mkbd-sub{display:flex;flex-wrap:wrap;gap:10px;align-items:center;width:100%}.mkbd-sub{flex-direction:column;align-items:stretch;gap:4px}.mkbd-subrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.mkbd-caps-label{font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"] }]
|
|
1136
|
+
}] });
|
|
1137
|
+
|
|
1138
|
+
/** Converte texto digitado no tipo mais provável (números e booleanos). */
|
|
1139
|
+
function coerce(raw) {
|
|
1140
|
+
if (raw === 'true')
|
|
1141
|
+
return true;
|
|
1142
|
+
if (raw === 'false')
|
|
1143
|
+
return false;
|
|
1144
|
+
if (/^-?\d+(\.\d+)?$/.test(raw))
|
|
1145
|
+
return Number(raw);
|
|
1146
|
+
return raw;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Editor de DADOS: os itens de exemplo do board (`config.items`), suas
|
|
1150
|
+
* entries (blocos `items` do cartão) e os filtros iniciais. São os dados
|
|
1151
|
+
* que viajam junto na exportação — no runtime real o host normalmente
|
|
1152
|
+
* substitui por um adapter.
|
|
1153
|
+
*/
|
|
1154
|
+
class MkbDataEditor {
|
|
1155
|
+
store = inject(DesignerStore);
|
|
1156
|
+
config = this.store.config;
|
|
1157
|
+
filtersError = signal(null, ...(ngDevMode ? [{ debugName: "filtersError" }] : /* istanbul ignore next */ []));
|
|
1158
|
+
items() {
|
|
1159
|
+
return this.config().items ?? [];
|
|
1160
|
+
}
|
|
1161
|
+
columnField() {
|
|
1162
|
+
return this.store.columnsAxis().field ?? 'column';
|
|
1163
|
+
}
|
|
1164
|
+
laneField() {
|
|
1165
|
+
return this.store.lanesAxis()?.field ?? 'lane';
|
|
1166
|
+
}
|
|
1167
|
+
hasLanes() {
|
|
1168
|
+
return !!this.store.lanesAxis();
|
|
1169
|
+
}
|
|
1170
|
+
columnIds() {
|
|
1171
|
+
return (this.store.columnsAxis().groups ?? []).map((g) => g.id);
|
|
1172
|
+
}
|
|
1173
|
+
laneIds() {
|
|
1174
|
+
return (this.store.lanesAxis()?.groups ?? []).map((g) => g.id);
|
|
1175
|
+
}
|
|
1176
|
+
label(item) {
|
|
1177
|
+
const title = item['title'] ?? item['titulo'] ?? item['name'];
|
|
1178
|
+
return typeof title === 'string' && title ? title : item.id;
|
|
1179
|
+
}
|
|
1180
|
+
position(item) {
|
|
1181
|
+
const col = this.str(item, this.columnField());
|
|
1182
|
+
const lane = this.hasLanes() ? this.str(item, this.laneField()) : '';
|
|
1183
|
+
return lane ? `${col} · ${lane}` : col;
|
|
1184
|
+
}
|
|
1185
|
+
str(record, key) {
|
|
1186
|
+
const v = record[key];
|
|
1187
|
+
return typeof v === 'string' ? v : v == null ? '' : String(v);
|
|
1188
|
+
}
|
|
1189
|
+
display(item, key) {
|
|
1190
|
+
const v = item[key];
|
|
1191
|
+
return typeof v === 'object' ? JSON.stringify(v) : String(v ?? '');
|
|
1192
|
+
}
|
|
1193
|
+
coerceValue(raw) {
|
|
1194
|
+
return coerce(raw);
|
|
1195
|
+
}
|
|
1196
|
+
/** Campos livres do item: tudo menos id, eixos e campos de entries. */
|
|
1197
|
+
freeFields(item) {
|
|
1198
|
+
const reserved = new Set([
|
|
1199
|
+
'id',
|
|
1200
|
+
this.columnField(),
|
|
1201
|
+
this.laneField(),
|
|
1202
|
+
...this.entryDefs().map((d) => d.field),
|
|
1203
|
+
]);
|
|
1204
|
+
return Object.keys(item).filter((k) => !reserved.has(k));
|
|
1205
|
+
}
|
|
1206
|
+
/** Blocos `items` declarados no cartão (recursivo em rows). */
|
|
1207
|
+
entryDefs() {
|
|
1208
|
+
const defs = [];
|
|
1209
|
+
const walk = (blocks) => {
|
|
1210
|
+
for (const block of blocks) {
|
|
1211
|
+
if (block.type === 'items') {
|
|
1212
|
+
defs.push({
|
|
1213
|
+
field: block.field,
|
|
1214
|
+
labelField: block.labelField ?? 'label',
|
|
1215
|
+
checkField: block.checkField,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
else if (block.type === 'row') {
|
|
1219
|
+
walk(block.blocks);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
walk(this.config().card?.blocks ?? []);
|
|
1224
|
+
return defs;
|
|
1225
|
+
}
|
|
1226
|
+
entriesOf(item, field) {
|
|
1227
|
+
const v = item[field];
|
|
1228
|
+
return Array.isArray(v) ? v : [];
|
|
1229
|
+
}
|
|
1230
|
+
// ---- mutações de item ---------------------------------------------------------
|
|
1231
|
+
addItem() {
|
|
1232
|
+
const column = this.columnIds()[0] ?? 'todo';
|
|
1233
|
+
const lane = this.hasLanes() ? this.laneIds()[0] : undefined;
|
|
1234
|
+
this.store.update((d) => {
|
|
1235
|
+
d.items ??= [];
|
|
1236
|
+
const item = {
|
|
1237
|
+
id: `item-${d.items.length + 1}`,
|
|
1238
|
+
[this.columnField()]: column,
|
|
1239
|
+
title: 'Novo item',
|
|
1240
|
+
};
|
|
1241
|
+
if (lane)
|
|
1242
|
+
item[this.laneField()] = lane;
|
|
1243
|
+
d.items.push(item);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
duplicateItem(index) {
|
|
1247
|
+
this.store.update((d) => {
|
|
1248
|
+
const source = d.items?.[index];
|
|
1249
|
+
if (!source || !d.items)
|
|
1250
|
+
return;
|
|
1251
|
+
const copy = structuredClone(source);
|
|
1252
|
+
copy['id'] = `${source.id}-copia`;
|
|
1253
|
+
d.items.splice(index + 1, 0, copy);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
removeItem(index) {
|
|
1257
|
+
this.store.update((d) => d.items?.splice(index, 1));
|
|
1258
|
+
}
|
|
1259
|
+
setItemProp(index, key, value) {
|
|
1260
|
+
this.store.update((d) => {
|
|
1261
|
+
const item = d.items?.[index];
|
|
1262
|
+
if (!item)
|
|
1263
|
+
return;
|
|
1264
|
+
if (value === undefined || value === '')
|
|
1265
|
+
delete item[key];
|
|
1266
|
+
else
|
|
1267
|
+
item[key] = value;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
addField(index) {
|
|
1271
|
+
this.store.update((d) => {
|
|
1272
|
+
const item = d.items?.[index];
|
|
1273
|
+
if (!item)
|
|
1274
|
+
return;
|
|
1275
|
+
let n = 1;
|
|
1276
|
+
while (`campo-${n}` in item)
|
|
1277
|
+
n++;
|
|
1278
|
+
item[`campo-${n}`] = '';
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
renameField(index, oldKey, event) {
|
|
1282
|
+
const newKey = event.target.value.trim();
|
|
1283
|
+
if (!newKey || newKey === oldKey)
|
|
1284
|
+
return;
|
|
1285
|
+
this.store.update((d) => {
|
|
1286
|
+
const item = d.items?.[index];
|
|
1287
|
+
if (!item || newKey in item)
|
|
1288
|
+
return;
|
|
1289
|
+
item[newKey] = item[oldKey];
|
|
1290
|
+
delete item[oldKey];
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
removeField(index, key) {
|
|
1294
|
+
this.setItemProp(index, key, undefined);
|
|
1295
|
+
}
|
|
1296
|
+
// ---- entries -------------------------------------------------------------------
|
|
1297
|
+
addEntry(index, def) {
|
|
1298
|
+
this.store.update((d) => {
|
|
1299
|
+
const item = d.items?.[index];
|
|
1300
|
+
if (!item)
|
|
1301
|
+
return;
|
|
1302
|
+
const list = (item[def.field] ??= []);
|
|
1303
|
+
const entry = {
|
|
1304
|
+
id: `entry-${list.length + 1}`,
|
|
1305
|
+
[def.labelField]: 'Novo item interno',
|
|
1306
|
+
};
|
|
1307
|
+
if (def.checkField)
|
|
1308
|
+
entry[def.checkField] = false;
|
|
1309
|
+
list.push(entry);
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
setEntryProp(index, field, entryIndex, key, value) {
|
|
1313
|
+
this.store.update((d) => {
|
|
1314
|
+
const item = d.items?.[index];
|
|
1315
|
+
const list = item?.[field];
|
|
1316
|
+
const entry = list?.[entryIndex];
|
|
1317
|
+
if (!entry)
|
|
1318
|
+
return;
|
|
1319
|
+
entry[key] = value;
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
removeEntry(index, field, entryIndex) {
|
|
1323
|
+
this.store.update((d) => {
|
|
1324
|
+
const item = d.items?.[index];
|
|
1325
|
+
const list = item?.[field];
|
|
1326
|
+
list?.splice(entryIndex, 1);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
// ---- filtros ---------------------------------------------------------------------
|
|
1330
|
+
filtersJson() {
|
|
1331
|
+
const filters = this.config().filters;
|
|
1332
|
+
return filters?.length ? JSON.stringify(filters, null, 2) : '';
|
|
1333
|
+
}
|
|
1334
|
+
setFilters(event) {
|
|
1335
|
+
const raw = event.target.value.trim();
|
|
1336
|
+
if (!raw) {
|
|
1337
|
+
this.filtersError.set(null);
|
|
1338
|
+
this.store.update((d) => delete d.filters);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
const parsed = JSON.parse(raw);
|
|
1343
|
+
if (!Array.isArray(parsed))
|
|
1344
|
+
throw new Error('esperado um ARRAY de filtros');
|
|
1345
|
+
this.filtersError.set(null);
|
|
1346
|
+
this.store.update((d) => (d.filters = parsed));
|
|
1347
|
+
}
|
|
1348
|
+
catch (e) {
|
|
1349
|
+
this.filtersError.set(`JSON inválido: ${e.message}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbDataEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1353
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbDataEditor, isStandalone: true, selector: "mkb-data-editor", ngImport: i0, template: `
|
|
1354
|
+
<h3 class="mkbd-h">Itens ({{ items().length }})</h3>
|
|
1355
|
+
<mat-accordion displayMode="flat" [multi]="false">
|
|
1356
|
+
@for (item of items(); track item.id; let i = $index) {
|
|
1357
|
+
<mat-expansion-panel class="mat-elevation-z0 mkbd-panel">
|
|
1358
|
+
<mat-expansion-panel-header>
|
|
1359
|
+
<mat-panel-title>{{ label(item) }}</mat-panel-title>
|
|
1360
|
+
<mat-panel-description>{{ position(item) }}</mat-panel-description>
|
|
1361
|
+
</mat-expansion-panel-header>
|
|
1362
|
+
|
|
1363
|
+
<div class="mkbd-row">
|
|
1364
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1365
|
+
<mat-label>Id</mat-label>
|
|
1366
|
+
<input matInput [ngModel]="item.id" (ngModelChange)="setItemProp(i, 'id', $event)" />
|
|
1367
|
+
</mat-form-field>
|
|
1368
|
+
@if (columnIds().length) {
|
|
1369
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1370
|
+
<mat-label>Coluna ({{ columnField() }})</mat-label>
|
|
1371
|
+
<mat-select
|
|
1372
|
+
[ngModel]="str(item, columnField())"
|
|
1373
|
+
(ngModelChange)="setItemProp(i, columnField(), $event)"
|
|
1374
|
+
>
|
|
1375
|
+
@for (id of columnIds(); track id) {
|
|
1376
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1377
|
+
}
|
|
1378
|
+
</mat-select>
|
|
1379
|
+
</mat-form-field>
|
|
1380
|
+
} @else {
|
|
1381
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1382
|
+
<mat-label>Coluna ({{ columnField() }})</mat-label>
|
|
1383
|
+
<input matInput [ngModel]="str(item, columnField())" (ngModelChange)="setItemProp(i, columnField(), $event)" />
|
|
1384
|
+
</mat-form-field>
|
|
1385
|
+
}
|
|
1386
|
+
@if (hasLanes()) {
|
|
1387
|
+
@if (laneIds().length) {
|
|
1388
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1389
|
+
<mat-label>Lane ({{ laneField() }})</mat-label>
|
|
1390
|
+
<mat-select [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)">
|
|
1391
|
+
@for (id of laneIds(); track id) {
|
|
1392
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1393
|
+
}
|
|
1394
|
+
</mat-select>
|
|
1395
|
+
</mat-form-field>
|
|
1396
|
+
} @else {
|
|
1397
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1398
|
+
<mat-label>Lane ({{ laneField() }})</mat-label>
|
|
1399
|
+
<input matInput [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)" />
|
|
1400
|
+
</mat-form-field>
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
</div>
|
|
1404
|
+
|
|
1405
|
+
<h4 class="mkbd-h4">Campos</h4>
|
|
1406
|
+
@for (key of freeFields(item); track key) {
|
|
1407
|
+
<div class="mkbd-row">
|
|
1408
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1409
|
+
<mat-label>Campo</mat-label>
|
|
1410
|
+
<input matInput [ngModel]="key" (change)="renameField(i, key, $event)" />
|
|
1411
|
+
</mat-form-field>
|
|
1412
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1413
|
+
<mat-label>Valor</mat-label>
|
|
1414
|
+
<input matInput [ngModel]="display(item, key)" (ngModelChange)="setItemProp(i, key, coerceValue($event))" />
|
|
1415
|
+
</mat-form-field>
|
|
1416
|
+
<button matIconButton type="button" (click)="removeField(i, key)" aria-label="Remover campo">✕</button>
|
|
1417
|
+
</div>
|
|
1418
|
+
}
|
|
1419
|
+
<button matButton type="button" (click)="addField(i)">+ Campo</button>
|
|
1420
|
+
|
|
1421
|
+
@for (def of entryDefs(); track def.field) {
|
|
1422
|
+
<h4 class="mkbd-h4">Itens internos — {{ def.field }}</h4>
|
|
1423
|
+
@for (entry of entriesOf(item, def.field); track entry.id; let ei = $index) {
|
|
1424
|
+
<div class="mkbd-row">
|
|
1425
|
+
@if (def.checkField; as checkField) {
|
|
1426
|
+
<mat-checkbox
|
|
1427
|
+
[ngModel]="entry[checkField] === true"
|
|
1428
|
+
(ngModelChange)="setEntryProp(i, def.field, ei, checkField, $event)"
|
|
1429
|
+
aria-label="Concluído"
|
|
1430
|
+
/>
|
|
1431
|
+
}
|
|
1432
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1433
|
+
<mat-label>{{ def.labelField }}</mat-label>
|
|
1434
|
+
<input
|
|
1435
|
+
matInput
|
|
1436
|
+
[ngModel]="str(entry, def.labelField)"
|
|
1437
|
+
(ngModelChange)="setEntryProp(i, def.field, ei, def.labelField, $event)"
|
|
1438
|
+
/>
|
|
1439
|
+
</mat-form-field>
|
|
1440
|
+
<button matIconButton type="button" (click)="removeEntry(i, def.field, ei)" aria-label="Remover item interno">✕</button>
|
|
1441
|
+
</div>
|
|
1442
|
+
}
|
|
1443
|
+
<button matButton type="button" (click)="addEntry(i, def)">+ Item interno</button>
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
<div class="mkbd-row mkbd-mt">
|
|
1447
|
+
<button matButton type="button" (click)="duplicateItem(i)">Duplicar</button>
|
|
1448
|
+
<button matButton type="button" (click)="removeItem(i)">Remover item</button>
|
|
1449
|
+
</div>
|
|
1450
|
+
</mat-expansion-panel>
|
|
1451
|
+
}
|
|
1452
|
+
</mat-accordion>
|
|
1453
|
+
<button matButton class="mkbd-mt" type="button" (click)="addItem()">+ Adicionar item</button>
|
|
1454
|
+
|
|
1455
|
+
<h3 class="mkbd-h">Filtros iniciais (JSON)</h3>
|
|
1456
|
+
<p class="mkbd-hint">Array de KanbanFilter aplicado na criação do board; vazio remove.</p>
|
|
1457
|
+
<mat-form-field appearance="outline" class="mkbd-full">
|
|
1458
|
+
<mat-label>filters</mat-label>
|
|
1459
|
+
<textarea matInput rows="4" [value]="filtersJson()" (change)="setFilters($event)"></textarea>
|
|
1460
|
+
@if (filtersError(); as err) {
|
|
1461
|
+
<mat-hint class="mkbd-error">{{ err }}</mat-hint>
|
|
1462
|
+
}
|
|
1463
|
+
</mat-form-field>
|
|
1464
|
+
`, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-h:first-child{margin-top:0}.mkbd-h4{margin:12px 0 6px;font-size:12px;font-weight:600;color:var(--mkb-muted, #6f6a7c)}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:2px}.mkbd-mt{margin-top:10px}.mkbd-slim{width:180px}.mkbd-grow{flex:1;min-width:180px}.mkbd-full{width:100%}.mkbd-panel{border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px!important;margin-bottom:6px}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i7$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i7$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i7$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i7$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i7$1.MatExpansionPanelDescription, selector: "mat-panel-description" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1465
|
+
}
|
|
1466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbDataEditor, decorators: [{
|
|
1467
|
+
type: Component,
|
|
1468
|
+
args: [{ selector: 'mkb-data-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1469
|
+
FormsModule,
|
|
1470
|
+
MatFormFieldModule,
|
|
1471
|
+
MatInputModule,
|
|
1472
|
+
MatSelectModule,
|
|
1473
|
+
MatCheckboxModule,
|
|
1474
|
+
MatButtonModule,
|
|
1475
|
+
MatExpansionModule,
|
|
1476
|
+
], template: `
|
|
1477
|
+
<h3 class="mkbd-h">Itens ({{ items().length }})</h3>
|
|
1478
|
+
<mat-accordion displayMode="flat" [multi]="false">
|
|
1479
|
+
@for (item of items(); track item.id; let i = $index) {
|
|
1480
|
+
<mat-expansion-panel class="mat-elevation-z0 mkbd-panel">
|
|
1481
|
+
<mat-expansion-panel-header>
|
|
1482
|
+
<mat-panel-title>{{ label(item) }}</mat-panel-title>
|
|
1483
|
+
<mat-panel-description>{{ position(item) }}</mat-panel-description>
|
|
1484
|
+
</mat-expansion-panel-header>
|
|
1485
|
+
|
|
1486
|
+
<div class="mkbd-row">
|
|
1487
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1488
|
+
<mat-label>Id</mat-label>
|
|
1489
|
+
<input matInput [ngModel]="item.id" (ngModelChange)="setItemProp(i, 'id', $event)" />
|
|
1490
|
+
</mat-form-field>
|
|
1491
|
+
@if (columnIds().length) {
|
|
1492
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1493
|
+
<mat-label>Coluna ({{ columnField() }})</mat-label>
|
|
1494
|
+
<mat-select
|
|
1495
|
+
[ngModel]="str(item, columnField())"
|
|
1496
|
+
(ngModelChange)="setItemProp(i, columnField(), $event)"
|
|
1497
|
+
>
|
|
1498
|
+
@for (id of columnIds(); track id) {
|
|
1499
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1500
|
+
}
|
|
1501
|
+
</mat-select>
|
|
1502
|
+
</mat-form-field>
|
|
1503
|
+
} @else {
|
|
1504
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1505
|
+
<mat-label>Coluna ({{ columnField() }})</mat-label>
|
|
1506
|
+
<input matInput [ngModel]="str(item, columnField())" (ngModelChange)="setItemProp(i, columnField(), $event)" />
|
|
1507
|
+
</mat-form-field>
|
|
1508
|
+
}
|
|
1509
|
+
@if (hasLanes()) {
|
|
1510
|
+
@if (laneIds().length) {
|
|
1511
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1512
|
+
<mat-label>Lane ({{ laneField() }})</mat-label>
|
|
1513
|
+
<mat-select [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)">
|
|
1514
|
+
@for (id of laneIds(); track id) {
|
|
1515
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1516
|
+
}
|
|
1517
|
+
</mat-select>
|
|
1518
|
+
</mat-form-field>
|
|
1519
|
+
} @else {
|
|
1520
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1521
|
+
<mat-label>Lane ({{ laneField() }})</mat-label>
|
|
1522
|
+
<input matInput [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)" />
|
|
1523
|
+
</mat-form-field>
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
</div>
|
|
1527
|
+
|
|
1528
|
+
<h4 class="mkbd-h4">Campos</h4>
|
|
1529
|
+
@for (key of freeFields(item); track key) {
|
|
1530
|
+
<div class="mkbd-row">
|
|
1531
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1532
|
+
<mat-label>Campo</mat-label>
|
|
1533
|
+
<input matInput [ngModel]="key" (change)="renameField(i, key, $event)" />
|
|
1534
|
+
</mat-form-field>
|
|
1535
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1536
|
+
<mat-label>Valor</mat-label>
|
|
1537
|
+
<input matInput [ngModel]="display(item, key)" (ngModelChange)="setItemProp(i, key, coerceValue($event))" />
|
|
1538
|
+
</mat-form-field>
|
|
1539
|
+
<button matIconButton type="button" (click)="removeField(i, key)" aria-label="Remover campo">✕</button>
|
|
1540
|
+
</div>
|
|
1541
|
+
}
|
|
1542
|
+
<button matButton type="button" (click)="addField(i)">+ Campo</button>
|
|
1543
|
+
|
|
1544
|
+
@for (def of entryDefs(); track def.field) {
|
|
1545
|
+
<h4 class="mkbd-h4">Itens internos — {{ def.field }}</h4>
|
|
1546
|
+
@for (entry of entriesOf(item, def.field); track entry.id; let ei = $index) {
|
|
1547
|
+
<div class="mkbd-row">
|
|
1548
|
+
@if (def.checkField; as checkField) {
|
|
1549
|
+
<mat-checkbox
|
|
1550
|
+
[ngModel]="entry[checkField] === true"
|
|
1551
|
+
(ngModelChange)="setEntryProp(i, def.field, ei, checkField, $event)"
|
|
1552
|
+
aria-label="Concluído"
|
|
1553
|
+
/>
|
|
1554
|
+
}
|
|
1555
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1556
|
+
<mat-label>{{ def.labelField }}</mat-label>
|
|
1557
|
+
<input
|
|
1558
|
+
matInput
|
|
1559
|
+
[ngModel]="str(entry, def.labelField)"
|
|
1560
|
+
(ngModelChange)="setEntryProp(i, def.field, ei, def.labelField, $event)"
|
|
1561
|
+
/>
|
|
1562
|
+
</mat-form-field>
|
|
1563
|
+
<button matIconButton type="button" (click)="removeEntry(i, def.field, ei)" aria-label="Remover item interno">✕</button>
|
|
1564
|
+
</div>
|
|
1565
|
+
}
|
|
1566
|
+
<button matButton type="button" (click)="addEntry(i, def)">+ Item interno</button>
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
<div class="mkbd-row mkbd-mt">
|
|
1570
|
+
<button matButton type="button" (click)="duplicateItem(i)">Duplicar</button>
|
|
1571
|
+
<button matButton type="button" (click)="removeItem(i)">Remover item</button>
|
|
1572
|
+
</div>
|
|
1573
|
+
</mat-expansion-panel>
|
|
1574
|
+
}
|
|
1575
|
+
</mat-accordion>
|
|
1576
|
+
<button matButton class="mkbd-mt" type="button" (click)="addItem()">+ Adicionar item</button>
|
|
1577
|
+
|
|
1578
|
+
<h3 class="mkbd-h">Filtros iniciais (JSON)</h3>
|
|
1579
|
+
<p class="mkbd-hint">Array de KanbanFilter aplicado na criação do board; vazio remove.</p>
|
|
1580
|
+
<mat-form-field appearance="outline" class="mkbd-full">
|
|
1581
|
+
<mat-label>filters</mat-label>
|
|
1582
|
+
<textarea matInput rows="4" [value]="filtersJson()" (change)="setFilters($event)"></textarea>
|
|
1583
|
+
@if (filtersError(); as err) {
|
|
1584
|
+
<mat-hint class="mkbd-error">{{ err }}</mat-hint>
|
|
1585
|
+
}
|
|
1586
|
+
</mat-form-field>
|
|
1587
|
+
`, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-h:first-child{margin-top:0}.mkbd-h4{margin:12px 0 6px;font-size:12px;font-weight:600;color:var(--mkb-muted, #6f6a7c)}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:2px}.mkbd-mt{margin-top:10px}.mkbd-slim{width:180px}.mkbd-grow{flex:1;min-width:180px}.mkbd-full{width:100%}.mkbd-panel{border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px!important;margin-bottom:6px}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"] }]
|
|
1588
|
+
}] });
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Editor do PIPELINE: eixos (colunas/lanes — estáticos ou derivados),
|
|
1592
|
+
* regras de transição/nomeadas e ordenação. Todos os recursos flexíveis
|
|
1593
|
+
* do BoardConfig fora do cartão.
|
|
1594
|
+
*/
|
|
1595
|
+
class MkbPipelineEditor {
|
|
1596
|
+
store = inject(DesignerStore);
|
|
1597
|
+
config = this.store.config;
|
|
1598
|
+
columnIds() {
|
|
1599
|
+
return (this.store.columnsAxis().groups ?? []).map((g) => g.id);
|
|
1600
|
+
}
|
|
1601
|
+
// ---- eixos -----------------------------------------------------------------
|
|
1602
|
+
toggleLanes(enabled) {
|
|
1603
|
+
this.store.update((d) => {
|
|
1604
|
+
if (enabled) {
|
|
1605
|
+
d.lanes = { groups: [{ id: 'lane-1', title: 'Lane 1' }] };
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
delete d.lanes;
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
setAxisMode(key, mode) {
|
|
1613
|
+
this.store.update((d) => {
|
|
1614
|
+
const axis = this.draftAxis(d, key);
|
|
1615
|
+
if (mode === 'derived') {
|
|
1616
|
+
delete axis.groups;
|
|
1617
|
+
delete axis.reorderable;
|
|
1618
|
+
axis.field ||= key === 'columns' ? 'column' : 'lane';
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
axis.groups ??= [{ id: 'grupo-1', title: 'Grupo 1' }];
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
setAxisProp(key, prop, value) {
|
|
1626
|
+
this.store.update((d) => {
|
|
1627
|
+
const axis = this.draftAxis(d, key);
|
|
1628
|
+
if (value === undefined)
|
|
1629
|
+
delete axis[prop];
|
|
1630
|
+
else
|
|
1631
|
+
axis[prop] = value;
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
setGroup(key, index, prop, value) {
|
|
1635
|
+
this.store.update((d) => {
|
|
1636
|
+
const group = this.draftAxis(d, key).groups?.[index];
|
|
1637
|
+
if (!group)
|
|
1638
|
+
return;
|
|
1639
|
+
if (value === undefined)
|
|
1640
|
+
delete group[prop];
|
|
1641
|
+
else
|
|
1642
|
+
group[prop] = value;
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
groupColor(group) {
|
|
1646
|
+
const c = group.meta?.['color'];
|
|
1647
|
+
return typeof c === 'string' ? c : '#efedf3';
|
|
1648
|
+
}
|
|
1649
|
+
setGroupColor(key, index, color) {
|
|
1650
|
+
this.store.update((d) => {
|
|
1651
|
+
const group = this.draftAxis(d, key).groups?.[index];
|
|
1652
|
+
if (!group)
|
|
1653
|
+
return;
|
|
1654
|
+
group.meta = { ...(group.meta ?? {}), color };
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
addGroup(key) {
|
|
1658
|
+
this.store.update((d) => {
|
|
1659
|
+
const groups = this.draftAxis(d, key).groups;
|
|
1660
|
+
groups?.push({ id: `grupo-${(groups.length + 1)}` });
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
removeGroup(key, index) {
|
|
1664
|
+
this.store.update((d) => this.draftAxis(d, key).groups?.splice(index, 1));
|
|
1665
|
+
}
|
|
1666
|
+
moveGroup(key, index, delta) {
|
|
1667
|
+
this.store.update((d) => {
|
|
1668
|
+
const groups = this.draftAxis(d, key).groups;
|
|
1669
|
+
if (!groups)
|
|
1670
|
+
return;
|
|
1671
|
+
const target = index + delta;
|
|
1672
|
+
if (target < 0 || target >= groups.length)
|
|
1673
|
+
return;
|
|
1674
|
+
const [g] = groups.splice(index, 1);
|
|
1675
|
+
groups.splice(target, 0, g);
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
draftAxis(d, key) {
|
|
1679
|
+
if (key === 'lanes') {
|
|
1680
|
+
d.lanes ??= {};
|
|
1681
|
+
if (Array.isArray(d.lanes))
|
|
1682
|
+
d.lanes = { groups: d.lanes };
|
|
1683
|
+
return d.lanes;
|
|
1684
|
+
}
|
|
1685
|
+
if (Array.isArray(d.columns))
|
|
1686
|
+
d.columns = { groups: d.columns };
|
|
1687
|
+
return d.columns;
|
|
1688
|
+
}
|
|
1689
|
+
// ---- regras ----------------------------------------------------------------
|
|
1690
|
+
transitions() {
|
|
1691
|
+
return this.config().rules?.transitions ?? [];
|
|
1692
|
+
}
|
|
1693
|
+
fromOf(t) {
|
|
1694
|
+
return typeof t.from === 'string' ? t.from : Array.isArray(t.from) ? (t.from[0] ?? '*') : '*';
|
|
1695
|
+
}
|
|
1696
|
+
toOf(t) {
|
|
1697
|
+
return typeof t.to === 'string' ? [t.to] : (t.to ?? []);
|
|
1698
|
+
}
|
|
1699
|
+
addTransition() {
|
|
1700
|
+
this.store.update((d) => {
|
|
1701
|
+
d.rules ??= {};
|
|
1702
|
+
d.rules.transitions ??= [];
|
|
1703
|
+
d.rules.transitions.push({});
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
removeTransition(index) {
|
|
1707
|
+
this.store.update((d) => {
|
|
1708
|
+
d.rules?.transitions?.splice(index, 1);
|
|
1709
|
+
if (d.rules?.transitions?.length === 0)
|
|
1710
|
+
delete d.rules.transitions;
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
setTransition(index, side, value) {
|
|
1714
|
+
this.store.update((d) => {
|
|
1715
|
+
const t = d.rules?.transitions?.[index];
|
|
1716
|
+
if (!t)
|
|
1717
|
+
return;
|
|
1718
|
+
if (side === 'from') {
|
|
1719
|
+
if (value === '*')
|
|
1720
|
+
delete t['from'];
|
|
1721
|
+
else
|
|
1722
|
+
t['from'] = value;
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
const list = value;
|
|
1726
|
+
if (!list.length)
|
|
1727
|
+
delete t['to'];
|
|
1728
|
+
else
|
|
1729
|
+
t['to'] = list.length === 1 ? list[0] : list;
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
namesOf(key) {
|
|
1734
|
+
return (this.config().rules?.[key] ?? []).join(', ');
|
|
1735
|
+
}
|
|
1736
|
+
setNames(key, csv) {
|
|
1737
|
+
const names = csv.split(/[\s,]+/).filter(Boolean);
|
|
1738
|
+
this.store.update((d) => {
|
|
1739
|
+
d.rules ??= {};
|
|
1740
|
+
if (names.length)
|
|
1741
|
+
d.rules[key] = names;
|
|
1742
|
+
else
|
|
1743
|
+
delete d.rules[key];
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
// ---- ordenação / geral -------------------------------------------------------
|
|
1747
|
+
toggleSort(enabled) {
|
|
1748
|
+
this.store.update((d) => {
|
|
1749
|
+
if (enabled)
|
|
1750
|
+
d.sort = { field: 'title', direction: 'asc' };
|
|
1751
|
+
else
|
|
1752
|
+
delete d.sort;
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
setSort(prop, value) {
|
|
1756
|
+
this.store.update((d) => {
|
|
1757
|
+
if (!d.sort)
|
|
1758
|
+
return;
|
|
1759
|
+
if (prop === 'field')
|
|
1760
|
+
d.sort.field = value;
|
|
1761
|
+
else
|
|
1762
|
+
d.sort.direction = value;
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
setReadOnly(value) {
|
|
1766
|
+
this.store.update((d) => {
|
|
1767
|
+
if (value)
|
|
1768
|
+
d.readOnly = true;
|
|
1769
|
+
else
|
|
1770
|
+
delete d.readOnly;
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbPipelineEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1774
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbPipelineEditor, isStandalone: true, selector: "mkb-pipeline-editor", ngImport: i0, template: `
|
|
1775
|
+
<!-- ======================= COLUNAS ======================= -->
|
|
1776
|
+
<h3 class="mkbd-h">Colunas</h3>
|
|
1777
|
+
<ng-container
|
|
1778
|
+
*ngTemplateOutlet="axisEditor; context: { axis: store.columnsAxis(), key: 'columns' }"
|
|
1779
|
+
/>
|
|
1780
|
+
|
|
1781
|
+
<!-- ======================= LANES ========================= -->
|
|
1782
|
+
<h3 class="mkbd-h">
|
|
1783
|
+
Lanes (swimlanes)
|
|
1784
|
+
<mat-slide-toggle
|
|
1785
|
+
[ngModel]="!!store.lanesAxis()"
|
|
1786
|
+
(ngModelChange)="toggleLanes($event)"
|
|
1787
|
+
aria-label="Usar lanes"
|
|
1788
|
+
/>
|
|
1789
|
+
</h3>
|
|
1790
|
+
@if (store.lanesAxis(); as lanes) {
|
|
1791
|
+
<ng-container *ngTemplateOutlet="axisEditor; context: { axis: lanes, key: 'lanes' }" />
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
<!-- ================= EDITOR DE EIXO (template) ============ -->
|
|
1795
|
+
<ng-template #axisEditor let-axis="axis" let-key="key">
|
|
1796
|
+
<div class="mkbd-row">
|
|
1797
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1798
|
+
<mat-label>Modo</mat-label>
|
|
1799
|
+
<mat-select
|
|
1800
|
+
[ngModel]="axis.groups ? 'static' : 'derived'"
|
|
1801
|
+
(ngModelChange)="setAxisMode(key, $event)"
|
|
1802
|
+
>
|
|
1803
|
+
<mat-option value="static">Estático (grupos declarados)</mat-option>
|
|
1804
|
+
<mat-option value="derived">Derivado (agrupamento dinâmico)</mat-option>
|
|
1805
|
+
</mat-select>
|
|
1806
|
+
</mat-form-field>
|
|
1807
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1808
|
+
<mat-label>Campo do item</mat-label>
|
|
1809
|
+
<input
|
|
1810
|
+
matInput
|
|
1811
|
+
[ngModel]="axis.field ?? ''"
|
|
1812
|
+
(ngModelChange)="setAxisProp(key, 'field', $event || undefined)"
|
|
1813
|
+
[placeholder]="key === 'columns' ? 'column' : 'lane'"
|
|
1814
|
+
/>
|
|
1815
|
+
</mat-form-field>
|
|
1816
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1817
|
+
<mat-label>Ordenar grupos</mat-label>
|
|
1818
|
+
<mat-select
|
|
1819
|
+
[ngModel]="axis.sortGroups ?? ''"
|
|
1820
|
+
(ngModelChange)="setAxisProp(key, 'sortGroups', $event || undefined)"
|
|
1821
|
+
>
|
|
1822
|
+
<mat-option value="">Ordem declarada / de aparição</mat-option>
|
|
1823
|
+
<mat-option value="asc">Crescente</mat-option>
|
|
1824
|
+
<mat-option value="desc">Decrescente</mat-option>
|
|
1825
|
+
</mat-select>
|
|
1826
|
+
</mat-form-field>
|
|
1827
|
+
@if (axis.groups) {
|
|
1828
|
+
<mat-slide-toggle
|
|
1829
|
+
[ngModel]="axis.reorderable ?? false"
|
|
1830
|
+
(ngModelChange)="setAxisProp(key, 'reorderable', $event || undefined)"
|
|
1831
|
+
>
|
|
1832
|
+
Reordenável (arrastar)
|
|
1833
|
+
</mat-slide-toggle>
|
|
1834
|
+
}
|
|
1835
|
+
</div>
|
|
1836
|
+
|
|
1837
|
+
@if (axis.groups; as groups) {
|
|
1838
|
+
<div class="mkbd-groups">
|
|
1839
|
+
@for (group of groups; track $index; let i = $index) {
|
|
1840
|
+
<div class="mkbd-group">
|
|
1841
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1842
|
+
<mat-label>Id</mat-label>
|
|
1843
|
+
<input matInput [ngModel]="group.id" (ngModelChange)="setGroup(key, i, 'id', $event)" />
|
|
1844
|
+
</mat-form-field>
|
|
1845
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1846
|
+
<mat-label>Título</mat-label>
|
|
1847
|
+
<input matInput [ngModel]="group.title ?? ''" (ngModelChange)="setGroup(key, i, 'title', $event || undefined)" />
|
|
1848
|
+
</mat-form-field>
|
|
1849
|
+
<label class="mkbd-color">
|
|
1850
|
+
Cor
|
|
1851
|
+
<input
|
|
1852
|
+
type="color"
|
|
1853
|
+
[ngModel]="groupColor(group)"
|
|
1854
|
+
(ngModelChange)="setGroupColor(key, i, $event)"
|
|
1855
|
+
/>
|
|
1856
|
+
</label>
|
|
1857
|
+
<mat-form-field appearance="outline" class="mkbd-tiny">
|
|
1858
|
+
<mat-label>WIP</mat-label>
|
|
1859
|
+
<input
|
|
1860
|
+
matInput
|
|
1861
|
+
type="number"
|
|
1862
|
+
min="0"
|
|
1863
|
+
[ngModel]="group.limit ?? null"
|
|
1864
|
+
(ngModelChange)="setGroup(key, i, 'limit', $event === null || $event === '' ? undefined : +$event)"
|
|
1865
|
+
/>
|
|
1866
|
+
</mat-form-field>
|
|
1867
|
+
<mat-checkbox
|
|
1868
|
+
[ngModel]="group.locked ?? false"
|
|
1869
|
+
(ngModelChange)="setGroup(key, i, 'locked', $event || undefined)"
|
|
1870
|
+
>
|
|
1871
|
+
Travado
|
|
1872
|
+
</mat-checkbox>
|
|
1873
|
+
<span class="mkbd-spacer"></span>
|
|
1874
|
+
<button matIconButton type="button" (click)="moveGroup(key, i, -1)" [disabled]="i === 0" aria-label="Subir">↑</button>
|
|
1875
|
+
<button matIconButton type="button" (click)="moveGroup(key, i, 1)" [disabled]="i === groups.length - 1" aria-label="Descer">↓</button>
|
|
1876
|
+
<button matIconButton type="button" (click)="removeGroup(key, i)" aria-label="Remover grupo">✕</button>
|
|
1877
|
+
</div>
|
|
1878
|
+
}
|
|
1879
|
+
<button matButton type="button" (click)="addGroup(key)">+ Adicionar {{ key === 'columns' ? 'coluna' : 'lane' }}</button>
|
|
1880
|
+
</div>
|
|
1881
|
+
}
|
|
1882
|
+
</ng-template>
|
|
1883
|
+
|
|
1884
|
+
<!-- ======================= REGRAS ========================= -->
|
|
1885
|
+
<h3 class="mkbd-h">Regras de transição (colunas)</h3>
|
|
1886
|
+
<p class="mkbd-hint">
|
|
1887
|
+
Sem linhas = qualquer transição é permitida. Com linhas, SÓ as listadas passam.
|
|
1888
|
+
</p>
|
|
1889
|
+
@for (t of transitions(); track $index; let i = $index) {
|
|
1890
|
+
<div class="mkbd-row">
|
|
1891
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1892
|
+
<mat-label>De</mat-label>
|
|
1893
|
+
<mat-select [ngModel]="fromOf(t)" (ngModelChange)="setTransition(i, 'from', $event)">
|
|
1894
|
+
<mat-option value="*">Qualquer</mat-option>
|
|
1895
|
+
@for (id of columnIds(); track id) {
|
|
1896
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1897
|
+
}
|
|
1898
|
+
</mat-select>
|
|
1899
|
+
</mat-form-field>
|
|
1900
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1901
|
+
<mat-label>Para</mat-label>
|
|
1902
|
+
<mat-select multiple [ngModel]="toOf(t)" (ngModelChange)="setTransition(i, 'to', $event)">
|
|
1903
|
+
@for (id of columnIds(); track id) {
|
|
1904
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
1905
|
+
}
|
|
1906
|
+
</mat-select>
|
|
1907
|
+
</mat-form-field>
|
|
1908
|
+
<button matIconButton type="button" (click)="removeTransition(i)" aria-label="Remover transição">✕</button>
|
|
1909
|
+
</div>
|
|
1910
|
+
}
|
|
1911
|
+
<button matButton type="button" (click)="addTransition()">+ Adicionar transição</button>
|
|
1912
|
+
|
|
1913
|
+
<div class="mkbd-row mkbd-mt">
|
|
1914
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1915
|
+
<mat-label>Regras nomeadas de cartão (rule:*)</mat-label>
|
|
1916
|
+
<input
|
|
1917
|
+
matInput
|
|
1918
|
+
[ngModel]="namesOf('rules')"
|
|
1919
|
+
(ngModelChange)="setNames('rules', $event)"
|
|
1920
|
+
placeholder="rule:berco-livre, rule:sla"
|
|
1921
|
+
/>
|
|
1922
|
+
</mat-form-field>
|
|
1923
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
1924
|
+
<mat-label>Regras nomeadas de itens (entryRules)</mat-label>
|
|
1925
|
+
<input matInput [ngModel]="namesOf('entryRules')" (ngModelChange)="setNames('entryRules', $event)" />
|
|
1926
|
+
</mat-form-field>
|
|
1927
|
+
</div>
|
|
1928
|
+
<p class="mkbd-hint">Os nomes referenciam código registrado pelo host no KanbanRegistry.</p>
|
|
1929
|
+
|
|
1930
|
+
<!-- ================== ORDENAÇÃO / GERAL ==================== -->
|
|
1931
|
+
<h3 class="mkbd-h">Ordenação e geral</h3>
|
|
1932
|
+
<div class="mkbd-row">
|
|
1933
|
+
<mat-slide-toggle [ngModel]="!!config().sort" (ngModelChange)="toggleSort($event)">
|
|
1934
|
+
Ordenar itens por campo
|
|
1935
|
+
</mat-slide-toggle>
|
|
1936
|
+
@if (config().sort; as sort) {
|
|
1937
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1938
|
+
<mat-label>Campo</mat-label>
|
|
1939
|
+
<input matInput [ngModel]="sort.field" (ngModelChange)="setSort('field', $event)" />
|
|
1940
|
+
</mat-form-field>
|
|
1941
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1942
|
+
<mat-label>Direção</mat-label>
|
|
1943
|
+
<mat-select [ngModel]="sort.direction ?? 'asc'" (ngModelChange)="setSort('direction', $event)">
|
|
1944
|
+
<mat-option value="asc">Crescente</mat-option>
|
|
1945
|
+
<mat-option value="desc">Decrescente</mat-option>
|
|
1946
|
+
</mat-select>
|
|
1947
|
+
</mat-form-field>
|
|
1948
|
+
}
|
|
1949
|
+
<mat-slide-toggle
|
|
1950
|
+
[ngModel]="config().readOnly ?? false"
|
|
1951
|
+
(ngModelChange)="setReadOnly($event)"
|
|
1952
|
+
>
|
|
1953
|
+
Somente leitura
|
|
1954
|
+
</mat-slide-toggle>
|
|
1955
|
+
</div>
|
|
1956
|
+
`, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600;display:flex;align-items:center;gap:12px}.mkbd-h:first-child{margin-top:0}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:10px}.mkbd-groups{display:flex;flex-direction:column;gap:4px}.mkbd-group{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 8px;border:1px dashed var(--mkb-border, #e3e0ea);border-radius:8px}.mkbd-slim{width:170px}.mkbd-tiny{width:90px}.mkbd-grow{flex:1;min-width:200px}.mkbd-spacer{flex:1}.mkbd-color{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i5.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatExpansionModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1957
|
+
}
|
|
1958
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbPipelineEditor, decorators: [{
|
|
1959
|
+
type: Component,
|
|
1960
|
+
args: [{ selector: 'mkb-pipeline-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1961
|
+
NgTemplateOutlet,
|
|
1962
|
+
FormsModule,
|
|
1963
|
+
MatFormFieldModule,
|
|
1964
|
+
MatInputModule,
|
|
1965
|
+
MatSelectModule,
|
|
1966
|
+
MatSlideToggleModule,
|
|
1967
|
+
MatCheckboxModule,
|
|
1968
|
+
MatButtonModule,
|
|
1969
|
+
MatExpansionModule,
|
|
1970
|
+
], template: `
|
|
1971
|
+
<!-- ======================= COLUNAS ======================= -->
|
|
1972
|
+
<h3 class="mkbd-h">Colunas</h3>
|
|
1973
|
+
<ng-container
|
|
1974
|
+
*ngTemplateOutlet="axisEditor; context: { axis: store.columnsAxis(), key: 'columns' }"
|
|
1975
|
+
/>
|
|
1976
|
+
|
|
1977
|
+
<!-- ======================= LANES ========================= -->
|
|
1978
|
+
<h3 class="mkbd-h">
|
|
1979
|
+
Lanes (swimlanes)
|
|
1980
|
+
<mat-slide-toggle
|
|
1981
|
+
[ngModel]="!!store.lanesAxis()"
|
|
1982
|
+
(ngModelChange)="toggleLanes($event)"
|
|
1983
|
+
aria-label="Usar lanes"
|
|
1984
|
+
/>
|
|
1985
|
+
</h3>
|
|
1986
|
+
@if (store.lanesAxis(); as lanes) {
|
|
1987
|
+
<ng-container *ngTemplateOutlet="axisEditor; context: { axis: lanes, key: 'lanes' }" />
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
<!-- ================= EDITOR DE EIXO (template) ============ -->
|
|
1991
|
+
<ng-template #axisEditor let-axis="axis" let-key="key">
|
|
1992
|
+
<div class="mkbd-row">
|
|
1993
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
1994
|
+
<mat-label>Modo</mat-label>
|
|
1995
|
+
<mat-select
|
|
1996
|
+
[ngModel]="axis.groups ? 'static' : 'derived'"
|
|
1997
|
+
(ngModelChange)="setAxisMode(key, $event)"
|
|
1998
|
+
>
|
|
1999
|
+
<mat-option value="static">Estático (grupos declarados)</mat-option>
|
|
2000
|
+
<mat-option value="derived">Derivado (agrupamento dinâmico)</mat-option>
|
|
2001
|
+
</mat-select>
|
|
2002
|
+
</mat-form-field>
|
|
2003
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2004
|
+
<mat-label>Campo do item</mat-label>
|
|
2005
|
+
<input
|
|
2006
|
+
matInput
|
|
2007
|
+
[ngModel]="axis.field ?? ''"
|
|
2008
|
+
(ngModelChange)="setAxisProp(key, 'field', $event || undefined)"
|
|
2009
|
+
[placeholder]="key === 'columns' ? 'column' : 'lane'"
|
|
2010
|
+
/>
|
|
2011
|
+
</mat-form-field>
|
|
2012
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2013
|
+
<mat-label>Ordenar grupos</mat-label>
|
|
2014
|
+
<mat-select
|
|
2015
|
+
[ngModel]="axis.sortGroups ?? ''"
|
|
2016
|
+
(ngModelChange)="setAxisProp(key, 'sortGroups', $event || undefined)"
|
|
2017
|
+
>
|
|
2018
|
+
<mat-option value="">Ordem declarada / de aparição</mat-option>
|
|
2019
|
+
<mat-option value="asc">Crescente</mat-option>
|
|
2020
|
+
<mat-option value="desc">Decrescente</mat-option>
|
|
2021
|
+
</mat-select>
|
|
2022
|
+
</mat-form-field>
|
|
2023
|
+
@if (axis.groups) {
|
|
2024
|
+
<mat-slide-toggle
|
|
2025
|
+
[ngModel]="axis.reorderable ?? false"
|
|
2026
|
+
(ngModelChange)="setAxisProp(key, 'reorderable', $event || undefined)"
|
|
2027
|
+
>
|
|
2028
|
+
Reordenável (arrastar)
|
|
2029
|
+
</mat-slide-toggle>
|
|
2030
|
+
}
|
|
2031
|
+
</div>
|
|
2032
|
+
|
|
2033
|
+
@if (axis.groups; as groups) {
|
|
2034
|
+
<div class="mkbd-groups">
|
|
2035
|
+
@for (group of groups; track $index; let i = $index) {
|
|
2036
|
+
<div class="mkbd-group">
|
|
2037
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2038
|
+
<mat-label>Id</mat-label>
|
|
2039
|
+
<input matInput [ngModel]="group.id" (ngModelChange)="setGroup(key, i, 'id', $event)" />
|
|
2040
|
+
</mat-form-field>
|
|
2041
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2042
|
+
<mat-label>Título</mat-label>
|
|
2043
|
+
<input matInput [ngModel]="group.title ?? ''" (ngModelChange)="setGroup(key, i, 'title', $event || undefined)" />
|
|
2044
|
+
</mat-form-field>
|
|
2045
|
+
<label class="mkbd-color">
|
|
2046
|
+
Cor
|
|
2047
|
+
<input
|
|
2048
|
+
type="color"
|
|
2049
|
+
[ngModel]="groupColor(group)"
|
|
2050
|
+
(ngModelChange)="setGroupColor(key, i, $event)"
|
|
2051
|
+
/>
|
|
2052
|
+
</label>
|
|
2053
|
+
<mat-form-field appearance="outline" class="mkbd-tiny">
|
|
2054
|
+
<mat-label>WIP</mat-label>
|
|
2055
|
+
<input
|
|
2056
|
+
matInput
|
|
2057
|
+
type="number"
|
|
2058
|
+
min="0"
|
|
2059
|
+
[ngModel]="group.limit ?? null"
|
|
2060
|
+
(ngModelChange)="setGroup(key, i, 'limit', $event === null || $event === '' ? undefined : +$event)"
|
|
2061
|
+
/>
|
|
2062
|
+
</mat-form-field>
|
|
2063
|
+
<mat-checkbox
|
|
2064
|
+
[ngModel]="group.locked ?? false"
|
|
2065
|
+
(ngModelChange)="setGroup(key, i, 'locked', $event || undefined)"
|
|
2066
|
+
>
|
|
2067
|
+
Travado
|
|
2068
|
+
</mat-checkbox>
|
|
2069
|
+
<span class="mkbd-spacer"></span>
|
|
2070
|
+
<button matIconButton type="button" (click)="moveGroup(key, i, -1)" [disabled]="i === 0" aria-label="Subir">↑</button>
|
|
2071
|
+
<button matIconButton type="button" (click)="moveGroup(key, i, 1)" [disabled]="i === groups.length - 1" aria-label="Descer">↓</button>
|
|
2072
|
+
<button matIconButton type="button" (click)="removeGroup(key, i)" aria-label="Remover grupo">✕</button>
|
|
2073
|
+
</div>
|
|
2074
|
+
}
|
|
2075
|
+
<button matButton type="button" (click)="addGroup(key)">+ Adicionar {{ key === 'columns' ? 'coluna' : 'lane' }}</button>
|
|
2076
|
+
</div>
|
|
2077
|
+
}
|
|
2078
|
+
</ng-template>
|
|
2079
|
+
|
|
2080
|
+
<!-- ======================= REGRAS ========================= -->
|
|
2081
|
+
<h3 class="mkbd-h">Regras de transição (colunas)</h3>
|
|
2082
|
+
<p class="mkbd-hint">
|
|
2083
|
+
Sem linhas = qualquer transição é permitida. Com linhas, SÓ as listadas passam.
|
|
2084
|
+
</p>
|
|
2085
|
+
@for (t of transitions(); track $index; let i = $index) {
|
|
2086
|
+
<div class="mkbd-row">
|
|
2087
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2088
|
+
<mat-label>De</mat-label>
|
|
2089
|
+
<mat-select [ngModel]="fromOf(t)" (ngModelChange)="setTransition(i, 'from', $event)">
|
|
2090
|
+
<mat-option value="*">Qualquer</mat-option>
|
|
2091
|
+
@for (id of columnIds(); track id) {
|
|
2092
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
2093
|
+
}
|
|
2094
|
+
</mat-select>
|
|
2095
|
+
</mat-form-field>
|
|
2096
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
2097
|
+
<mat-label>Para</mat-label>
|
|
2098
|
+
<mat-select multiple [ngModel]="toOf(t)" (ngModelChange)="setTransition(i, 'to', $event)">
|
|
2099
|
+
@for (id of columnIds(); track id) {
|
|
2100
|
+
<mat-option [value]="id">{{ id }}</mat-option>
|
|
2101
|
+
}
|
|
2102
|
+
</mat-select>
|
|
2103
|
+
</mat-form-field>
|
|
2104
|
+
<button matIconButton type="button" (click)="removeTransition(i)" aria-label="Remover transição">✕</button>
|
|
2105
|
+
</div>
|
|
2106
|
+
}
|
|
2107
|
+
<button matButton type="button" (click)="addTransition()">+ Adicionar transição</button>
|
|
2108
|
+
|
|
2109
|
+
<div class="mkbd-row mkbd-mt">
|
|
2110
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
2111
|
+
<mat-label>Regras nomeadas de cartão (rule:*)</mat-label>
|
|
2112
|
+
<input
|
|
2113
|
+
matInput
|
|
2114
|
+
[ngModel]="namesOf('rules')"
|
|
2115
|
+
(ngModelChange)="setNames('rules', $event)"
|
|
2116
|
+
placeholder="rule:berco-livre, rule:sla"
|
|
2117
|
+
/>
|
|
2118
|
+
</mat-form-field>
|
|
2119
|
+
<mat-form-field appearance="outline" class="mkbd-grow">
|
|
2120
|
+
<mat-label>Regras nomeadas de itens (entryRules)</mat-label>
|
|
2121
|
+
<input matInput [ngModel]="namesOf('entryRules')" (ngModelChange)="setNames('entryRules', $event)" />
|
|
2122
|
+
</mat-form-field>
|
|
2123
|
+
</div>
|
|
2124
|
+
<p class="mkbd-hint">Os nomes referenciam código registrado pelo host no KanbanRegistry.</p>
|
|
2125
|
+
|
|
2126
|
+
<!-- ================== ORDENAÇÃO / GERAL ==================== -->
|
|
2127
|
+
<h3 class="mkbd-h">Ordenação e geral</h3>
|
|
2128
|
+
<div class="mkbd-row">
|
|
2129
|
+
<mat-slide-toggle [ngModel]="!!config().sort" (ngModelChange)="toggleSort($event)">
|
|
2130
|
+
Ordenar itens por campo
|
|
2131
|
+
</mat-slide-toggle>
|
|
2132
|
+
@if (config().sort; as sort) {
|
|
2133
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2134
|
+
<mat-label>Campo</mat-label>
|
|
2135
|
+
<input matInput [ngModel]="sort.field" (ngModelChange)="setSort('field', $event)" />
|
|
2136
|
+
</mat-form-field>
|
|
2137
|
+
<mat-form-field appearance="outline" class="mkbd-slim">
|
|
2138
|
+
<mat-label>Direção</mat-label>
|
|
2139
|
+
<mat-select [ngModel]="sort.direction ?? 'asc'" (ngModelChange)="setSort('direction', $event)">
|
|
2140
|
+
<mat-option value="asc">Crescente</mat-option>
|
|
2141
|
+
<mat-option value="desc">Decrescente</mat-option>
|
|
2142
|
+
</mat-select>
|
|
2143
|
+
</mat-form-field>
|
|
2144
|
+
}
|
|
2145
|
+
<mat-slide-toggle
|
|
2146
|
+
[ngModel]="config().readOnly ?? false"
|
|
2147
|
+
(ngModelChange)="setReadOnly($event)"
|
|
2148
|
+
>
|
|
2149
|
+
Somente leitura
|
|
2150
|
+
</mat-slide-toggle>
|
|
2151
|
+
</div>
|
|
2152
|
+
`, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600;display:flex;align-items:center;gap:12px}.mkbd-h:first-child{margin-top:0}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:10px}.mkbd-groups{display:flex;flex-direction:column;gap:4px}.mkbd-group{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 8px;border:1px dashed var(--mkb-border, #e3e0ea);border-radius:8px}.mkbd-slim{width:170px}.mkbd-tiny{width:90px}.mkbd-grow{flex:1;min-width:200px}.mkbd-spacer{flex:1}.mkbd-color{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}\n"] }]
|
|
2153
|
+
}] });
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Editor de TEMA: personaliza os tokens --mkb-* (espaçamento, cores, forma).
|
|
2157
|
+
* Só os tokens ALTERADOS entram na exportação (design JSON / CSS) — o resto
|
|
2158
|
+
* continua herdando os defaults de kanban-tokens.css e do DS do host.
|
|
2159
|
+
*/
|
|
2160
|
+
class MkbThemeEditor {
|
|
2161
|
+
store = inject(DesignerStore);
|
|
2162
|
+
groups = TOKEN_GROUPS;
|
|
2163
|
+
valueOf(def) {
|
|
2164
|
+
return this.store.theme()[def.token] ?? def.default;
|
|
2165
|
+
}
|
|
2166
|
+
isChanged(def) {
|
|
2167
|
+
return def.token in this.store.theme();
|
|
2168
|
+
}
|
|
2169
|
+
changedCount() {
|
|
2170
|
+
return Object.keys(this.store.theme()).length;
|
|
2171
|
+
}
|
|
2172
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbThemeEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2173
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbThemeEditor, isStandalone: true, selector: "mkb-theme-editor", ngImport: i0, template: `
|
|
2174
|
+
<p class="mkbd-hint">
|
|
2175
|
+
Só o que você ALTERAR entra na exportação; o resto herda o default do
|
|
2176
|
+
pacote e os tokens do Design System do host.
|
|
2177
|
+
</p>
|
|
2178
|
+
@for (group of groups; track group.label) {
|
|
2179
|
+
<h3 class="mkbd-h">{{ group.label }}</h3>
|
|
2180
|
+
<div class="mkbd-grid">
|
|
2181
|
+
@for (def of group.tokens; track def.token) {
|
|
2182
|
+
<div class="mkbd-token" [class.mkbd-token-changed]="isChanged(def)">
|
|
2183
|
+
@if (def.kind === 'color') {
|
|
2184
|
+
<label class="mkbd-color">
|
|
2185
|
+
<input
|
|
2186
|
+
type="color"
|
|
2187
|
+
[ngModel]="valueOf(def)"
|
|
2188
|
+
(ngModelChange)="store.setToken(def.token, $event)"
|
|
2189
|
+
[attr.aria-label]="def.label"
|
|
2190
|
+
/>
|
|
2191
|
+
<span>
|
|
2192
|
+
<strong>{{ def.label }}</strong>
|
|
2193
|
+
<code>{{ def.token }}</code>
|
|
2194
|
+
</span>
|
|
2195
|
+
</label>
|
|
2196
|
+
} @else {
|
|
2197
|
+
<mat-form-field appearance="outline" class="mkbd-field">
|
|
2198
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
2199
|
+
<input
|
|
2200
|
+
matInput
|
|
2201
|
+
[ngModel]="valueOf(def)"
|
|
2202
|
+
(ngModelChange)="store.setToken(def.token, $event)"
|
|
2203
|
+
[placeholder]="def.default"
|
|
2204
|
+
/>
|
|
2205
|
+
<mat-hint><code>{{ def.token }}</code></mat-hint>
|
|
2206
|
+
</mat-form-field>
|
|
2207
|
+
}
|
|
2208
|
+
@if (isChanged(def)) {
|
|
2209
|
+
<button matIconButton type="button" (click)="store.setToken(def.token, '')" [attr.aria-label]="'Restaurar ' + def.label">↺</button>
|
|
2210
|
+
}
|
|
2211
|
+
</div>
|
|
2212
|
+
}
|
|
2213
|
+
</div>
|
|
2214
|
+
}
|
|
2215
|
+
<div class="mkbd-actions">
|
|
2216
|
+
<button matButton type="button" (click)="store.resetTheme()" [disabled]="!changedCount()">
|
|
2217
|
+
Restaurar todos os defaults
|
|
2218
|
+
</button>
|
|
2219
|
+
<span class="mkbd-hint">{{ changedCount() }} token(s) alterado(s)</span>
|
|
2220
|
+
</div>
|
|
2221
|
+
`, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);margin:0}.mkbd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px 14px}.mkbd-token{display:flex;align-items:center;gap:6px;border-radius:8px;padding:2px 6px}.mkbd-token-changed{background:color-mix(in srgb,var(--mkb-accent, #5e35b1) 8%,transparent)}.mkbd-field{width:100%}.mkbd-color{display:flex;align-items:center;gap:10px;cursor:pointer;padding:4px 0}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}.mkbd-color span{display:flex;flex-direction:column;font-size:12px}.mkbd-color code,.mkbd-field code{font-size:10.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-actions{display:flex;align-items:center;gap:12px;margin-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2222
|
+
}
|
|
2223
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbThemeEditor, decorators: [{
|
|
2224
|
+
type: Component,
|
|
2225
|
+
args: [{ selector: 'mkb-theme-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule, MatFormFieldModule, MatInputModule, MatButtonModule], template: `
|
|
2226
|
+
<p class="mkbd-hint">
|
|
2227
|
+
Só o que você ALTERAR entra na exportação; o resto herda o default do
|
|
2228
|
+
pacote e os tokens do Design System do host.
|
|
2229
|
+
</p>
|
|
2230
|
+
@for (group of groups; track group.label) {
|
|
2231
|
+
<h3 class="mkbd-h">{{ group.label }}</h3>
|
|
2232
|
+
<div class="mkbd-grid">
|
|
2233
|
+
@for (def of group.tokens; track def.token) {
|
|
2234
|
+
<div class="mkbd-token" [class.mkbd-token-changed]="isChanged(def)">
|
|
2235
|
+
@if (def.kind === 'color') {
|
|
2236
|
+
<label class="mkbd-color">
|
|
2237
|
+
<input
|
|
2238
|
+
type="color"
|
|
2239
|
+
[ngModel]="valueOf(def)"
|
|
2240
|
+
(ngModelChange)="store.setToken(def.token, $event)"
|
|
2241
|
+
[attr.aria-label]="def.label"
|
|
2242
|
+
/>
|
|
2243
|
+
<span>
|
|
2244
|
+
<strong>{{ def.label }}</strong>
|
|
2245
|
+
<code>{{ def.token }}</code>
|
|
2246
|
+
</span>
|
|
2247
|
+
</label>
|
|
2248
|
+
} @else {
|
|
2249
|
+
<mat-form-field appearance="outline" class="mkbd-field">
|
|
2250
|
+
<mat-label>{{ def.label }}</mat-label>
|
|
2251
|
+
<input
|
|
2252
|
+
matInput
|
|
2253
|
+
[ngModel]="valueOf(def)"
|
|
2254
|
+
(ngModelChange)="store.setToken(def.token, $event)"
|
|
2255
|
+
[placeholder]="def.default"
|
|
2256
|
+
/>
|
|
2257
|
+
<mat-hint><code>{{ def.token }}</code></mat-hint>
|
|
2258
|
+
</mat-form-field>
|
|
2259
|
+
}
|
|
2260
|
+
@if (isChanged(def)) {
|
|
2261
|
+
<button matIconButton type="button" (click)="store.setToken(def.token, '')" [attr.aria-label]="'Restaurar ' + def.label">↺</button>
|
|
2262
|
+
}
|
|
2263
|
+
</div>
|
|
2264
|
+
}
|
|
2265
|
+
</div>
|
|
2266
|
+
}
|
|
2267
|
+
<div class="mkbd-actions">
|
|
2268
|
+
<button matButton type="button" (click)="store.resetTheme()" [disabled]="!changedCount()">
|
|
2269
|
+
Restaurar todos os defaults
|
|
2270
|
+
</button>
|
|
2271
|
+
<span class="mkbd-hint">{{ changedCount() }} token(s) alterado(s)</span>
|
|
2272
|
+
</div>
|
|
2273
|
+
`, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);margin:0}.mkbd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px 14px}.mkbd-token{display:flex;align-items:center;gap:6px;border-radius:8px;padding:2px 6px}.mkbd-token-changed{background:color-mix(in srgb,var(--mkb-accent, #5e35b1) 8%,transparent)}.mkbd-field{width:100%}.mkbd-color{display:flex;align-items:center;gap:10px;cursor:pointer;padding:4px 0}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}.mkbd-color span{display:flex;flex-direction:column;font-size:12px}.mkbd-color code,.mkbd-field code{font-size:10.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-actions{display:flex;align-items:center;gap:12px;margin-top:16px}\n"] }]
|
|
2274
|
+
}] });
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Designer do Board — tela OPT-IN (entry point isolado
|
|
2278
|
+
* `@mosaicoo/kanban/designer`): edita visualmente o BoardConfig (pipeline,
|
|
2279
|
+
* cartão, dados, tema) com preview ao vivo e importa/exporta no formato que
|
|
2280
|
+
* o kanban consome em runtime. Exige @angular/material + @angular/forms
|
|
2281
|
+
* (peers opcionais do pacote — quem não usa o designer não paga por eles).
|
|
2282
|
+
*/
|
|
2283
|
+
class MosKanbanDesigner {
|
|
2284
|
+
store = inject(DesignerStore);
|
|
2285
|
+
/** Config inicial (senão: rascunho salvo → default). */
|
|
2286
|
+
config = input(null, ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
|
|
2287
|
+
/** Tema inicial (tokens alterados). */
|
|
2288
|
+
theme = input(null, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
2289
|
+
/** Registry do host — deixa o preview resolver rule:, icon: e format:. */
|
|
2290
|
+
registry = input(undefined, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
|
|
2291
|
+
/** Chave do rascunho automático em localStorage; '' desliga. */
|
|
2292
|
+
storageKey = input('mkb-designer-draft', ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
|
|
2293
|
+
/** Config a cada mudança VÁLIDA (para persistência pelo host). */
|
|
2294
|
+
configChange = output();
|
|
2295
|
+
themeChange = output();
|
|
2296
|
+
importErrors = signal([], ...(ngDevMode ? [{ debugName: "importErrors" }] : /* istanbul ignore next */ []));
|
|
2297
|
+
initialized = false;
|
|
2298
|
+
constructor() {
|
|
2299
|
+
// Inicialização: input config > rascunho salvo > default do store.
|
|
2300
|
+
effect(() => {
|
|
2301
|
+
const config = this.config();
|
|
2302
|
+
const key = untracked(this.storageKey);
|
|
2303
|
+
if (!this.initialized) {
|
|
2304
|
+
this.initialized = true;
|
|
2305
|
+
if (config) {
|
|
2306
|
+
this.store.load(config, untracked(this.theme) ?? undefined);
|
|
2307
|
+
}
|
|
2308
|
+
else if (key) {
|
|
2309
|
+
this.store.loadDraft(key);
|
|
2310
|
+
}
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
if (config)
|
|
2314
|
+
this.store.load(config, untracked(this.theme) ?? undefined);
|
|
2315
|
+
});
|
|
2316
|
+
// Registry é referência viva (funções) — passa direto ao preview.
|
|
2317
|
+
effect(() => {
|
|
2318
|
+
this.store.registry = this.registry();
|
|
2319
|
+
});
|
|
2320
|
+
// Cada mudança válida: emite pro host + salva rascunho.
|
|
2321
|
+
effect(() => {
|
|
2322
|
+
const config = this.store.config();
|
|
2323
|
+
const theme = this.store.theme();
|
|
2324
|
+
if (!untracked(this.store.validation).valid)
|
|
2325
|
+
return;
|
|
2326
|
+
this.configChange.emit(config);
|
|
2327
|
+
this.themeChange.emit(theme);
|
|
2328
|
+
const key = untracked(this.storageKey);
|
|
2329
|
+
if (key)
|
|
2330
|
+
this.store.saveDraft(key);
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
hasTheme() {
|
|
2334
|
+
return Object.keys(this.store.theme()).length > 0;
|
|
2335
|
+
}
|
|
2336
|
+
// ---- import / export -----------------------------------------------------------
|
|
2337
|
+
onImportFile(event) {
|
|
2338
|
+
const inputEl = event.target;
|
|
2339
|
+
const file = inputEl.files?.[0];
|
|
2340
|
+
inputEl.value = '';
|
|
2341
|
+
if (!file)
|
|
2342
|
+
return;
|
|
2343
|
+
file.text().then((text) => {
|
|
2344
|
+
const result = this.store.import(text);
|
|
2345
|
+
this.importErrors.set(result.ok ? [] : result.errors);
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
applyJson(area) {
|
|
2349
|
+
const result = this.store.import(area.value);
|
|
2350
|
+
this.importErrors.set(result.ok ? [] : result.errors);
|
|
2351
|
+
// O textarea é editado pelo usuário, e o [value] só reescreve quando a
|
|
2352
|
+
// string ligada muda — sem isto ele fica com o texto digitado (design file
|
|
2353
|
+
// ou açúcar de eixo) em vez da config canônica que passou a valer.
|
|
2354
|
+
if (result.ok)
|
|
2355
|
+
area.value = this.store.exportConfigJson();
|
|
2356
|
+
}
|
|
2357
|
+
exportConfig() {
|
|
2358
|
+
this.download('board-config.json', this.store.exportConfigJson(), 'application/json');
|
|
2359
|
+
}
|
|
2360
|
+
exportDesign() {
|
|
2361
|
+
this.download('board-design.json', this.store.exportDesignJson(), 'application/json');
|
|
2362
|
+
}
|
|
2363
|
+
exportCss() {
|
|
2364
|
+
const css = this.store.exportThemeCss();
|
|
2365
|
+
if (css)
|
|
2366
|
+
this.download('kanban-theme.css', css, 'text/css');
|
|
2367
|
+
}
|
|
2368
|
+
clearDraft() {
|
|
2369
|
+
const key = this.storageKey();
|
|
2370
|
+
if (key)
|
|
2371
|
+
this.store.clearDraft(key);
|
|
2372
|
+
}
|
|
2373
|
+
download(name, content, mime) {
|
|
2374
|
+
const url = URL.createObjectURL(new Blob([content], { type: mime }));
|
|
2375
|
+
const a = document.createElement('a');
|
|
2376
|
+
a.href = url;
|
|
2377
|
+
a.download = name;
|
|
2378
|
+
a.click();
|
|
2379
|
+
URL.revokeObjectURL(url);
|
|
2380
|
+
}
|
|
2381
|
+
ngOnDestroy() {
|
|
2382
|
+
this.store.destroy();
|
|
2383
|
+
}
|
|
2384
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MosKanbanDesigner, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2385
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MosKanbanDesigner, isStandalone: true, selector: "mos-kanban-designer", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", themeChange: "themeChange" }, providers: [DesignerStore], ngImport: i0, template: `
|
|
2386
|
+
<div class="mkbd-toolbar">
|
|
2387
|
+
<span class="mkbd-title">Designer do Board</span>
|
|
2388
|
+
@if (store.errors().length; as count) {
|
|
2389
|
+
<span class="mkbd-chip mkbd-chip-error" role="status">{{ count }} erro(s)</span>
|
|
2390
|
+
} @else {
|
|
2391
|
+
<span class="mkbd-chip mkbd-chip-ok" role="status">Config válida</span>
|
|
2392
|
+
}
|
|
2393
|
+
<span class="mkbd-spacer"></span>
|
|
2394
|
+
<button matButton type="button" (click)="fileInput.click()">Importar</button>
|
|
2395
|
+
<input
|
|
2396
|
+
#fileInput
|
|
2397
|
+
type="file"
|
|
2398
|
+
accept=".json,application/json"
|
|
2399
|
+
hidden
|
|
2400
|
+
(change)="onImportFile($event)"
|
|
2401
|
+
/>
|
|
2402
|
+
<button matButton type="button" (click)="exportConfig()">Exportar config</button>
|
|
2403
|
+
<button matButton type="button" (click)="exportDesign()">Exportar design</button>
|
|
2404
|
+
<button matButton type="button" (click)="exportCss()" [disabled]="!hasTheme()">Exportar CSS</button>
|
|
2405
|
+
@if (storageKey()) {
|
|
2406
|
+
<button matButton type="button" (click)="clearDraft()">Limpar rascunho</button>
|
|
2407
|
+
}
|
|
2408
|
+
</div>
|
|
2409
|
+
|
|
2410
|
+
@if (importErrors().length) {
|
|
2411
|
+
<div class="mkbd-import-errors" role="alert">
|
|
2412
|
+
<strong>Importação rejeitada:</strong>
|
|
2413
|
+
<ul>
|
|
2414
|
+
@for (issue of importErrors(); track $index) {
|
|
2415
|
+
<li><code>{{ issue.path }}</code> {{ issue.message }}</li>
|
|
2416
|
+
}
|
|
2417
|
+
</ul>
|
|
2418
|
+
<button matButton type="button" (click)="importErrors.set([])">Fechar</button>
|
|
2419
|
+
</div>
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
<div class="mkbd-body">
|
|
2423
|
+
<div class="mkbd-editors">
|
|
2424
|
+
<mat-tab-group stretchTabs="false" animationDuration="0ms">
|
|
2425
|
+
<mat-tab label="Pipeline"><mkb-pipeline-editor /></mat-tab>
|
|
2426
|
+
<mat-tab label="Cartão"><mkb-card-editor /></mat-tab>
|
|
2427
|
+
<mat-tab label="Dados"><mkb-data-editor /></mat-tab>
|
|
2428
|
+
<mat-tab label="Tema"><mkb-theme-editor /></mat-tab>
|
|
2429
|
+
<mat-tab label="JSON">
|
|
2430
|
+
<p class="mkbd-hint">
|
|
2431
|
+
BoardConfig ao vivo. Edite e aplique — inválido não substitui o design atual.
|
|
2432
|
+
</p>
|
|
2433
|
+
<textarea
|
|
2434
|
+
class="mkbd-json"
|
|
2435
|
+
rows="24"
|
|
2436
|
+
spellcheck="false"
|
|
2437
|
+
[value]="store.exportConfigJson()"
|
|
2438
|
+
#jsonArea
|
|
2439
|
+
></textarea>
|
|
2440
|
+
<button matButton type="button" (click)="applyJson(jsonArea)">Aplicar JSON</button>
|
|
2441
|
+
</mat-tab>
|
|
2442
|
+
</mat-tab-group>
|
|
2443
|
+
</div>
|
|
2444
|
+
|
|
2445
|
+
<div class="mkbd-preview">
|
|
2446
|
+
<div class="mkbd-preview-head">
|
|
2447
|
+
<span>Preview ao vivo</span>
|
|
2448
|
+
@if (store.errors().length) {
|
|
2449
|
+
<span class="mkbd-hint">mostrando a última config válida</span>
|
|
2450
|
+
}
|
|
2451
|
+
</div>
|
|
2452
|
+
@if (store.errors().length) {
|
|
2453
|
+
<ul class="mkbd-issues">
|
|
2454
|
+
@for (issue of store.errors(); track $index) {
|
|
2455
|
+
<li><code>{{ issue.path }}</code> {{ issue.message }}</li>
|
|
2456
|
+
}
|
|
2457
|
+
</ul>
|
|
2458
|
+
}
|
|
2459
|
+
<div class="mkbd-preview-board" [style]="store.themeStyle()">
|
|
2460
|
+
<mos-kanban-board [board]="store.previewBoard()" />
|
|
2461
|
+
</div>
|
|
2462
|
+
</div>
|
|
2463
|
+
</div>
|
|
2464
|
+
`, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;font-family:var(--mkb-font, inherit)}.mkbd-toolbar{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--mkb-border, #e3e0ea);flex-wrap:wrap}.mkbd-title{font-size:14px;font-weight:600}.mkbd-chip{font-size:11px;padding:2px 10px;border-radius:999px}.mkbd-chip-ok{background:color-mix(in srgb,var(--mkb-success, #1b6e3c) 12%,transparent);color:var(--mkb-success, #1b6e3c)}.mkbd-chip-error{background:color-mix(in srgb,var(--mkb-danger, #b3261e) 12%,transparent);color:var(--mkb-danger, #b3261e)}.mkbd-spacer{flex:1}.mkbd-import-errors{margin:8px 12px;padding:8px 12px;border:1px solid var(--mkb-danger, #b3261e);border-radius:8px;font-size:12px}.mkbd-import-errors ul{margin:4px 0;padding-left:18px}.mkbd-body{display:flex;flex:1;min-height:0}.mkbd-editors{flex:1 1 46%;min-width:340px;overflow:auto;padding:8px 12px 24px;border-right:1px solid var(--mkb-border, #e3e0ea)}.mkbd-preview{flex:1 1 54%;min-width:0;display:flex;flex-direction:column}.mkbd-preview-head{display:flex;align-items:baseline;gap:10px;padding:8px 12px 4px;font-size:12px;font-weight:600}.mkbd-preview-board{flex:1;min-height:0;overflow:auto}.mkbd-issues{margin:0 12px 6px;padding:6px 12px 6px 26px;font-size:12px;color:var(--mkb-danger, #b3261e);border:1px dashed var(--mkb-danger, #b3261e);border-radius:8px}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-json{width:100%;font-family:ui-monospace,monospace;font-size:12px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;padding:8px;box-sizing:border-box}@media(max-width:900px){.mkbd-body{flex-direction:column}.mkbd-editors{border-right:none;border-bottom:1px solid var(--mkb-border, #e3e0ea)}}\n"], dependencies: [{ kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i1$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i1$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MosKanbanBoard, selector: "mos-kanban-board", inputs: ["board", "loading", "error", "showSearch", "itemActions"], outputs: ["itemClick", "itemMove", "moveRejected", "itemAction", "entryAction", "dragStarted", "dragEnded", "dragCanceled"] }, { kind: "component", type: MkbPipelineEditor, selector: "mkb-pipeline-editor" }, { kind: "component", type: MkbCardEditor, selector: "mkb-card-editor" }, { kind: "component", type: MkbDataEditor, selector: "mkb-data-editor" }, { kind: "component", type: MkbThemeEditor, selector: "mkb-theme-editor" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2465
|
+
}
|
|
2466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MosKanbanDesigner, decorators: [{
|
|
2467
|
+
type: Component,
|
|
2468
|
+
args: [{ selector: 'mos-kanban-designer', changeDetection: ChangeDetectionStrategy.OnPush, providers: [DesignerStore], imports: [
|
|
2469
|
+
MatTabsModule,
|
|
2470
|
+
MatButtonModule,
|
|
2471
|
+
MosKanbanBoard,
|
|
2472
|
+
MkbPipelineEditor,
|
|
2473
|
+
MkbCardEditor,
|
|
2474
|
+
MkbDataEditor,
|
|
2475
|
+
MkbThemeEditor,
|
|
2476
|
+
], template: `
|
|
2477
|
+
<div class="mkbd-toolbar">
|
|
2478
|
+
<span class="mkbd-title">Designer do Board</span>
|
|
2479
|
+
@if (store.errors().length; as count) {
|
|
2480
|
+
<span class="mkbd-chip mkbd-chip-error" role="status">{{ count }} erro(s)</span>
|
|
2481
|
+
} @else {
|
|
2482
|
+
<span class="mkbd-chip mkbd-chip-ok" role="status">Config válida</span>
|
|
2483
|
+
}
|
|
2484
|
+
<span class="mkbd-spacer"></span>
|
|
2485
|
+
<button matButton type="button" (click)="fileInput.click()">Importar</button>
|
|
2486
|
+
<input
|
|
2487
|
+
#fileInput
|
|
2488
|
+
type="file"
|
|
2489
|
+
accept=".json,application/json"
|
|
2490
|
+
hidden
|
|
2491
|
+
(change)="onImportFile($event)"
|
|
2492
|
+
/>
|
|
2493
|
+
<button matButton type="button" (click)="exportConfig()">Exportar config</button>
|
|
2494
|
+
<button matButton type="button" (click)="exportDesign()">Exportar design</button>
|
|
2495
|
+
<button matButton type="button" (click)="exportCss()" [disabled]="!hasTheme()">Exportar CSS</button>
|
|
2496
|
+
@if (storageKey()) {
|
|
2497
|
+
<button matButton type="button" (click)="clearDraft()">Limpar rascunho</button>
|
|
2498
|
+
}
|
|
2499
|
+
</div>
|
|
2500
|
+
|
|
2501
|
+
@if (importErrors().length) {
|
|
2502
|
+
<div class="mkbd-import-errors" role="alert">
|
|
2503
|
+
<strong>Importação rejeitada:</strong>
|
|
2504
|
+
<ul>
|
|
2505
|
+
@for (issue of importErrors(); track $index) {
|
|
2506
|
+
<li><code>{{ issue.path }}</code> {{ issue.message }}</li>
|
|
2507
|
+
}
|
|
2508
|
+
</ul>
|
|
2509
|
+
<button matButton type="button" (click)="importErrors.set([])">Fechar</button>
|
|
2510
|
+
</div>
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
<div class="mkbd-body">
|
|
2514
|
+
<div class="mkbd-editors">
|
|
2515
|
+
<mat-tab-group stretchTabs="false" animationDuration="0ms">
|
|
2516
|
+
<mat-tab label="Pipeline"><mkb-pipeline-editor /></mat-tab>
|
|
2517
|
+
<mat-tab label="Cartão"><mkb-card-editor /></mat-tab>
|
|
2518
|
+
<mat-tab label="Dados"><mkb-data-editor /></mat-tab>
|
|
2519
|
+
<mat-tab label="Tema"><mkb-theme-editor /></mat-tab>
|
|
2520
|
+
<mat-tab label="JSON">
|
|
2521
|
+
<p class="mkbd-hint">
|
|
2522
|
+
BoardConfig ao vivo. Edite e aplique — inválido não substitui o design atual.
|
|
2523
|
+
</p>
|
|
2524
|
+
<textarea
|
|
2525
|
+
class="mkbd-json"
|
|
2526
|
+
rows="24"
|
|
2527
|
+
spellcheck="false"
|
|
2528
|
+
[value]="store.exportConfigJson()"
|
|
2529
|
+
#jsonArea
|
|
2530
|
+
></textarea>
|
|
2531
|
+
<button matButton type="button" (click)="applyJson(jsonArea)">Aplicar JSON</button>
|
|
2532
|
+
</mat-tab>
|
|
2533
|
+
</mat-tab-group>
|
|
2534
|
+
</div>
|
|
2535
|
+
|
|
2536
|
+
<div class="mkbd-preview">
|
|
2537
|
+
<div class="mkbd-preview-head">
|
|
2538
|
+
<span>Preview ao vivo</span>
|
|
2539
|
+
@if (store.errors().length) {
|
|
2540
|
+
<span class="mkbd-hint">mostrando a última config válida</span>
|
|
2541
|
+
}
|
|
2542
|
+
</div>
|
|
2543
|
+
@if (store.errors().length) {
|
|
2544
|
+
<ul class="mkbd-issues">
|
|
2545
|
+
@for (issue of store.errors(); track $index) {
|
|
2546
|
+
<li><code>{{ issue.path }}</code> {{ issue.message }}</li>
|
|
2547
|
+
}
|
|
2548
|
+
</ul>
|
|
2549
|
+
}
|
|
2550
|
+
<div class="mkbd-preview-board" [style]="store.themeStyle()">
|
|
2551
|
+
<mos-kanban-board [board]="store.previewBoard()" />
|
|
2552
|
+
</div>
|
|
2553
|
+
</div>
|
|
2554
|
+
</div>
|
|
2555
|
+
`, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;font-family:var(--mkb-font, inherit)}.mkbd-toolbar{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--mkb-border, #e3e0ea);flex-wrap:wrap}.mkbd-title{font-size:14px;font-weight:600}.mkbd-chip{font-size:11px;padding:2px 10px;border-radius:999px}.mkbd-chip-ok{background:color-mix(in srgb,var(--mkb-success, #1b6e3c) 12%,transparent);color:var(--mkb-success, #1b6e3c)}.mkbd-chip-error{background:color-mix(in srgb,var(--mkb-danger, #b3261e) 12%,transparent);color:var(--mkb-danger, #b3261e)}.mkbd-spacer{flex:1}.mkbd-import-errors{margin:8px 12px;padding:8px 12px;border:1px solid var(--mkb-danger, #b3261e);border-radius:8px;font-size:12px}.mkbd-import-errors ul{margin:4px 0;padding-left:18px}.mkbd-body{display:flex;flex:1;min-height:0}.mkbd-editors{flex:1 1 46%;min-width:340px;overflow:auto;padding:8px 12px 24px;border-right:1px solid var(--mkb-border, #e3e0ea)}.mkbd-preview{flex:1 1 54%;min-width:0;display:flex;flex-direction:column}.mkbd-preview-head{display:flex;align-items:baseline;gap:10px;padding:8px 12px 4px;font-size:12px;font-weight:600}.mkbd-preview-board{flex:1;min-height:0;overflow:auto}.mkbd-issues{margin:0 12px 6px;padding:6px 12px 6px 26px;font-size:12px;color:var(--mkb-danger, #b3261e);border:1px dashed var(--mkb-danger, #b3261e);border-radius:8px}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-json{width:100%;font-family:ui-monospace,monospace;font-size:12px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;padding:8px;box-sizing:border-box}@media(max-width:900px){.mkbd-body{flex-direction:column}.mkbd-editors{border-right:none;border-bottom:1px solid var(--mkb-border, #e3e0ea)}}\n"] }]
|
|
2556
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }], configChange: [{ type: i0.Output, args: ["configChange"] }], themeChange: [{ type: i0.Output, args: ["themeChange"] }] } });
|
|
2557
|
+
|
|
2558
|
+
/**
|
|
2559
|
+
* @mosaicoo/kanban/designer — tela OPT-IN de design do board.
|
|
2560
|
+
*
|
|
2561
|
+
* Entry point ISOLADO: só quem importa daqui paga por @angular/material e
|
|
2562
|
+
* @angular/forms (peers opcionais). O runtime (ui/adapters/core) não depende
|
|
2563
|
+
* de nada deste entry point.
|
|
2564
|
+
*/
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* Generated bundle index. Do not edit.
|
|
2568
|
+
*/
|
|
2569
|
+
|
|
2570
|
+
export { BLOCK_FIELDS, BLOCK_TYPE_LABELS, DesignerStore, MosKanbanDesigner, TOKEN_GROUPS };
|