@zauru-sdk/components 6.8.2 → 6.9.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/CHANGELOG.md +19 -0
- package/dist/Tab/PermissionMatrix.d.ts +21 -0
- package/dist/Tab/RouteTabNav.d.ts +13 -0
- package/dist/Tab/index.d.ts +2 -0
- package/dist/esm/Tab/PermissionMatrix.js +30 -0
- package/dist/esm/Tab/RouteTabNav.js +21 -0
- package/dist/esm/Tab/index.js +2 -0
- package/package.json +7 -7
- package/src/Tab/PermissionMatrix.tsx +223 -0
- package/src/Tab/RouteTabNav.tsx +62 -0
- package/src/Tab/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [6.9.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.8.3...@zauru-sdk/components@6.9.0) (2026-10-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* motor RBAC genérico para webapps Remix (roles, asignaciones y matriz) ([b48c635](https://github.com/intuitiva/zauru-typescript-sdk/commit/b48c635101814ebf0b362636839b626d23a13b57))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [6.8.3](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.8.2...@zauru-sdk/components@6.8.3) (2026-10-05)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @zauru-sdk/components
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [6.8.2](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.8.1...@zauru-sdk/components@6.8.2) (2026-09-22)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @zauru-sdk/components
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PermissionGroup } from "@zauru-sdk/types";
|
|
2
|
+
export type PermissionMatrixRole = {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
permissionKeys: string[];
|
|
6
|
+
};
|
|
7
|
+
type Props = {
|
|
8
|
+
groups: PermissionGroup[];
|
|
9
|
+
roles: PermissionMatrixRole[];
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
pendingCells?: ReadonlySet<string>;
|
|
12
|
+
onToggle: (roleId: number, key: string, granted: boolean) => void;
|
|
13
|
+
onBulkRole?: (roleId: number, mode: "all" | "none") => void;
|
|
14
|
+
onBulkSection?: (roleId: number, groupId: string, mode: "add" | "remove") => void;
|
|
15
|
+
emptyText?: string;
|
|
16
|
+
markAllLabel?: string;
|
|
17
|
+
unmarkAllLabel?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function permissionMatrixCellKey(roleId: number, key: string): string;
|
|
20
|
+
export declare const PermissionMatrix: ({ groups, roles, disabled, pendingCells, onToggle, onBulkRole, onBulkSection, emptyText, markAllLabel, unmarkAllLabel, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RouteTabNavItem = {
|
|
2
|
+
to: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hide?: boolean;
|
|
5
|
+
end?: boolean;
|
|
6
|
+
};
|
|
7
|
+
type Props = {
|
|
8
|
+
items: RouteTabNavItem[];
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
selectedColor?: "indigo" | "slate" | "blue" | "green";
|
|
11
|
+
};
|
|
12
|
+
export declare const RouteTabNav: ({ items, ariaLabel, selectedColor, }: Props) => import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
package/dist/Tab/index.d.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
export function permissionMatrixCellKey(roleId, key) {
|
|
3
|
+
return `${roleId}:${key}`;
|
|
4
|
+
}
|
|
5
|
+
const checkboxClass = "form-checkbox h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50";
|
|
6
|
+
export const PermissionMatrix = ({ groups, roles, disabled = false, pendingCells, onToggle, onBulkRole, onBulkSection, emptyText = "Crea un rol para editar la matriz de permisos.", markAllLabel = "Marcar todo", unmarkAllLabel = "Desmarcar todo", }) => {
|
|
7
|
+
const allKeys = groups.flatMap((group) => group.actions.map((action) => action.key));
|
|
8
|
+
if (roles.length === 0) {
|
|
9
|
+
return (_jsx("p", { className: "text-sm text-gray-600", role: "status", children: emptyText }));
|
|
10
|
+
}
|
|
11
|
+
const roleHasKeys = (role, keys) => keys.length > 0 && keys.every((key) => role.permissionKeys.includes(key));
|
|
12
|
+
return (_jsx("div", { className: "overflow-x-auto rounded-md border border-gray-200", children: _jsxs("table", { className: "min-w-full border-collapse text-sm", children: [_jsx("caption", { className: "sr-only", children: "Matriz de permisos por rol. Filas son acciones, columnas son roles." }), _jsx("thead", { children: _jsxs("tr", { className: "bg-gray-50", children: [_jsx("th", { scope: "col", className: "sticky left-0 z-10 min-w-[16rem] border-b border-gray-200 bg-gray-50 px-3 py-2 text-left font-semibold text-gray-800", children: "Permiso" }), roles.map((role) => {
|
|
13
|
+
const allGranted = roleHasKeys(role, allKeys);
|
|
14
|
+
return (_jsx("th", { scope: "col", className: "min-w-[9rem] border-b border-l border-gray-200 px-3 py-2 text-center font-semibold text-gray-800", children: _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("span", { className: "break-words", children: role.name }), onBulkRole ? (_jsx("button", { type: "button", className: "cursor-pointer text-xs font-medium text-indigo-700 hover:text-indigo-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded", disabled: disabled, onClick: () => onBulkRole(role.id, allGranted ? "none" : "all"), children: allGranted ? unmarkAllLabel : markAllLabel })) : null] }) }, role.id));
|
|
15
|
+
})] }) }), _jsx("tbody", { children: groups.map((group) => {
|
|
16
|
+
const sectionKeys = group.actions.map((action) => action.key);
|
|
17
|
+
return (_jsx(GroupRows, { group: group, roles: roles, sectionKeys: sectionKeys, disabled: disabled, pendingCells: pendingCells, onToggle: onToggle, onBulkSection: onBulkSection, checkboxClass: checkboxClass }, group.id));
|
|
18
|
+
}) })] }) }));
|
|
19
|
+
};
|
|
20
|
+
function GroupRows({ group, roles, sectionKeys, disabled, pendingCells, onToggle, onBulkSection, checkboxClass, }) {
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("tr", { className: "bg-slate-100", children: _jsx("th", { scope: "colgroup", colSpan: roles.length + 1, className: "sticky left-0 border-b border-gray-200 px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-slate-700", children: group.label }) }), group.actions.map((action) => (_jsxs("tr", { className: "odd:bg-white even:bg-slate-50", children: [_jsx("th", { scope: "row", className: "sticky left-0 z-10 border-b border-gray-100 bg-inherit px-3 py-2 text-left font-normal text-gray-800", children: _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: action.label }), _jsx("code", { className: "text-xs text-gray-500", children: action.key })] }) }), roles.map((role) => {
|
|
22
|
+
const granted = role.permissionKeys.includes(action.key);
|
|
23
|
+
const cellKey = permissionMatrixCellKey(role.id, action.key);
|
|
24
|
+
const pending = pendingCells?.has(cellKey) ?? false;
|
|
25
|
+
return (_jsx("td", { className: "border-b border-l border-gray-100 px-3 py-2 text-center", children: _jsx("input", { type: "checkbox", className: checkboxClass, checked: granted, disabled: disabled || pending, "aria-label": `${role.name}: ${action.label}`, onChange: () => onToggle(role.id, action.key, !granted) }) }, role.id));
|
|
26
|
+
})] }, action.key))), onBulkSection ? (_jsxs("tr", { children: [_jsx("td", { className: "sticky left-0 z-10 border-b border-gray-200 bg-white px-3 py-1 text-xs text-gray-500", children: "Secci\u00F3n" }), roles.map((role) => {
|
|
27
|
+
const sectionGranted = sectionKeys.every((key) => role.permissionKeys.includes(key));
|
|
28
|
+
return (_jsx("td", { className: "border-b border-l border-gray-200 px-3 py-1 text-center", children: _jsx("button", { type: "button", className: "cursor-pointer text-xs font-medium text-indigo-700 hover:text-indigo-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded disabled:cursor-not-allowed disabled:opacity-50", disabled: disabled, onClick: () => onBulkSection(role.id, group.id, sectionGranted ? "remove" : "add"), children: sectionGranted ? "Quitar sección" : "Marcar sección" }) }, role.id));
|
|
29
|
+
})] })) : null] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NavLink } from "@remix-run/react";
|
|
3
|
+
const ACTIVE = {
|
|
4
|
+
indigo: "border-indigo-600 text-indigo-700",
|
|
5
|
+
slate: "border-slate-700 text-slate-800",
|
|
6
|
+
blue: "border-blue-600 text-blue-700",
|
|
7
|
+
green: "border-green-600 text-green-700",
|
|
8
|
+
};
|
|
9
|
+
export const RouteTabNav = ({ items, ariaLabel = "Secciones", selectedColor = "indigo", }) => {
|
|
10
|
+
const visible = items.filter((item) => !item.hide);
|
|
11
|
+
if (visible.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("nav", { "aria-label": ariaLabel, className: "border-b border-gray-200", children: _jsx("ul", { className: "flex flex-wrap gap-1 -mb-px", children: visible.map((item) => (_jsx("li", { children: _jsx(NavLink, { to: item.to, end: item.end, className: ({ isActive }) => [
|
|
15
|
+
"inline-flex items-center cursor-pointer rounded-t-md border-b-2 px-3 py-2 text-sm font-medium transition-colors duration-150",
|
|
16
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2",
|
|
17
|
+
isActive
|
|
18
|
+
? ACTIVE[selectedColor]
|
|
19
|
+
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-800",
|
|
20
|
+
].join(" "), children: item.label }) }, item.to))) }) }));
|
|
21
|
+
};
|
package/dist/esm/Tab/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/components",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "Componentes reutilizables en las WebApps de Zauru.",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"@reduxjs/toolkit": "^2.2.1",
|
|
45
45
|
"@remix-run/node": "^2.8.1",
|
|
46
46
|
"@remix-run/react": "^2.8.1",
|
|
47
|
-
"@zauru-sdk/common": "^6.8.
|
|
48
|
-
"@zauru-sdk/hooks": "^6.3.
|
|
47
|
+
"@zauru-sdk/common": "^6.8.4",
|
|
48
|
+
"@zauru-sdk/hooks": "^6.3.17",
|
|
49
49
|
"@zauru-sdk/icons": "^6.1.0",
|
|
50
|
-
"@zauru-sdk/redux": "^6.5.
|
|
51
|
-
"@zauru-sdk/types": "^6.
|
|
52
|
-
"@zauru-sdk/utils": "^6.
|
|
50
|
+
"@zauru-sdk/redux": "^6.5.15",
|
|
51
|
+
"@zauru-sdk/types": "^6.22.0",
|
|
52
|
+
"@zauru-sdk/utils": "^6.21.0",
|
|
53
53
|
"browser-image-compression": "^2.0.2",
|
|
54
54
|
"framer-motion": "^12.0.0",
|
|
55
55
|
"jsonwebtoken": "^9.0.2",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"styled-components": "^5.3.5",
|
|
63
63
|
"zod": "^3.23.8"
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "800600a8cdb22f1c49293fbc3a48d26aa03771c9"
|
|
66
66
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { PermissionGroup } from "@zauru-sdk/types";
|
|
2
|
+
|
|
3
|
+
export type PermissionMatrixRole = {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
permissionKeys: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
groups: PermissionGroup[];
|
|
11
|
+
roles: PermissionMatrixRole[];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
pendingCells?: ReadonlySet<string>;
|
|
14
|
+
onToggle: (roleId: number, key: string, granted: boolean) => void;
|
|
15
|
+
onBulkRole?: (roleId: number, mode: "all" | "none") => void;
|
|
16
|
+
onBulkSection?: (
|
|
17
|
+
roleId: number,
|
|
18
|
+
groupId: string,
|
|
19
|
+
mode: "add" | "remove",
|
|
20
|
+
) => void;
|
|
21
|
+
emptyText?: string;
|
|
22
|
+
markAllLabel?: string;
|
|
23
|
+
unmarkAllLabel?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function permissionMatrixCellKey(roleId: number, key: string): string {
|
|
27
|
+
return `${roleId}:${key}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const checkboxClass =
|
|
31
|
+
"form-checkbox h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50";
|
|
32
|
+
|
|
33
|
+
export const PermissionMatrix = ({
|
|
34
|
+
groups,
|
|
35
|
+
roles,
|
|
36
|
+
disabled = false,
|
|
37
|
+
pendingCells,
|
|
38
|
+
onToggle,
|
|
39
|
+
onBulkRole,
|
|
40
|
+
onBulkSection,
|
|
41
|
+
emptyText = "Crea un rol para editar la matriz de permisos.",
|
|
42
|
+
markAllLabel = "Marcar todo",
|
|
43
|
+
unmarkAllLabel = "Desmarcar todo",
|
|
44
|
+
}: Props) => {
|
|
45
|
+
const allKeys = groups.flatMap((group) =>
|
|
46
|
+
group.actions.map((action) => action.key),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (roles.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<p className="text-sm text-gray-600" role="status">
|
|
52
|
+
{emptyText}
|
|
53
|
+
</p>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const roleHasKeys = (role: PermissionMatrixRole, keys: string[]) =>
|
|
58
|
+
keys.length > 0 && keys.every((key) => role.permissionKeys.includes(key));
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="overflow-x-auto rounded-md border border-gray-200">
|
|
62
|
+
<table className="min-w-full border-collapse text-sm">
|
|
63
|
+
<caption className="sr-only">
|
|
64
|
+
Matriz de permisos por rol. Filas son acciones, columnas son roles.
|
|
65
|
+
</caption>
|
|
66
|
+
<thead>
|
|
67
|
+
<tr className="bg-gray-50">
|
|
68
|
+
<th
|
|
69
|
+
scope="col"
|
|
70
|
+
className="sticky left-0 z-10 min-w-[16rem] border-b border-gray-200 bg-gray-50 px-3 py-2 text-left font-semibold text-gray-800"
|
|
71
|
+
>
|
|
72
|
+
Permiso
|
|
73
|
+
</th>
|
|
74
|
+
{roles.map((role) => {
|
|
75
|
+
const allGranted = roleHasKeys(role, allKeys);
|
|
76
|
+
return (
|
|
77
|
+
<th
|
|
78
|
+
key={role.id}
|
|
79
|
+
scope="col"
|
|
80
|
+
className="min-w-[9rem] border-b border-l border-gray-200 px-3 py-2 text-center font-semibold text-gray-800"
|
|
81
|
+
>
|
|
82
|
+
<div className="flex flex-col items-center gap-1">
|
|
83
|
+
<span className="break-words">{role.name}</span>
|
|
84
|
+
{onBulkRole ? (
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
className="cursor-pointer text-xs font-medium text-indigo-700 hover:text-indigo-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded"
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
onClick={() =>
|
|
90
|
+
onBulkRole(role.id, allGranted ? "none" : "all")
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{allGranted ? unmarkAllLabel : markAllLabel}
|
|
94
|
+
</button>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
</th>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</tr>
|
|
101
|
+
</thead>
|
|
102
|
+
<tbody>
|
|
103
|
+
{groups.map((group) => {
|
|
104
|
+
const sectionKeys = group.actions.map((action) => action.key);
|
|
105
|
+
return (
|
|
106
|
+
<GroupRows
|
|
107
|
+
key={group.id}
|
|
108
|
+
group={group}
|
|
109
|
+
roles={roles}
|
|
110
|
+
sectionKeys={sectionKeys}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
pendingCells={pendingCells}
|
|
113
|
+
onToggle={onToggle}
|
|
114
|
+
onBulkSection={onBulkSection}
|
|
115
|
+
checkboxClass={checkboxClass}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</tbody>
|
|
120
|
+
</table>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function GroupRows({
|
|
126
|
+
group,
|
|
127
|
+
roles,
|
|
128
|
+
sectionKeys,
|
|
129
|
+
disabled,
|
|
130
|
+
pendingCells,
|
|
131
|
+
onToggle,
|
|
132
|
+
onBulkSection,
|
|
133
|
+
checkboxClass,
|
|
134
|
+
}: {
|
|
135
|
+
group: PermissionGroup;
|
|
136
|
+
roles: PermissionMatrixRole[];
|
|
137
|
+
sectionKeys: string[];
|
|
138
|
+
disabled: boolean;
|
|
139
|
+
pendingCells?: ReadonlySet<string>;
|
|
140
|
+
onToggle: Props["onToggle"];
|
|
141
|
+
onBulkSection: Props["onBulkSection"];
|
|
142
|
+
checkboxClass: string;
|
|
143
|
+
}) {
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<tr className="bg-slate-100">
|
|
147
|
+
<th
|
|
148
|
+
scope="colgroup"
|
|
149
|
+
colSpan={roles.length + 1}
|
|
150
|
+
className="sticky left-0 border-b border-gray-200 px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-slate-700"
|
|
151
|
+
>
|
|
152
|
+
{group.label}
|
|
153
|
+
</th>
|
|
154
|
+
</tr>
|
|
155
|
+
{group.actions.map((action) => (
|
|
156
|
+
<tr key={action.key} className="odd:bg-white even:bg-slate-50">
|
|
157
|
+
<th
|
|
158
|
+
scope="row"
|
|
159
|
+
className="sticky left-0 z-10 border-b border-gray-100 bg-inherit px-3 py-2 text-left font-normal text-gray-800"
|
|
160
|
+
>
|
|
161
|
+
<div className="flex flex-col gap-0.5">
|
|
162
|
+
<span>{action.label}</span>
|
|
163
|
+
<code className="text-xs text-gray-500">{action.key}</code>
|
|
164
|
+
</div>
|
|
165
|
+
</th>
|
|
166
|
+
{roles.map((role) => {
|
|
167
|
+
const granted = role.permissionKeys.includes(action.key);
|
|
168
|
+
const cellKey = permissionMatrixCellKey(role.id, action.key);
|
|
169
|
+
const pending = pendingCells?.has(cellKey) ?? false;
|
|
170
|
+
return (
|
|
171
|
+
<td
|
|
172
|
+
key={role.id}
|
|
173
|
+
className="border-b border-l border-gray-100 px-3 py-2 text-center"
|
|
174
|
+
>
|
|
175
|
+
<input
|
|
176
|
+
type="checkbox"
|
|
177
|
+
className={checkboxClass}
|
|
178
|
+
checked={granted}
|
|
179
|
+
disabled={disabled || pending}
|
|
180
|
+
aria-label={`${role.name}: ${action.label}`}
|
|
181
|
+
onChange={() => onToggle(role.id, action.key, !granted)}
|
|
182
|
+
/>
|
|
183
|
+
</td>
|
|
184
|
+
);
|
|
185
|
+
})}
|
|
186
|
+
</tr>
|
|
187
|
+
))}
|
|
188
|
+
{onBulkSection ? (
|
|
189
|
+
<tr>
|
|
190
|
+
<td className="sticky left-0 z-10 border-b border-gray-200 bg-white px-3 py-1 text-xs text-gray-500">
|
|
191
|
+
Sección
|
|
192
|
+
</td>
|
|
193
|
+
{roles.map((role) => {
|
|
194
|
+
const sectionGranted = sectionKeys.every((key) =>
|
|
195
|
+
role.permissionKeys.includes(key),
|
|
196
|
+
);
|
|
197
|
+
return (
|
|
198
|
+
<td
|
|
199
|
+
key={role.id}
|
|
200
|
+
className="border-b border-l border-gray-200 px-3 py-1 text-center"
|
|
201
|
+
>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className="cursor-pointer text-xs font-medium text-indigo-700 hover:text-indigo-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded disabled:cursor-not-allowed disabled:opacity-50"
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
onClick={() =>
|
|
207
|
+
onBulkSection(
|
|
208
|
+
role.id,
|
|
209
|
+
group.id,
|
|
210
|
+
sectionGranted ? "remove" : "add",
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
{sectionGranted ? "Quitar sección" : "Marcar sección"}
|
|
215
|
+
</button>
|
|
216
|
+
</td>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</tr>
|
|
220
|
+
) : null}
|
|
221
|
+
</>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NavLink } from "@remix-run/react";
|
|
2
|
+
|
|
3
|
+
export type RouteTabNavItem = {
|
|
4
|
+
to: string;
|
|
5
|
+
label: string;
|
|
6
|
+
hide?: boolean;
|
|
7
|
+
end?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
items: RouteTabNavItem[];
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
selectedColor?: "indigo" | "slate" | "blue" | "green";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ACTIVE: Record<NonNullable<Props["selectedColor"]>, string> = {
|
|
17
|
+
indigo: "border-indigo-600 text-indigo-700",
|
|
18
|
+
slate: "border-slate-700 text-slate-800",
|
|
19
|
+
blue: "border-blue-600 text-blue-700",
|
|
20
|
+
green: "border-green-600 text-green-700",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const RouteTabNav = ({
|
|
24
|
+
items,
|
|
25
|
+
ariaLabel = "Secciones",
|
|
26
|
+
selectedColor = "indigo",
|
|
27
|
+
}: Props) => {
|
|
28
|
+
const visible = items.filter((item) => !item.hide);
|
|
29
|
+
|
|
30
|
+
if (visible.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<nav
|
|
36
|
+
aria-label={ariaLabel}
|
|
37
|
+
className="border-b border-gray-200"
|
|
38
|
+
>
|
|
39
|
+
<ul className="flex flex-wrap gap-1 -mb-px">
|
|
40
|
+
{visible.map((item) => (
|
|
41
|
+
<li key={item.to}>
|
|
42
|
+
<NavLink
|
|
43
|
+
to={item.to}
|
|
44
|
+
end={item.end}
|
|
45
|
+
className={({ isActive }) =>
|
|
46
|
+
[
|
|
47
|
+
"inline-flex items-center cursor-pointer rounded-t-md border-b-2 px-3 py-2 text-sm font-medium transition-colors duration-150",
|
|
48
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2",
|
|
49
|
+
isActive
|
|
50
|
+
? ACTIVE[selectedColor]
|
|
51
|
+
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-800",
|
|
52
|
+
].join(" ")
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
{item.label}
|
|
56
|
+
</NavLink>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
</nav>
|
|
61
|
+
);
|
|
62
|
+
};
|
package/src/Tab/index.ts
CHANGED