@luanpdd/kit-mcp 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +168 -168
- package/gates/agent-no-recursive-dispatch.md +82 -82
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +76 -76
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/audit-log-implementer.md +313 -313
- package/kit/agents/auditor-consistencia-isolamento.md +413 -413
- package/kit/agents/b2b-saas-architect.md +156 -156
- package/kit/agents/cascading-failures-auditor.md +298 -298
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/crm-pipeline-implementer.md +256 -256
- package/kit/agents/debugger.md +813 -813
- package/kit/agents/detector-tenant-quente.md +337 -337
- package/kit/agents/evolution-go-integrator.md +200 -200
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +564 -564
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/invite-flow-implementer.md +189 -189
- package/kit/agents/legacy-characterizer.md +368 -368
- package/kit/agents/lgpd-compliance-auditor.md +295 -295
- package/kit/agents/multi-tenant-isolation-auditor.md +253 -253
- package/kit/agents/multi-tenant-rls-writer.md +340 -340
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/observability-coverage-auditor.md +315 -315
- package/kit/agents/org-onboarding-implementer.md +223 -223
- package/kit/agents/payload-capture-instrumenter.md +273 -273
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +922 -922
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/refactor-safety-auditor.md +404 -404
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- package/kit/agents/seam-finder.md +359 -359
- package/kit/agents/shotgun-surgery-detector.md +349 -349
- package/kit/agents/supabase-branching-architect.md +562 -562
- package/kit/agents/supabase-cicd-pipeline-implementer.md +777 -777
- package/kit/agents/supabase-column-privileges-writer.md +399 -399
- package/kit/agents/supabase-edge-fn-tester.md +287 -0
- package/kit/agents/supabase-edge-fn-writer.md +239 -210
- package/kit/agents/supabase-migration-writer.md +385 -385
- package/kit/agents/supabase-rbac-implementer.md +392 -392
- package/kit/agents/supabase-realtime-implementer.md +363 -267
- package/kit/agents/supabase-rls-hardener.md +521 -521
- package/kit/agents/supabase-rls-writer.md +323 -323
- package/kit/agents/supabase-roles-implementer.md +355 -355
- package/kit/agents/super-admin-implementer.md +281 -281
- package/kit/agents/ui-auditor.md +437 -437
- package/kit/agents/ui-checker.md +302 -302
- package/kit/agents/ui-researcher.md +355 -355
- package/kit/agents/user-profiler.md +175 -175
- package/kit/agents/validador-evolucao-schema.md +335 -335
- package/kit/agents/verifier.md +728 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-cascading.md +111 -111
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
- package/kit/commands/auditar-refactor.md +219 -219
- package/kit/commands/auditar-release.md +109 -109
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/burn-rate-status.md +408 -408
- package/kit/commands/capturar-payloads.md +193 -193
- package/kit/commands/caracterizar.md +212 -212
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +188 -188
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/detectar-duplicacao.md +197 -197
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/encontrar-seams.md +136 -136
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/legacy.md +263 -263
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/load-shedding.md +117 -117
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/multi-tenant.md +163 -163
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/refactor-seguro.md +321 -321
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/storytelling.md +179 -179
- package/kit/commands/supabase.md +30 -7
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +14 -8
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/_shared-supabase/glossary.md +17 -0
- package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
- package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
- package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
- package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
- package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
- package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
- package/kit/skills/legacy-extract-class/SKILL.md +203 -203
- package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
- package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
- package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
- package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
- package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
- package/kit/skills/member-invite-flow/SKILL.md +305 -305
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
- package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
- package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
- package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
- package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
- package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
- package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
- package/kit/skills/supabase-edge-functions/SKILL.md +229 -141
- package/kit/skills/supabase-edge-functions-auth/SKILL.md +309 -0
- package/kit/skills/supabase-edge-functions-limits/SKILL.md +302 -0
- package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +279 -0
- package/kit/skills/supabase-edge-functions-testing/SKILL.md +277 -0
- package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +357 -0
- package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
- package/kit/skills/supabase-migrations/SKILL.md +297 -297
- package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
- package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
- package/kit/skills/supabase-realtime/SKILL.md +460 -236
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
- package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
- package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
- package/package.json +1 -1
- package/src/core/kit.js +216 -216
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +418 -418
- package/src/core/watch.js +121 -121
- package/src/mcp-server/index.js +693 -693
|
@@ -1,328 +1,328 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: member-management-react-shadcn
|
|
3
|
-
description: Use ao implementar UI de member management em B2B SaaS multi-tenant React + shadcn/ui…
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Member Management — React + shadcn/ui
|
|
7
|
-
|
|
8
|
-
## Quando usar
|
|
9
|
-
|
|
10
|
-
LLM carrega esta skill ao implementar painel UI de member management. Trigger phrases:
|
|
11
|
-
|
|
12
|
-
- "member management UI", "painel de membros React"
|
|
13
|
-
- "data table TanStack shadcn", "members list with filters"
|
|
14
|
-
- "invite member dialog", "role assignment dropdown"
|
|
15
|
-
- "shadcn member admin"
|
|
16
|
-
|
|
17
|
-
## Regras absolutas
|
|
18
|
-
|
|
19
|
-
**REGRA #1 (composição canônica de 9 componentes shadcn):** Painel canônico usa: `data-table` (TanStack v8), `dialog`, `select`, `badge`, `dropdown-menu`, `avatar`, `command` (search palette), `form`, `toast`. Adicionar mais componentes só com justificativa.
|
|
20
|
-
|
|
21
|
-
**REGRA #2 (PermissionGate em todo botão de ação):** Cada ação (invite, remove, role change) vem wrapped em `<PermissionGate>` com permission relevante. Member sem permission não vê botão.
|
|
22
|
-
|
|
23
|
-
**REGRA #3 (server-side filters via Supabase, não client):** Filtros (role, status, search) executados via Supabase query (`.eq`, `.ilike`), não em client-side. Performance + RLS preservada.
|
|
24
|
-
|
|
25
|
-
**REGRA #4 (optimistic UI com rollback):** Operações de role change/remove fazem optimistic update + rollback em error. Toast de feedback via shadcn `toast` em ambos casos.
|
|
26
|
-
|
|
27
|
-
## Patterns canônicos
|
|
28
|
-
|
|
29
|
-
### Members DataTable
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
// app/orgs/[slug]/members/MembersTable.tsx
|
|
33
|
-
'use client'
|
|
34
|
-
|
|
35
|
-
import { useState, useMemo } from 'react'
|
|
36
|
-
import {
|
|
37
|
-
ColumnDef,
|
|
38
|
-
useReactTable,
|
|
39
|
-
getCoreRowModel,
|
|
40
|
-
getFilteredRowModel,
|
|
41
|
-
flexRender
|
|
42
|
-
} from '@tanstack/react-table'
|
|
43
|
-
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table'
|
|
44
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
45
|
-
import { Badge } from '@/components/ui/badge'
|
|
46
|
-
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
|
47
|
-
import { Button } from '@/components/ui/button'
|
|
48
|
-
import { MoreHorizontal } from 'lucide-react'
|
|
49
|
-
import { PermissionGate } from '@/components/PermissionGate'
|
|
50
|
-
import { useMembers } from '@/lib/hooks/use-members'
|
|
51
|
-
import { ChangeRoleDialog } from './ChangeRoleDialog'
|
|
52
|
-
import { RemoveMemberConfirm } from './RemoveMemberConfirm'
|
|
53
|
-
|
|
54
|
-
interface Member {
|
|
55
|
-
id: string
|
|
56
|
-
user: { id: string; email: string; name: string; avatar_url?: string }
|
|
57
|
-
role: { id: string; name: string }
|
|
58
|
-
status: 'active' | 'suspended' | 'left'
|
|
59
|
-
joined_at: string
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const columns: ColumnDef<Member>[] = [
|
|
63
|
-
{
|
|
64
|
-
id: 'avatar',
|
|
65
|
-
cell: ({ row }) => (
|
|
66
|
-
<Avatar>
|
|
67
|
-
<AvatarImage src={row.original.user.avatar_url} />
|
|
68
|
-
<AvatarFallback>{row.original.user.name?.[0] || '?'}</AvatarFallback>
|
|
69
|
-
</Avatar>
|
|
70
|
-
)
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
accessorKey: 'user.name',
|
|
74
|
-
header: 'Nome',
|
|
75
|
-
cell: ({ row }) => (
|
|
76
|
-
<div>
|
|
77
|
-
<div className="font-medium">{row.original.user.name}</div>
|
|
78
|
-
<div className="text-sm text-muted-foreground">{row.original.user.email}</div>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
accessorKey: 'role.name',
|
|
84
|
-
header: 'Role',
|
|
85
|
-
cell: ({ row }) => (
|
|
86
|
-
<Badge variant={row.original.role.name === 'owner' ? 'default' : 'secondary'}>
|
|
87
|
-
{row.original.role.name}
|
|
88
|
-
</Badge>
|
|
89
|
-
)
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
accessorKey: 'status',
|
|
93
|
-
header: 'Status',
|
|
94
|
-
cell: ({ row }) => (
|
|
95
|
-
<Badge variant={row.original.status === 'active' ? 'default' : 'destructive'}>
|
|
96
|
-
{row.original.status}
|
|
97
|
-
</Badge>
|
|
98
|
-
)
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
accessorKey: 'joined_at',
|
|
102
|
-
header: 'Entrou em',
|
|
103
|
-
cell: ({ row }) => new Date(row.original.joined_at).toLocaleDateString('pt-BR')
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'actions',
|
|
107
|
-
cell: ({ row }) => <MemberActions member={row.original} />
|
|
108
|
-
}
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
function MemberActions({ member }: { member: Member }) {
|
|
112
|
-
const [changeRoleOpen, setChangeRoleOpen] = useState(false)
|
|
113
|
-
const [removeOpen, setRemoveOpen] = useState(false)
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<>
|
|
117
|
-
<DropdownMenu>
|
|
118
|
-
<DropdownMenuTrigger asChild>
|
|
119
|
-
<Button variant="ghost" size="icon"><MoreHorizontal /></Button>
|
|
120
|
-
</DropdownMenuTrigger>
|
|
121
|
-
<DropdownMenuContent>
|
|
122
|
-
<PermissionGate permission="update:members">
|
|
123
|
-
<DropdownMenuItem onClick={() => setChangeRoleOpen(true)}>Alterar role</DropdownMenuItem>
|
|
124
|
-
</PermissionGate>
|
|
125
|
-
<PermissionGate permission="remove:members">
|
|
126
|
-
<DropdownMenuItem onClick={() => setRemoveOpen(true)} className="text-destructive">
|
|
127
|
-
Remover
|
|
128
|
-
</DropdownMenuItem>
|
|
129
|
-
</PermissionGate>
|
|
130
|
-
</DropdownMenuContent>
|
|
131
|
-
</DropdownMenu>
|
|
132
|
-
|
|
133
|
-
<ChangeRoleDialog member={member} open={changeRoleOpen} onOpenChange={setChangeRoleOpen} />
|
|
134
|
-
<RemoveMemberConfirm member={member} open={removeOpen} onOpenChange={setRemoveOpen} />
|
|
135
|
-
</>
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function MembersTable() {
|
|
140
|
-
const { data: members } = useMembers()
|
|
141
|
-
const table = useReactTable({
|
|
142
|
-
data: members || [],
|
|
143
|
-
columns,
|
|
144
|
-
getCoreRowModel: getCoreRowModel(),
|
|
145
|
-
getFilteredRowModel: getFilteredRowModel()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<Table>
|
|
150
|
-
<TableHeader>
|
|
151
|
-
{table.getHeaderGroups().map(hg => (
|
|
152
|
-
<TableRow key={hg.id}>
|
|
153
|
-
{hg.headers.map(h => (
|
|
154
|
-
<TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</TableHead>
|
|
155
|
-
))}
|
|
156
|
-
</TableRow>
|
|
157
|
-
))}
|
|
158
|
-
</TableHeader>
|
|
159
|
-
<TableBody>
|
|
160
|
-
{table.getRowModel().rows.map(row => (
|
|
161
|
-
<TableRow key={row.id}>
|
|
162
|
-
{row.getVisibleCells().map(cell => (
|
|
163
|
-
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
|
164
|
-
))}
|
|
165
|
-
</TableRow>
|
|
166
|
-
))}
|
|
167
|
-
</TableBody>
|
|
168
|
-
</Table>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Invite Dialog (REGRA #1: Dialog + Form)
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
// app/orgs/[slug]/members/InviteMemberDialog.tsx
|
|
177
|
-
'use client'
|
|
178
|
-
|
|
179
|
-
import { useState } from 'react'
|
|
180
|
-
import { useForm } from 'react-hook-form'
|
|
181
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
182
|
-
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
|
|
183
|
-
import { Input } from '@/components/ui/input'
|
|
184
|
-
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
|
|
185
|
-
import { Button } from '@/components/ui/button'
|
|
186
|
-
import { useToast } from '@/components/ui/use-toast'
|
|
187
|
-
import { supabase } from '@/lib/supabase'
|
|
188
|
-
import { useOrgStore } from '@/lib/stores/org-store'
|
|
189
|
-
import { useAssignableRoles } from '@/lib/hooks/use-assignable-roles'
|
|
190
|
-
|
|
191
|
-
export function InviteMemberDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) {
|
|
192
|
-
const { toast } = useToast()
|
|
193
|
-
const orgId = useOrgStore(s => s.activeOrgId)
|
|
194
|
-
const { data: roles } = useAssignableRoles(orgId!)
|
|
195
|
-
|
|
196
|
-
const form = useForm({ defaultValues: { email: '', roleName: 'member' } })
|
|
197
|
-
|
|
198
|
-
async function onSubmit({ email, roleName }: { email: string; roleName: string }) {
|
|
199
|
-
const { data: token, error } = await supabase.rpc('create_invite', {
|
|
200
|
-
p_org_id: orgId!,
|
|
201
|
-
p_email: email,
|
|
202
|
-
p_role_name: roleName
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
if (error) {
|
|
206
|
-
toast({ title: 'Erro ao criar invite', description: error.message, variant: 'destructive' })
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Enviar email via Edge Function
|
|
211
|
-
await supabase.functions.invoke('send-invite-email', {
|
|
212
|
-
body: { email, token, base_url: window.location.origin }
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
toast({ title: 'Invite enviado', description: `Email de convite enviado a ${email}` })
|
|
216
|
-
onOpenChange(false)
|
|
217
|
-
form.reset()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return (
|
|
221
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
222
|
-
<DialogContent>
|
|
223
|
-
<DialogHeader>
|
|
224
|
-
<DialogTitle>Convidar membro</DialogTitle>
|
|
225
|
-
</DialogHeader>
|
|
226
|
-
<Form {...form}>
|
|
227
|
-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
228
|
-
<FormField name="email" rules={{ required: 'Email obrigatório', pattern: /^.+@.+\..+$/ }} render={({ field }) => (
|
|
229
|
-
<FormItem>
|
|
230
|
-
<FormLabel>Email</FormLabel>
|
|
231
|
-
<FormControl><Input type="email" {...field} /></FormControl>
|
|
232
|
-
<FormMessage />
|
|
233
|
-
</FormItem>
|
|
234
|
-
)} />
|
|
235
|
-
<FormField name="roleName" render={({ field }) => (
|
|
236
|
-
<FormItem>
|
|
237
|
-
<FormLabel>Role</FormLabel>
|
|
238
|
-
<Select onValueChange={field.onChange} value={field.value}>
|
|
239
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
240
|
-
<SelectContent>
|
|
241
|
-
{roles?.map(r => <SelectItem key={r.id} value={r.name}>{r.name}</SelectItem>)}
|
|
242
|
-
</SelectContent>
|
|
243
|
-
</Select>
|
|
244
|
-
<FormMessage />
|
|
245
|
-
</FormItem>
|
|
246
|
-
)} />
|
|
247
|
-
<DialogFooter>
|
|
248
|
-
<Button type="submit">Enviar convite</Button>
|
|
249
|
-
</DialogFooter>
|
|
250
|
-
</form>
|
|
251
|
-
</Form>
|
|
252
|
-
</DialogContent>
|
|
253
|
-
</Dialog>
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Optimistic UI com rollback (REGRA #4)
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
async function removeMember(member: Member) {
|
|
262
|
-
// Optimistic
|
|
263
|
-
const prev = queryClient.getQueryData(['members', orgId])
|
|
264
|
-
queryClient.setQueryData(['members', orgId], (old: Member[]) =>
|
|
265
|
-
old.filter(m => m.id !== member.id)
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
const { error } = await supabase
|
|
269
|
-
.from('organization_members')
|
|
270
|
-
.delete()
|
|
271
|
-
.eq('id', member.id)
|
|
272
|
-
|
|
273
|
-
if (error) {
|
|
274
|
-
// Rollback
|
|
275
|
-
queryClient.setQueryData(['members', orgId], prev)
|
|
276
|
-
toast({ title: 'Erro ao remover', description: error.message, variant: 'destructive' })
|
|
277
|
-
} else {
|
|
278
|
-
toast({ title: 'Membro removido' })
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
## Anti-patterns
|
|
284
|
-
|
|
285
|
-
### Anti-pattern 1: Filtros em client-side em lista grande
|
|
286
|
-
|
|
287
|
-
**Errado:**
|
|
288
|
-
```typescript
|
|
289
|
-
const filtered = members.filter(m => m.role === 'admin') // 10000 members buscados
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Por quê:** REGRA #3 — performance terrível, bandwidth wasted, RLS preservada mas overhead.
|
|
293
|
-
|
|
294
|
-
**Certo:** server-side via Supabase: `.eq('roles.name', 'admin')`.
|
|
295
|
-
|
|
296
|
-
### Anti-pattern 2: Botão remove sem PermissionGate
|
|
297
|
-
|
|
298
|
-
**Errado:**
|
|
299
|
-
```typescript
|
|
300
|
-
<Button onClick={removeMember}>Remover</Button>
|
|
301
|
-
// Sem PermissionGate — qualquer member vê
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Por quê:** REGRA #2 — UX confusa, click resulta em 403 visível.
|
|
305
|
-
|
|
306
|
-
**Certo:** `<PermissionGate permission="remove:members">{<Button ...>}</PermissionGate>`.
|
|
307
|
-
|
|
308
|
-
### Anti-pattern 3: Sem optimistic UI em ações comuns
|
|
309
|
-
|
|
310
|
-
**Errado:**
|
|
311
|
-
```typescript
|
|
312
|
-
await api.removeMember(id) // user espera 500ms+
|
|
313
|
-
refetch() // mais 500ms
|
|
314
|
-
// Lista demora a atualizar = UX lenta
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
**Por quê:** REGRA #4 — cada ação parece lenta, frustração.
|
|
318
|
-
|
|
319
|
-
**Certo:** optimistic update imediato + rollback em error + toast.
|
|
320
|
-
|
|
321
|
-
## Ver também
|
|
322
|
-
|
|
323
|
-
- [org-switcher-react-pattern](../org-switcher-react-pattern/SKILL.md) — sibling, useOrgStore
|
|
324
|
-
- [permission-gate-react-pattern](../permission-gate-react-pattern/SKILL.md) — sibling, PermissionGate component usado aqui
|
|
325
|
-
- [member-invite-flow](../member-invite-flow/SKILL.md) — Phase 110, RPC create_invite
|
|
326
|
-
- [_shared-multi-tenant/glossary.md](../_shared-multi-tenant/glossary.md) — `shadcn/ui`, `permission gate`
|
|
327
|
-
- [shadcn/ui Components](https://ui.shadcn.com/docs/components)
|
|
328
|
-
- [TanStack Table v8](https://tanstack.com/table/latest)
|
|
1
|
+
---
|
|
2
|
+
name: member-management-react-shadcn
|
|
3
|
+
description: Use ao implementar UI de member management em B2B SaaS multi-tenant React + shadcn/ui…
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Member Management — React + shadcn/ui
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill ao implementar painel UI de member management. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "member management UI", "painel de membros React"
|
|
13
|
+
- "data table TanStack shadcn", "members list with filters"
|
|
14
|
+
- "invite member dialog", "role assignment dropdown"
|
|
15
|
+
- "shadcn member admin"
|
|
16
|
+
|
|
17
|
+
## Regras absolutas
|
|
18
|
+
|
|
19
|
+
**REGRA #1 (composição canônica de 9 componentes shadcn):** Painel canônico usa: `data-table` (TanStack v8), `dialog`, `select`, `badge`, `dropdown-menu`, `avatar`, `command` (search palette), `form`, `toast`. Adicionar mais componentes só com justificativa.
|
|
20
|
+
|
|
21
|
+
**REGRA #2 (PermissionGate em todo botão de ação):** Cada ação (invite, remove, role change) vem wrapped em `<PermissionGate>` com permission relevante. Member sem permission não vê botão.
|
|
22
|
+
|
|
23
|
+
**REGRA #3 (server-side filters via Supabase, não client):** Filtros (role, status, search) executados via Supabase query (`.eq`, `.ilike`), não em client-side. Performance + RLS preservada.
|
|
24
|
+
|
|
25
|
+
**REGRA #4 (optimistic UI com rollback):** Operações de role change/remove fazem optimistic update + rollback em error. Toast de feedback via shadcn `toast` em ambos casos.
|
|
26
|
+
|
|
27
|
+
## Patterns canônicos
|
|
28
|
+
|
|
29
|
+
### Members DataTable
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// app/orgs/[slug]/members/MembersTable.tsx
|
|
33
|
+
'use client'
|
|
34
|
+
|
|
35
|
+
import { useState, useMemo } from 'react'
|
|
36
|
+
import {
|
|
37
|
+
ColumnDef,
|
|
38
|
+
useReactTable,
|
|
39
|
+
getCoreRowModel,
|
|
40
|
+
getFilteredRowModel,
|
|
41
|
+
flexRender
|
|
42
|
+
} from '@tanstack/react-table'
|
|
43
|
+
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table'
|
|
44
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
45
|
+
import { Badge } from '@/components/ui/badge'
|
|
46
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
|
47
|
+
import { Button } from '@/components/ui/button'
|
|
48
|
+
import { MoreHorizontal } from 'lucide-react'
|
|
49
|
+
import { PermissionGate } from '@/components/PermissionGate'
|
|
50
|
+
import { useMembers } from '@/lib/hooks/use-members'
|
|
51
|
+
import { ChangeRoleDialog } from './ChangeRoleDialog'
|
|
52
|
+
import { RemoveMemberConfirm } from './RemoveMemberConfirm'
|
|
53
|
+
|
|
54
|
+
interface Member {
|
|
55
|
+
id: string
|
|
56
|
+
user: { id: string; email: string; name: string; avatar_url?: string }
|
|
57
|
+
role: { id: string; name: string }
|
|
58
|
+
status: 'active' | 'suspended' | 'left'
|
|
59
|
+
joined_at: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const columns: ColumnDef<Member>[] = [
|
|
63
|
+
{
|
|
64
|
+
id: 'avatar',
|
|
65
|
+
cell: ({ row }) => (
|
|
66
|
+
<Avatar>
|
|
67
|
+
<AvatarImage src={row.original.user.avatar_url} />
|
|
68
|
+
<AvatarFallback>{row.original.user.name?.[0] || '?'}</AvatarFallback>
|
|
69
|
+
</Avatar>
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
accessorKey: 'user.name',
|
|
74
|
+
header: 'Nome',
|
|
75
|
+
cell: ({ row }) => (
|
|
76
|
+
<div>
|
|
77
|
+
<div className="font-medium">{row.original.user.name}</div>
|
|
78
|
+
<div className="text-sm text-muted-foreground">{row.original.user.email}</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
accessorKey: 'role.name',
|
|
84
|
+
header: 'Role',
|
|
85
|
+
cell: ({ row }) => (
|
|
86
|
+
<Badge variant={row.original.role.name === 'owner' ? 'default' : 'secondary'}>
|
|
87
|
+
{row.original.role.name}
|
|
88
|
+
</Badge>
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
accessorKey: 'status',
|
|
93
|
+
header: 'Status',
|
|
94
|
+
cell: ({ row }) => (
|
|
95
|
+
<Badge variant={row.original.status === 'active' ? 'default' : 'destructive'}>
|
|
96
|
+
{row.original.status}
|
|
97
|
+
</Badge>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
accessorKey: 'joined_at',
|
|
102
|
+
header: 'Entrou em',
|
|
103
|
+
cell: ({ row }) => new Date(row.original.joined_at).toLocaleDateString('pt-BR')
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'actions',
|
|
107
|
+
cell: ({ row }) => <MemberActions member={row.original} />
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
function MemberActions({ member }: { member: Member }) {
|
|
112
|
+
const [changeRoleOpen, setChangeRoleOpen] = useState(false)
|
|
113
|
+
const [removeOpen, setRemoveOpen] = useState(false)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<DropdownMenu>
|
|
118
|
+
<DropdownMenuTrigger asChild>
|
|
119
|
+
<Button variant="ghost" size="icon"><MoreHorizontal /></Button>
|
|
120
|
+
</DropdownMenuTrigger>
|
|
121
|
+
<DropdownMenuContent>
|
|
122
|
+
<PermissionGate permission="update:members">
|
|
123
|
+
<DropdownMenuItem onClick={() => setChangeRoleOpen(true)}>Alterar role</DropdownMenuItem>
|
|
124
|
+
</PermissionGate>
|
|
125
|
+
<PermissionGate permission="remove:members">
|
|
126
|
+
<DropdownMenuItem onClick={() => setRemoveOpen(true)} className="text-destructive">
|
|
127
|
+
Remover
|
|
128
|
+
</DropdownMenuItem>
|
|
129
|
+
</PermissionGate>
|
|
130
|
+
</DropdownMenuContent>
|
|
131
|
+
</DropdownMenu>
|
|
132
|
+
|
|
133
|
+
<ChangeRoleDialog member={member} open={changeRoleOpen} onOpenChange={setChangeRoleOpen} />
|
|
134
|
+
<RemoveMemberConfirm member={member} open={removeOpen} onOpenChange={setRemoveOpen} />
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function MembersTable() {
|
|
140
|
+
const { data: members } = useMembers()
|
|
141
|
+
const table = useReactTable({
|
|
142
|
+
data: members || [],
|
|
143
|
+
columns,
|
|
144
|
+
getCoreRowModel: getCoreRowModel(),
|
|
145
|
+
getFilteredRowModel: getFilteredRowModel()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Table>
|
|
150
|
+
<TableHeader>
|
|
151
|
+
{table.getHeaderGroups().map(hg => (
|
|
152
|
+
<TableRow key={hg.id}>
|
|
153
|
+
{hg.headers.map(h => (
|
|
154
|
+
<TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</TableHead>
|
|
155
|
+
))}
|
|
156
|
+
</TableRow>
|
|
157
|
+
))}
|
|
158
|
+
</TableHeader>
|
|
159
|
+
<TableBody>
|
|
160
|
+
{table.getRowModel().rows.map(row => (
|
|
161
|
+
<TableRow key={row.id}>
|
|
162
|
+
{row.getVisibleCells().map(cell => (
|
|
163
|
+
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
|
164
|
+
))}
|
|
165
|
+
</TableRow>
|
|
166
|
+
))}
|
|
167
|
+
</TableBody>
|
|
168
|
+
</Table>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Invite Dialog (REGRA #1: Dialog + Form)
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// app/orgs/[slug]/members/InviteMemberDialog.tsx
|
|
177
|
+
'use client'
|
|
178
|
+
|
|
179
|
+
import { useState } from 'react'
|
|
180
|
+
import { useForm } from 'react-hook-form'
|
|
181
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
182
|
+
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
|
|
183
|
+
import { Input } from '@/components/ui/input'
|
|
184
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
|
|
185
|
+
import { Button } from '@/components/ui/button'
|
|
186
|
+
import { useToast } from '@/components/ui/use-toast'
|
|
187
|
+
import { supabase } from '@/lib/supabase'
|
|
188
|
+
import { useOrgStore } from '@/lib/stores/org-store'
|
|
189
|
+
import { useAssignableRoles } from '@/lib/hooks/use-assignable-roles'
|
|
190
|
+
|
|
191
|
+
export function InviteMemberDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) {
|
|
192
|
+
const { toast } = useToast()
|
|
193
|
+
const orgId = useOrgStore(s => s.activeOrgId)
|
|
194
|
+
const { data: roles } = useAssignableRoles(orgId!)
|
|
195
|
+
|
|
196
|
+
const form = useForm({ defaultValues: { email: '', roleName: 'member' } })
|
|
197
|
+
|
|
198
|
+
async function onSubmit({ email, roleName }: { email: string; roleName: string }) {
|
|
199
|
+
const { data: token, error } = await supabase.rpc('create_invite', {
|
|
200
|
+
p_org_id: orgId!,
|
|
201
|
+
p_email: email,
|
|
202
|
+
p_role_name: roleName
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (error) {
|
|
206
|
+
toast({ title: 'Erro ao criar invite', description: error.message, variant: 'destructive' })
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Enviar email via Edge Function
|
|
211
|
+
await supabase.functions.invoke('send-invite-email', {
|
|
212
|
+
body: { email, token, base_url: window.location.origin }
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
toast({ title: 'Invite enviado', description: `Email de convite enviado a ${email}` })
|
|
216
|
+
onOpenChange(false)
|
|
217
|
+
form.reset()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
222
|
+
<DialogContent>
|
|
223
|
+
<DialogHeader>
|
|
224
|
+
<DialogTitle>Convidar membro</DialogTitle>
|
|
225
|
+
</DialogHeader>
|
|
226
|
+
<Form {...form}>
|
|
227
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
228
|
+
<FormField name="email" rules={{ required: 'Email obrigatório', pattern: /^.+@.+\..+$/ }} render={({ field }) => (
|
|
229
|
+
<FormItem>
|
|
230
|
+
<FormLabel>Email</FormLabel>
|
|
231
|
+
<FormControl><Input type="email" {...field} /></FormControl>
|
|
232
|
+
<FormMessage />
|
|
233
|
+
</FormItem>
|
|
234
|
+
)} />
|
|
235
|
+
<FormField name="roleName" render={({ field }) => (
|
|
236
|
+
<FormItem>
|
|
237
|
+
<FormLabel>Role</FormLabel>
|
|
238
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
239
|
+
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
240
|
+
<SelectContent>
|
|
241
|
+
{roles?.map(r => <SelectItem key={r.id} value={r.name}>{r.name}</SelectItem>)}
|
|
242
|
+
</SelectContent>
|
|
243
|
+
</Select>
|
|
244
|
+
<FormMessage />
|
|
245
|
+
</FormItem>
|
|
246
|
+
)} />
|
|
247
|
+
<DialogFooter>
|
|
248
|
+
<Button type="submit">Enviar convite</Button>
|
|
249
|
+
</DialogFooter>
|
|
250
|
+
</form>
|
|
251
|
+
</Form>
|
|
252
|
+
</DialogContent>
|
|
253
|
+
</Dialog>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Optimistic UI com rollback (REGRA #4)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
async function removeMember(member: Member) {
|
|
262
|
+
// Optimistic
|
|
263
|
+
const prev = queryClient.getQueryData(['members', orgId])
|
|
264
|
+
queryClient.setQueryData(['members', orgId], (old: Member[]) =>
|
|
265
|
+
old.filter(m => m.id !== member.id)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const { error } = await supabase
|
|
269
|
+
.from('organization_members')
|
|
270
|
+
.delete()
|
|
271
|
+
.eq('id', member.id)
|
|
272
|
+
|
|
273
|
+
if (error) {
|
|
274
|
+
// Rollback
|
|
275
|
+
queryClient.setQueryData(['members', orgId], prev)
|
|
276
|
+
toast({ title: 'Erro ao remover', description: error.message, variant: 'destructive' })
|
|
277
|
+
} else {
|
|
278
|
+
toast({ title: 'Membro removido' })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Anti-patterns
|
|
284
|
+
|
|
285
|
+
### Anti-pattern 1: Filtros em client-side em lista grande
|
|
286
|
+
|
|
287
|
+
**Errado:**
|
|
288
|
+
```typescript
|
|
289
|
+
const filtered = members.filter(m => m.role === 'admin') // 10000 members buscados
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Por quê:** REGRA #3 — performance terrível, bandwidth wasted, RLS preservada mas overhead.
|
|
293
|
+
|
|
294
|
+
**Certo:** server-side via Supabase: `.eq('roles.name', 'admin')`.
|
|
295
|
+
|
|
296
|
+
### Anti-pattern 2: Botão remove sem PermissionGate
|
|
297
|
+
|
|
298
|
+
**Errado:**
|
|
299
|
+
```typescript
|
|
300
|
+
<Button onClick={removeMember}>Remover</Button>
|
|
301
|
+
// Sem PermissionGate — qualquer member vê
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Por quê:** REGRA #2 — UX confusa, click resulta em 403 visível.
|
|
305
|
+
|
|
306
|
+
**Certo:** `<PermissionGate permission="remove:members">{<Button ...>}</PermissionGate>`.
|
|
307
|
+
|
|
308
|
+
### Anti-pattern 3: Sem optimistic UI em ações comuns
|
|
309
|
+
|
|
310
|
+
**Errado:**
|
|
311
|
+
```typescript
|
|
312
|
+
await api.removeMember(id) // user espera 500ms+
|
|
313
|
+
refetch() // mais 500ms
|
|
314
|
+
// Lista demora a atualizar = UX lenta
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Por quê:** REGRA #4 — cada ação parece lenta, frustração.
|
|
318
|
+
|
|
319
|
+
**Certo:** optimistic update imediato + rollback em error + toast.
|
|
320
|
+
|
|
321
|
+
## Ver também
|
|
322
|
+
|
|
323
|
+
- [org-switcher-react-pattern](../org-switcher-react-pattern/SKILL.md) — sibling, useOrgStore
|
|
324
|
+
- [permission-gate-react-pattern](../permission-gate-react-pattern/SKILL.md) — sibling, PermissionGate component usado aqui
|
|
325
|
+
- [member-invite-flow](../member-invite-flow/SKILL.md) — Phase 110, RPC create_invite
|
|
326
|
+
- [_shared-multi-tenant/glossary.md](../_shared-multi-tenant/glossary.md) — `shadcn/ui`, `permission gate`
|
|
327
|
+
- [shadcn/ui Components](https://ui.shadcn.com/docs/components)
|
|
328
|
+
- [TanStack Table v8](https://tanstack.com/table/latest)
|