@nocios/crudify-ui 1.0.34 → 1.0.35

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,15 @@
1
+ import React from "react";
2
+ interface PolicyFieldsValue {
3
+ allow: string[];
4
+ owner_allow: string[];
5
+ deny: string[];
6
+ }
7
+ interface FieldSelectorProps {
8
+ value: PolicyFieldsValue;
9
+ onChange: (value: PolicyFieldsValue) => void;
10
+ availableFields: string[];
11
+ error?: string;
12
+ disabled?: boolean;
13
+ }
14
+ declare const FieldSelector: React.FC<FieldSelectorProps>;
15
+ export default FieldSelector;
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Box, Typography, Button, Stack, FormHelperText, ToggleButton, ToggleButtonGroup, } from "@mui/material";
5
+ import { CheckCircle, Cancel, SelectAll, ClearAll } from "@mui/icons-material";
6
+ const FieldSelector = ({ value, onChange, availableFields, error, disabled = false, }) => {
7
+ const { t } = useTranslation();
8
+ const [mode, setMode] = useState('custom');
9
+ const isUpdatingRef = useRef(false);
10
+ // Ensure value is normalized with current available fields
11
+ useEffect(() => {
12
+ const current = value || { allow: [], owner_allow: [], deny: [] };
13
+ const all = new Set(availableFields);
14
+ const allow = (current.allow || []).filter(f => all.has(f));
15
+ const owner = (current.owner_allow || []).filter(f => all.has(f));
16
+ const deny = (current.deny || []).filter(f => all.has(f));
17
+ // Add any missing fields to deny by default
18
+ availableFields.forEach(f => {
19
+ if (!allow.includes(f) && !owner.includes(f) && !deny.includes(f))
20
+ deny.push(f);
21
+ });
22
+ const normalized = { allow, owner_allow: owner, deny };
23
+ if (JSON.stringify(normalized) !== JSON.stringify(current)) {
24
+ onChange(normalized);
25
+ }
26
+ // Set mode based on normalized
27
+ if (allow.length === availableFields.length)
28
+ setMode('all');
29
+ else if (deny.length === availableFields.length)
30
+ setMode('none');
31
+ else
32
+ setMode('custom');
33
+ }, [availableFields, value]);
34
+ const setAllAllow = () => {
35
+ isUpdatingRef.current = true;
36
+ onChange({ allow: [...availableFields], owner_allow: [], deny: [] });
37
+ setMode('all');
38
+ setTimeout(() => { isUpdatingRef.current = false; }, 0);
39
+ };
40
+ const setAllDeny = () => {
41
+ isUpdatingRef.current = true;
42
+ onChange({ allow: [], owner_allow: [], deny: [...availableFields] });
43
+ setMode('none');
44
+ setTimeout(() => { isUpdatingRef.current = false; }, 0);
45
+ };
46
+ const getFieldState = (fieldName) => {
47
+ if (value?.allow?.includes(fieldName))
48
+ return 'allow';
49
+ if (value?.owner_allow?.includes(fieldName))
50
+ return 'owner_allow';
51
+ return 'deny';
52
+ };
53
+ const setFieldState = (fieldName, state) => {
54
+ isUpdatingRef.current = true;
55
+ const allow = new Set(value?.allow || []);
56
+ const owner = new Set(value?.owner_allow || []);
57
+ const deny = new Set(value?.deny || []);
58
+ // Remove from all, then add to target
59
+ allow.delete(fieldName);
60
+ owner.delete(fieldName);
61
+ deny.delete(fieldName);
62
+ if (state === 'allow')
63
+ allow.add(fieldName);
64
+ if (state === 'owner_allow')
65
+ owner.add(fieldName);
66
+ if (state === 'deny')
67
+ deny.add(fieldName);
68
+ onChange({ allow: Array.from(allow), owner_allow: Array.from(owner), deny: Array.from(deny) });
69
+ setMode('custom');
70
+ setTimeout(() => { isUpdatingRef.current = false; }, 0);
71
+ };
72
+ if (availableFields.length === 0) {
73
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: t("modules.form.publicPolicies.fields.conditions.label") }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { fontStyle: 'italic' }, children: t("modules.form.publicPolicies.fields.conditions.noFieldsAvailable") }), error && (_jsx(FormHelperText, { error: true, sx: { mt: 1 }, children: error }))] }));
74
+ }
75
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: t("modules.form.publicPolicies.fields.conditions.label") }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mb: 3 }, children: [_jsx(Button, { variant: mode === 'all' ? 'contained' : 'outlined', startIcon: _jsx(SelectAll, {}), onClick: setAllAllow, disabled: disabled, size: "small", sx: {
76
+ minWidth: 120,
77
+ ...(mode === 'all' && {
78
+ backgroundColor: '#16a34a',
79
+ '&:hover': { backgroundColor: '#15803d' }
80
+ })
81
+ }, children: t("modules.form.publicPolicies.fields.conditions.allFields") }), _jsx(Button, { variant: mode === 'none' ? 'contained' : 'outlined', startIcon: _jsx(ClearAll, {}), onClick: setAllDeny, disabled: disabled, size: "small", sx: {
82
+ minWidth: 120,
83
+ ...(mode === 'none' && {
84
+ backgroundColor: '#cf222e',
85
+ '&:hover': { backgroundColor: '#bc1f2c' }
86
+ })
87
+ }, children: t("modules.form.publicPolicies.fields.conditions.noFields") })] }), _jsxs(Box, { sx: { p: 2, border: '1px solid #d1d9e0', borderRadius: 1, backgroundColor: '#f6f8fa' }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: t("modules.form.publicPolicies.fields.conditions.help") }), _jsx(Stack, { spacing: 1, children: availableFields.map((fieldName) => {
88
+ const fieldState = getFieldState(fieldName);
89
+ return (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "body2", sx: { minWidth: 100, fontFamily: 'monospace' }, children: fieldName }), _jsxs(ToggleButtonGroup, { value: fieldState, exclusive: true, size: "small", children: [_jsxs(ToggleButton, { value: "allow", onClick: () => setFieldState(fieldName, 'allow'), disabled: disabled, sx: {
90
+ px: 2,
91
+ color: fieldState === 'allow' ? '#ffffff' : '#6b7280',
92
+ backgroundColor: fieldState === 'allow' ? '#16a34a' : '#f3f4f6',
93
+ borderColor: fieldState === 'allow' ? '#16a34a' : '#d1d5db',
94
+ '&:hover': {
95
+ backgroundColor: fieldState === 'allow' ? '#15803d' : '#e5e7eb',
96
+ borderColor: fieldState === 'allow' ? '#15803d' : '#9ca3af'
97
+ },
98
+ '&.Mui-selected': {
99
+ backgroundColor: '#16a34a',
100
+ color: '#ffffff',
101
+ '&:hover': {
102
+ backgroundColor: '#15803d'
103
+ }
104
+ }
105
+ }, children: [_jsx(CheckCircle, { sx: { fontSize: 16, mr: 0.5 } }), t('modules.form.publicPolicies.fields.conditions.states.allow')] }), _jsx(ToggleButton, { value: "owner_allow", onClick: () => setFieldState(fieldName, 'owner_allow'), disabled: disabled, sx: {
106
+ px: 2,
107
+ color: fieldState === 'owner_allow' ? '#ffffff' : '#6b7280',
108
+ backgroundColor: fieldState === 'owner_allow' ? '#0ea5e9' : '#f3f4f6',
109
+ borderColor: fieldState === 'owner_allow' ? '#0ea5e9' : '#d1d5db',
110
+ '&:hover': {
111
+ backgroundColor: fieldState === 'owner_allow' ? '#0284c7' : '#e5e7eb',
112
+ borderColor: fieldState === 'owner_allow' ? '#0284c7' : '#9ca3af'
113
+ },
114
+ '&.Mui-selected': {
115
+ backgroundColor: '#0ea5e9',
116
+ color: '#ffffff',
117
+ '&:hover': {
118
+ backgroundColor: '#0284c7'
119
+ }
120
+ }
121
+ }, children: t('modules.form.publicPolicies.fields.conditions.states.ownerAllow') }), _jsxs(ToggleButton, { value: "deny", onClick: () => setFieldState(fieldName, 'deny'), disabled: disabled, sx: {
122
+ px: 2,
123
+ color: fieldState === 'deny' ? '#ffffff' : '#6b7280',
124
+ backgroundColor: fieldState === 'deny' ? '#dc2626' : '#f3f4f6',
125
+ borderColor: fieldState === 'deny' ? '#dc2626' : '#d1d5db',
126
+ '&:hover': {
127
+ backgroundColor: fieldState === 'deny' ? '#b91c1c' : '#e5e7eb',
128
+ borderColor: fieldState === 'deny' ? '#b91c1c' : '#9ca3af'
129
+ },
130
+ '&.Mui-selected': {
131
+ backgroundColor: '#dc2626',
132
+ color: '#ffffff',
133
+ '&:hover': {
134
+ backgroundColor: '#b91c1c'
135
+ }
136
+ }
137
+ }, children: [_jsx(Cancel, { sx: { fontSize: 16, mr: 0.5 } }), t('modules.form.publicPolicies.fields.conditions.states.deny')] })] })] }, fieldName));
138
+ }) })] }), error && (_jsx(FormHelperText, { error: true, sx: { mt: 1 }, children: error }))] }));
139
+ };
140
+ export default FieldSelector;
@@ -0,0 +1 @@
1
+ export { default } from "./FieldSelector";
@@ -0,0 +1 @@
1
+ export { default } from "./FieldSelector";
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import type { PolicyAction } from "./constants";
3
+ type Policy = {
4
+ id: string;
5
+ action: PolicyAction;
6
+ fields: {
7
+ allow: string[];
8
+ owner_allow: string[];
9
+ deny: string[];
10
+ };
11
+ };
12
+ type FieldErrorMap = string | ({
13
+ _error?: string;
14
+ } & Record<string, string | undefined>);
15
+ interface PoliciesProps {
16
+ policies: Policy[];
17
+ onChange: (next: Policy[]) => void;
18
+ availableFields: string[];
19
+ errors?: FieldErrorMap;
20
+ isSubmitting?: boolean;
21
+ }
22
+ declare const Policies: React.FC<PoliciesProps>;
23
+ export default Policies;
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Box, Typography, Button, Stack, Alert, Divider, } from "@mui/material";
5
+ import { Add } from "@mui/icons-material";
6
+ import PolicyItem from "./PolicyItem";
7
+ import { PREFERRED_POLICY_ORDER } from "./constants";
8
+ const generateId = () => {
9
+ const c = globalThis?.crypto;
10
+ if (c && typeof c.randomUUID === "function")
11
+ return c.randomUUID();
12
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
13
+ };
14
+ const Policies = ({ policies, onChange, availableFields, errors, isSubmitting = false, }) => {
15
+ const { t } = useTranslation();
16
+ const policyRefs = useRef({});
17
+ const takenActions = new Set((policies || []).map((p) => p.action).filter(Boolean));
18
+ const remainingActions = PREFERRED_POLICY_ORDER.filter((a) => !takenActions.has(a));
19
+ const canAddPolicy = remainingActions.length > 0;
20
+ const addPolicy = () => {
21
+ const defaultAction = remainingActions[0] || "create";
22
+ const newPolicy = {
23
+ id: generateId(),
24
+ action: defaultAction,
25
+ fields: {
26
+ allow: [],
27
+ owner_allow: [],
28
+ deny: availableFields,
29
+ },
30
+ };
31
+ const next = [...(policies || []), newPolicy];
32
+ onChange(next);
33
+ setTimeout(() => {
34
+ const newIndex = next.length - 1;
35
+ const el = policyRefs.current[newIndex];
36
+ if (el) {
37
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
38
+ }
39
+ }, 100);
40
+ };
41
+ const removePolicy = (index) => {
42
+ const next = [...policies];
43
+ next.splice(index, 1);
44
+ onChange(next);
45
+ };
46
+ const arrayError = (() => {
47
+ if (!errors)
48
+ return null;
49
+ if (typeof errors === "string")
50
+ return errors;
51
+ const msg = errors._error;
52
+ return typeof msg === "string" ? msg : null;
53
+ })();
54
+ const usedActions = new Set((policies || []).map((p) => p.action));
55
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { sx: { borderColor: '#e0e4e7' } }), _jsxs(Box, { children: [_jsx(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 3, children: _jsxs(Box, { children: [_jsx(Typography, { variant: "h6", sx: {
56
+ fontWeight: 600,
57
+ color: '#111418',
58
+ mb: 1
59
+ }, children: t("modules.form.publicPolicies.title") }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { fontSize: '0.875rem' }, children: t("modules.form.publicPolicies.description") })] }) }), arrayError && (_jsx(Alert, { severity: "error", sx: { mb: 3 }, children: arrayError })), _jsxs(Stack, { spacing: 3, children: [(policies || []).length === 0 ? (_jsx(Alert, { severity: "info", children: t("modules.form.publicPolicies.noPolicies") })) : (policies.map((policy, index) => (_jsx(PolicyItem, { ref: (el) => { policyRefs.current[index] = el; }, policy: policy, onChange: (nextPolicy) => {
60
+ const next = [...policies];
61
+ next[index] = nextPolicy;
62
+ onChange(next);
63
+ }, onRemove: () => removePolicy(index), availableFields: availableFields, isSubmitting: isSubmitting, usedActions: usedActions, error: (typeof errors === 'object' && errors && policy.id in errors) ? errors[policy.id] : undefined }, policy.id)))), canAddPolicy && (_jsx(Box, { children: _jsx(Button, { type: "button", variant: "outlined", startIcon: _jsx(Add, {}), onClick: addPolicy, disabled: isSubmitting, sx: {
64
+ borderColor: '#d0d7de',
65
+ color: '#656d76',
66
+ '&:hover': {
67
+ borderColor: '#8c959f',
68
+ backgroundColor: 'transparent'
69
+ }
70
+ }, children: t("modules.form.publicPolicies.addPolicy") }) }))] })] })] }));
71
+ };
72
+ export default Policies;
@@ -0,0 +1,21 @@
1
+ import type { PolicyAction } from "../constants";
2
+ type Policy = {
3
+ id: string;
4
+ action: PolicyAction;
5
+ fields: {
6
+ allow: string[];
7
+ owner_allow: string[];
8
+ deny: string[];
9
+ };
10
+ };
11
+ interface PolicyItemProps {
12
+ policy: Policy;
13
+ onChange: (next: Policy) => void;
14
+ onRemove: () => void;
15
+ availableFields: string[];
16
+ isSubmitting?: boolean;
17
+ usedActions: Set<Policy["action"]>;
18
+ error?: string;
19
+ }
20
+ declare const PolicyItem: import("react").ForwardRefExoticComponent<PolicyItemProps & import("react").RefAttributes<HTMLDivElement>>;
21
+ export default PolicyItem;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Box, FormControl, InputLabel, Select, MenuItem, IconButton, Typography, FormHelperText, Stack, Paper, Divider, } from "@mui/material";
5
+ import { Delete } from "@mui/icons-material";
6
+ import FieldSelector from "../FieldSelector";
7
+ import { POLICY_ACTIONS } from "../constants";
8
+ const PolicyItem = forwardRef(({ policy, onChange, onRemove, availableFields, isSubmitting = false, usedActions, error, }, ref) => {
9
+ const { t } = useTranslation();
10
+ const takenActions = new Set(Array.from(usedActions || []));
11
+ takenActions.delete(policy.action);
12
+ const actionOptions = POLICY_ACTIONS.map((a) => ({
13
+ value: a,
14
+ label: t(`modules.form.publicPolicies.fields.action.options.${a}`),
15
+ }));
16
+ return (_jsxs(Paper, { ref: ref, sx: {
17
+ p: 3,
18
+ border: '1px solid #d1d9e0',
19
+ borderRadius: 2,
20
+ position: 'relative',
21
+ backgroundColor: '#ffffff'
22
+ }, children: [_jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", sx: {
23
+ fontWeight: 600,
24
+ color: '#111418',
25
+ fontSize: '1rem'
26
+ }, children: t("modules.form.publicPolicies.policyTitle") }), _jsx(IconButton, { onClick: onRemove, size: "small", disabled: isSubmitting, "aria-label": t("modules.form.publicPolicies.removePolicy"), sx: {
27
+ color: '#656d76',
28
+ '&:hover': {
29
+ color: '#cf222e',
30
+ backgroundColor: 'rgba(207, 34, 46, 0.1)'
31
+ }
32
+ }, children: _jsx(Delete, {}) })] }), _jsxs(Stack, { spacing: 3, children: [_jsx(Stack, { direction: { xs: "column", md: "row" }, spacing: 2, children: _jsx(Box, { sx: { flex: 1, minWidth: 200 }, children: _jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: t("modules.form.publicPolicies.fields.action.label") }), _jsx(Select, { value: policy.action, label: t("modules.form.publicPolicies.fields.action.label"), disabled: isSubmitting, onChange: (e) => {
33
+ const next = { ...policy, action: e.target.value };
34
+ onChange(next);
35
+ }, sx: {
36
+ backgroundColor: '#ffffff',
37
+ '&:hover .MuiOutlinedInput-notchedOutline': {
38
+ borderColor: '#8c959f',
39
+ },
40
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
41
+ borderColor: '#0969da',
42
+ borderWidth: 2,
43
+ }
44
+ }, children: actionOptions.map((option) => {
45
+ const disabledOption = takenActions.has(option.value);
46
+ return (_jsx(MenuItem, { value: option.value, disabled: disabledOption, children: option.label }, option.value));
47
+ }) }), error && (_jsx(FormHelperText, { error: true, children: error }))] }) }) }), _jsx(FieldSelector, { value: policy.fields, onChange: (nextFields) => onChange({ ...policy, fields: nextFields }), availableFields: availableFields, disabled: isSubmitting }), _jsx(Paper, { variant: "outlined", sx: { p: 2, backgroundColor: '#f9fafb' }, children: _jsxs(Stack, { spacing: 0.5, divider: _jsx(Divider, { sx: { borderColor: '#e5e7eb' } }), children: [_jsxs(Typography, { variant: "body2", sx: { fontFamily: 'monospace', color: 'text.secondary' }, children: [_jsxs(Box, { component: "span", sx: { color: '#16a34a' }, children: [t('modules.form.publicPolicies.fields.conditions.states.allow'), ":"] }), " ", (policy?.fields?.allow || []).join(', ') || '-'] }), _jsxs(Typography, { variant: "body2", sx: { fontFamily: 'monospace', color: 'text.secondary' }, children: [_jsxs(Box, { component: "span", sx: { color: '#0ea5e9' }, children: [t('modules.form.publicPolicies.fields.conditions.states.ownerAllow'), ":"] }), " ", (policy?.fields?.owner_allow || []).join(', ') || '-'] }), _jsxs(Typography, { variant: "body2", sx: { fontFamily: 'monospace', color: 'text.secondary' }, children: [_jsxs(Box, { component: "span", sx: { color: '#dc2626' }, children: [t('modules.form.publicPolicies.fields.conditions.states.deny'), ":"] }), " ", (policy?.fields?.deny || []).join(', ') || '-'] })] }) })] })] }));
48
+ });
49
+ export default PolicyItem;
@@ -0,0 +1 @@
1
+ export { default } from "./PolicyItem";
@@ -0,0 +1 @@
1
+ export { default } from "./PolicyItem";
@@ -0,0 +1,3 @@
1
+ export declare const POLICY_ACTIONS: readonly ["create", "read", "update", "delete"];
2
+ export type PolicyAction = typeof POLICY_ACTIONS[number];
3
+ export declare const PREFERRED_POLICY_ORDER: PolicyAction[];
@@ -0,0 +1,7 @@
1
+ export const POLICY_ACTIONS = ["create", "read", "update", "delete"];
2
+ export const PREFERRED_POLICY_ORDER = [
3
+ "create",
4
+ "read",
5
+ "update",
6
+ "delete",
7
+ ];
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as HelloWorld } from "./components/HelloWorld";
2
2
  export { default as Cascada } from "./components/Cascada";
3
+ export { default as Policies } from "./components/PublicPolicies/Policies";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as HelloWorld } from "./components/HelloWorld";
2
2
  export { default as Cascada } from "./components/Cascada";
3
+ export { default as Policies } from "./components/PublicPolicies/Policies";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocios/crudify-ui",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Biblioteca de componentes UI para Crudify",
5
5
  "author": "Nocios",
6
6
  "type": "module",