@xeplr/ui-account 1.0.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 -0
- package/package.json +17 -0
- package/src/AccessContext.jsx +125 -0
- package/src/AccessGuard.jsx +35 -0
- package/src/ProtectedRoute.jsx +55 -0
- package/src/ThemeContext.jsx +84 -0
- package/src/adminApi.js +68 -0
- package/src/api.js +196 -0
- package/src/designs/AccessMatrixSample.jsx +199 -0
- package/src/designs/ActivateSample.jsx +38 -0
- package/src/designs/ChangePasswordSample.jsx +31 -0
- package/src/designs/ForgotPasswordSample.jsx +22 -0
- package/src/designs/LoginSample.jsx +26 -0
- package/src/designs/MasterSettingsSample.jsx +206 -0
- package/src/designs/NotActivatedSample.jsx +17 -0
- package/src/designs/ProfileSample.jsx +36 -0
- package/src/designs/RegisterSample.jsx +34 -0
- package/src/designs/ResetPasswordSample.jsx +42 -0
- package/src/designs/TenantPickerSample.jsx +39 -0
- package/src/designs/TenantSample.jsx +182 -0
- package/src/designs/UserRolesMatrixSample.jsx +77 -0
- package/src/designs/admin.css +525 -0
- package/src/designs/auth.css +160 -0
- package/src/designs/index.js +13 -0
- package/src/designs/theme.css +231 -0
- package/src/index.js +41 -0
- package/src/masterApi.js +28 -0
- package/src/pages.jsx +92 -0
- package/src/token.js +38 -0
- package/src/useAccessMatrixController.js +244 -0
- package/src/useActivateController.js +37 -0
- package/src/useChangePasswordController.js +48 -0
- package/src/useForgotPasswordController.js +32 -0
- package/src/useLoginController.js +53 -0
- package/src/useMasterSettingsController.js +199 -0
- package/src/useProfileController.js +62 -0
- package/src/useRegisterController.js +41 -0
- package/src/useResetPasswordController.js +36 -0
- package/src/useTenantController.js +146 -0
- package/src/useTenantPickerController.js +97 -0
- package/src/useUserRolesController.js +81 -0
- package/src/validateDesign.js +101 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import './admin.css';
|
|
2
|
+
|
|
3
|
+
var ACTION_LABELS = { view: 'View Only', edit: 'Add / Edit', delete: 'Delete' };
|
|
4
|
+
var ACTION_CLASSES = { view: 'xeplr-admin-action-view', edit: 'xeplr-admin-action-edit', delete: 'xeplr-admin-action-delete' };
|
|
5
|
+
var UNCAT_TABS = [
|
|
6
|
+
{ key: 'apis', label: 'APIs' },
|
|
7
|
+
{ key: 'pages', label: 'Pages' },
|
|
8
|
+
{ key: 'elements', label: 'Elements' },
|
|
9
|
+
{ key: 'menus', label: 'Menus' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default function AccessMatrixSample({
|
|
13
|
+
roles, modules, uncategorized, uncategorizedCount, uncatSubTab, setUncatSubTab,
|
|
14
|
+
activeView, setActiveView, search, setSearch,
|
|
15
|
+
loading, error, handleModuleToggle, handleItemToggle,
|
|
16
|
+
isItemAssigned, isModuleSaving, isItemSaving, reload
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="xeplr-admin-container">
|
|
20
|
+
<div className="xeplr-admin-header">
|
|
21
|
+
<h2>Access Matrix</h2>
|
|
22
|
+
<button type="button" onClick={reload} className="xeplr-admin-btn-secondary">Refresh</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{/* Top-level tabs: Modules vs Uncategorized */}
|
|
26
|
+
<div className="xeplr-admin-tabs" role="tablist">
|
|
27
|
+
<button
|
|
28
|
+
role="tab"
|
|
29
|
+
aria-selected={activeView === 'modules'}
|
|
30
|
+
className={'xeplr-admin-tab' + (activeView === 'modules' ? ' xeplr-admin-tab-active' : '')}
|
|
31
|
+
onClick={function() { setActiveView('modules'); }}
|
|
32
|
+
>
|
|
33
|
+
Modules
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
role="tab"
|
|
37
|
+
aria-selected={activeView === 'uncategorized'}
|
|
38
|
+
className={'xeplr-admin-tab' + (activeView === 'uncategorized' ? ' xeplr-admin-tab-active' : '')}
|
|
39
|
+
onClick={function() { setActiveView('uncategorized'); }}
|
|
40
|
+
>
|
|
41
|
+
Uncategorized
|
|
42
|
+
{uncategorizedCount > 0 && <span className="xeplr-admin-tab-badge">{uncategorizedCount}</span>}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Search */}
|
|
47
|
+
<div className="xeplr-admin-toolbar">
|
|
48
|
+
<input
|
|
49
|
+
id="xeplr-admin-access-search"
|
|
50
|
+
type="text"
|
|
51
|
+
placeholder={activeView === 'modules' ? 'Search modules...' : 'Search items...'}
|
|
52
|
+
value={search}
|
|
53
|
+
onChange={function(e) { setSearch(e.target.value); }}
|
|
54
|
+
className="xeplr-admin-search"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{error && <div className="xeplr-admin-alert xeplr-admin-alert-error">{error}</div>}
|
|
59
|
+
|
|
60
|
+
{loading ? (
|
|
61
|
+
<div className="xeplr-admin-loading">Loading...</div>
|
|
62
|
+
) : activeView === 'modules' ? (
|
|
63
|
+
renderModulesView(roles, modules, handleModuleToggle, isModuleSaving)
|
|
64
|
+
) : (
|
|
65
|
+
renderUncategorizedView(roles, uncategorized, uncatSubTab, setUncatSubTab, handleItemToggle, isItemAssigned, isItemSaving)
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderModulesView(roles, modules, handleModuleToggle, isModuleSaving) {
|
|
72
|
+
if (modules.length === 0) {
|
|
73
|
+
return <div className="xeplr-admin-empty-box">No modules found. Items need a <code>module:action</code> group value to appear here.</div>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="xeplr-admin-matrix-wrapper">
|
|
78
|
+
<table className="xeplr-admin-matrix" role="grid">
|
|
79
|
+
<thead>
|
|
80
|
+
<tr>
|
|
81
|
+
<th className="xeplr-admin-sticky-col xeplr-admin-module-col">Module</th>
|
|
82
|
+
<th className="xeplr-admin-action-col">Action</th>
|
|
83
|
+
{roles.map(function(role) {
|
|
84
|
+
return <th key={role.id} className="xeplr-admin-role-header">{role.name}</th>;
|
|
85
|
+
})}
|
|
86
|
+
</tr>
|
|
87
|
+
</thead>
|
|
88
|
+
<tbody>
|
|
89
|
+
{modules.map(function(mod) {
|
|
90
|
+
return mod.actions.map(function(actionEntry, idx) {
|
|
91
|
+
var actionLabel = ACTION_LABELS[actionEntry.action] || actionEntry.action;
|
|
92
|
+
var actionClass = ACTION_CLASSES[actionEntry.action] || '';
|
|
93
|
+
return (
|
|
94
|
+
<tr key={mod.name + ':' + actionEntry.action} className={idx === 0 ? 'xeplr-admin-module-first-row' : ''}>
|
|
95
|
+
{idx === 0 && (
|
|
96
|
+
<td className="xeplr-admin-sticky-col xeplr-admin-module-name" rowSpan={mod.actions.length}>
|
|
97
|
+
{mod.name}
|
|
98
|
+
</td>
|
|
99
|
+
)}
|
|
100
|
+
<td className={'xeplr-admin-action-cell ' + actionClass}>
|
|
101
|
+
{actionLabel}
|
|
102
|
+
</td>
|
|
103
|
+
{roles.map(function(role) {
|
|
104
|
+
var state = actionEntry.getRoleState(role.id);
|
|
105
|
+
var isSaving = isModuleSaving(mod.name, actionEntry.action, role.id);
|
|
106
|
+
return (
|
|
107
|
+
<td key={role.id} className="xeplr-admin-cell">
|
|
108
|
+
<label className="xeplr-admin-toggle">
|
|
109
|
+
<input
|
|
110
|
+
type="checkbox"
|
|
111
|
+
checked={state === 'all'}
|
|
112
|
+
ref={function(el) {
|
|
113
|
+
if (el) el.indeterminate = state === 'partial';
|
|
114
|
+
}}
|
|
115
|
+
disabled={isSaving}
|
|
116
|
+
onChange={function() { handleModuleToggle(mod.name, actionEntry.action, role.id, state); }}
|
|
117
|
+
/>
|
|
118
|
+
<span className={'xeplr-admin-checkmark' + (state === 'partial' ? ' xeplr-admin-partial' : '') + (isSaving ? ' xeplr-admin-saving' : '')} />
|
|
119
|
+
</label>
|
|
120
|
+
</td>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</tr>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
})}
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderUncategorizedView(roles, uncategorized, uncatSubTab, setUncatSubTab, handleItemToggle, isItemAssigned, isItemSaving) {
|
|
134
|
+
var items = uncategorized[uncatSubTab] || [];
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div>
|
|
138
|
+
{/* Sub-tabs for technical types */}
|
|
139
|
+
<div className="xeplr-admin-subtabs">
|
|
140
|
+
{UNCAT_TABS.map(function(tab) {
|
|
141
|
+
var count = (uncategorized[tab.key] || []).length;
|
|
142
|
+
return (
|
|
143
|
+
<button
|
|
144
|
+
key={tab.key}
|
|
145
|
+
className={'xeplr-admin-subtab' + (uncatSubTab === tab.key ? ' xeplr-admin-subtab-active' : '')}
|
|
146
|
+
onClick={function() { setUncatSubTab(tab.key); }}
|
|
147
|
+
>
|
|
148
|
+
{tab.label}
|
|
149
|
+
{count > 0 && <span className="xeplr-admin-subtab-count">{count}</span>}
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="xeplr-admin-matrix-wrapper">
|
|
156
|
+
<table className="xeplr-admin-matrix" role="grid">
|
|
157
|
+
<thead>
|
|
158
|
+
<tr>
|
|
159
|
+
<th className="xeplr-admin-sticky-col">Name</th>
|
|
160
|
+
{roles.map(function(role) {
|
|
161
|
+
return <th key={role.id} className="xeplr-admin-role-header">{role.name}</th>;
|
|
162
|
+
})}
|
|
163
|
+
</tr>
|
|
164
|
+
</thead>
|
|
165
|
+
<tbody>
|
|
166
|
+
{items.length === 0 ? (
|
|
167
|
+
<tr><td colSpan={roles.length + 1} className="xeplr-admin-empty">No uncategorized items</td></tr>
|
|
168
|
+
) : (
|
|
169
|
+
items.map(function(item) {
|
|
170
|
+
return (
|
|
171
|
+
<tr key={item.id}>
|
|
172
|
+
<td className="xeplr-admin-sticky-col xeplr-admin-item-name">{item.name}</td>
|
|
173
|
+
{roles.map(function(role) {
|
|
174
|
+
var assigned = isItemAssigned(item, role.id);
|
|
175
|
+
var savingThis = isItemSaving(uncatSubTab, item.id, role.id);
|
|
176
|
+
return (
|
|
177
|
+
<td key={role.id} className="xeplr-admin-cell">
|
|
178
|
+
<label className="xeplr-admin-toggle">
|
|
179
|
+
<input
|
|
180
|
+
type="checkbox"
|
|
181
|
+
checked={assigned}
|
|
182
|
+
disabled={savingThis}
|
|
183
|
+
onChange={function() { handleItemToggle(uncatSubTab, item.id, role.id, assigned); }}
|
|
184
|
+
/>
|
|
185
|
+
<span className={'xeplr-admin-checkmark' + (savingThis ? ' xeplr-admin-saving' : '')} />
|
|
186
|
+
</label>
|
|
187
|
+
</td>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</tr>
|
|
191
|
+
);
|
|
192
|
+
})
|
|
193
|
+
)}
|
|
194
|
+
</tbody>
|
|
195
|
+
</table>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function ActivateSample({ token, error, success, loading }) {
|
|
5
|
+
if (!token) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="xeplr-auth-container">
|
|
8
|
+
<div className="xeplr-auth-alert xeplr-auth-alert-error">Invalid activation link</div>
|
|
9
|
+
<div className="xeplr-auth-links">
|
|
10
|
+
<p><Link to="/auth/register">Register a new account</Link></p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="xeplr-auth-container">
|
|
18
|
+
<h1>Account Activation</h1>
|
|
19
|
+
{loading && <div className="xeplr-auth-alert">Activating your account...</div>}
|
|
20
|
+
{error && (
|
|
21
|
+
<>
|
|
22
|
+
<div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>
|
|
23
|
+
<div className="xeplr-auth-links">
|
|
24
|
+
<p><Link to="/auth/login">Go to Login</Link></p>
|
|
25
|
+
</div>
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
{success && (
|
|
29
|
+
<>
|
|
30
|
+
<div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>
|
|
31
|
+
<div className="xeplr-auth-links">
|
|
32
|
+
<p><Link to="/auth/login">Go to Login</Link></p>
|
|
33
|
+
</div>
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import './auth.css';
|
|
2
|
+
|
|
3
|
+
export default function ChangePasswordSample({
|
|
4
|
+
currentPassword, setCurrentPassword,
|
|
5
|
+
newPassword, setNewPassword,
|
|
6
|
+
confirmPassword, setConfirmPassword,
|
|
7
|
+
error, success, loading, handleSubmit
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="xeplr-auth-container">
|
|
11
|
+
<h1>Change Password</h1>
|
|
12
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
13
|
+
{success && <div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>}
|
|
14
|
+
<form onSubmit={handleSubmit}>
|
|
15
|
+
<div className="xeplr-auth-form-group">
|
|
16
|
+
<label htmlFor="xeplr-current-password">Current Password</label>
|
|
17
|
+
<input id="xeplr-current-password" type="password" value={currentPassword} onChange={e => setCurrentPassword(e.target.value)} required placeholder="Enter current password" />
|
|
18
|
+
</div>
|
|
19
|
+
<div className="xeplr-auth-form-group">
|
|
20
|
+
<label htmlFor="xeplr-new-password">New Password</label>
|
|
21
|
+
<input id="xeplr-new-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required placeholder="Enter new password" />
|
|
22
|
+
</div>
|
|
23
|
+
<div className="xeplr-auth-form-group">
|
|
24
|
+
<label htmlFor="xeplr-confirm-password">Confirm New Password</label>
|
|
25
|
+
<input id="xeplr-confirm-password" type="password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} required placeholder="Confirm new password" />
|
|
26
|
+
</div>
|
|
27
|
+
<button type="submit" disabled={loading}>{loading ? 'Changing...' : 'Change Password'}</button>
|
|
28
|
+
</form>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function ForgotPasswordSample({ email, setEmail, error, success, loading, handleSubmit }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="xeplr-auth-container">
|
|
7
|
+
<h1>Forgot Password</h1>
|
|
8
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
9
|
+
{success && <div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>}
|
|
10
|
+
<form onSubmit={handleSubmit}>
|
|
11
|
+
<div className="xeplr-auth-form-group">
|
|
12
|
+
<label htmlFor="xeplr-email">Email</label>
|
|
13
|
+
<input id="xeplr-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required placeholder="Enter your registered email" />
|
|
14
|
+
</div>
|
|
15
|
+
<button type="submit" disabled={loading}>{loading ? 'Sending...' : 'Send Reset Link'}</button>
|
|
16
|
+
</form>
|
|
17
|
+
<div className="xeplr-auth-links">
|
|
18
|
+
<p><Link to="/auth/login">Back to Login</Link></p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function LoginSample({ email, setEmail, password, setPassword, error, loading, handleSubmit }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="xeplr-auth-container">
|
|
7
|
+
<h1>Login</h1>
|
|
8
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
9
|
+
<form onSubmit={handleSubmit}>
|
|
10
|
+
<div className="xeplr-auth-form-group">
|
|
11
|
+
<label htmlFor="xeplr-email">Email</label>
|
|
12
|
+
<input id="xeplr-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required placeholder="Enter your email" />
|
|
13
|
+
</div>
|
|
14
|
+
<div className="xeplr-auth-form-group">
|
|
15
|
+
<label htmlFor="xeplr-password">Password</label>
|
|
16
|
+
<input id="xeplr-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required placeholder="Enter your password" />
|
|
17
|
+
</div>
|
|
18
|
+
<button type="submit" disabled={loading}>{loading ? 'Logging in...' : 'Login'}</button>
|
|
19
|
+
</form>
|
|
20
|
+
<div className="xeplr-auth-links">
|
|
21
|
+
<p>Don't have an account? <Link to="/auth/register">Register</Link></p>
|
|
22
|
+
<p><Link to="/auth/forgot-password">Forgot Password?</Link></p>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import './admin.css';
|
|
2
|
+
|
|
3
|
+
export default function MasterSettingsSample({
|
|
4
|
+
tabs, activeTab, setActiveTab, currentTab, items, search, setSearch,
|
|
5
|
+
loading, error, saving, editingItem, editForm, groupNames,
|
|
6
|
+
startAdd, startEdit, cancelEdit, updateField, handleSave, handleDelete, reload
|
|
7
|
+
}) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="xeplr-admin-container">
|
|
10
|
+
<div className="xeplr-admin-header">
|
|
11
|
+
<h2>Master Settings</h2>
|
|
12
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
13
|
+
<button type="button" onClick={startAdd} className="xeplr-admin-btn-primary" disabled={saving || editingItem === '__new__'}>
|
|
14
|
+
+ Add {currentTab.label.replace(/s$/, '')}
|
|
15
|
+
</button>
|
|
16
|
+
<button type="button" onClick={reload} className="xeplr-admin-btn-secondary">Refresh</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{/* Tabs */}
|
|
21
|
+
<div className="xeplr-admin-tabs" role="tablist">
|
|
22
|
+
{tabs.map(function(tab) {
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
key={tab.key}
|
|
26
|
+
role="tab"
|
|
27
|
+
aria-selected={activeTab === tab.key}
|
|
28
|
+
className={'xeplr-admin-tab' + (activeTab === tab.key ? ' xeplr-admin-tab-active' : '')}
|
|
29
|
+
onClick={function() { setActiveTab(tab.key); }}
|
|
30
|
+
>
|
|
31
|
+
{tab.label}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
})}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* Search */}
|
|
38
|
+
<div className="xeplr-admin-toolbar">
|
|
39
|
+
<input
|
|
40
|
+
id="xeplr-admin-master-search"
|
|
41
|
+
type="text"
|
|
42
|
+
placeholder={'Search ' + currentTab.label.toLowerCase() + '...'}
|
|
43
|
+
value={search}
|
|
44
|
+
onChange={function(e) { setSearch(e.target.value); }}
|
|
45
|
+
className="xeplr-admin-search"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{error && <div className="xeplr-admin-alert xeplr-admin-alert-error">{error}</div>}
|
|
50
|
+
|
|
51
|
+
{loading ? (
|
|
52
|
+
<div className="xeplr-admin-loading">Loading...</div>
|
|
53
|
+
) : (
|
|
54
|
+
<div className="xeplr-admin-matrix-wrapper">
|
|
55
|
+
<table className="xeplr-admin-matrix xeplr-admin-master-table">
|
|
56
|
+
<thead>
|
|
57
|
+
<tr>
|
|
58
|
+
{currentTab.fields.map(function(field) {
|
|
59
|
+
return <th key={field.key} className={field.key === 'name' ? 'xeplr-admin-sticky-col' : ''}>{field.label}</th>;
|
|
60
|
+
})}
|
|
61
|
+
<th className="xeplr-admin-actions-col">Actions</th>
|
|
62
|
+
</tr>
|
|
63
|
+
</thead>
|
|
64
|
+
<tbody>
|
|
65
|
+
{/* New item row */}
|
|
66
|
+
{editingItem === '__new__' && (
|
|
67
|
+
<tr className="xeplr-admin-editing-row">
|
|
68
|
+
{currentTab.fields.map(function(field) {
|
|
69
|
+
return (
|
|
70
|
+
<td key={field.key} className={field.key === 'name' ? 'xeplr-admin-sticky-col' : ''}>
|
|
71
|
+
{renderField(field, editForm[field.key], function(val) { updateField(field.key, val); }, groupNames)}
|
|
72
|
+
</td>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
<td className="xeplr-admin-actions-col">
|
|
76
|
+
<div className="xeplr-admin-action-btns">
|
|
77
|
+
<button onClick={handleSave} disabled={saving} className="xeplr-admin-btn-save" title="Save">
|
|
78
|
+
{saving ? '...' : '\u2713'}
|
|
79
|
+
</button>
|
|
80
|
+
<button onClick={cancelEdit} className="xeplr-admin-btn-cancel" title="Cancel">
|
|
81
|
+
{'\u2715'}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{items.length === 0 && editingItem !== '__new__' ? (
|
|
89
|
+
<tr><td colSpan={currentTab.fields.length + 1} className="xeplr-admin-empty">No {currentTab.label.toLowerCase()} found</td></tr>
|
|
90
|
+
) : (
|
|
91
|
+
items.map(function(item) {
|
|
92
|
+
var isEditing = editingItem === item.id;
|
|
93
|
+
return (
|
|
94
|
+
<tr key={item.id} className={isEditing ? 'xeplr-admin-editing-row' : ''}>
|
|
95
|
+
{currentTab.fields.map(function(field) {
|
|
96
|
+
var cellClass = field.key === 'name' ? 'xeplr-admin-sticky-col' : '';
|
|
97
|
+
if (isEditing) {
|
|
98
|
+
return (
|
|
99
|
+
<td key={field.key} className={cellClass}>
|
|
100
|
+
{renderField(field, editForm[field.key], function(val) { updateField(field.key, val); }, groupNames)}
|
|
101
|
+
</td>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
<td key={field.key} className={cellClass}>
|
|
106
|
+
{renderValue(field, item[field.key])}
|
|
107
|
+
</td>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
<td className="xeplr-admin-actions-col">
|
|
111
|
+
{isEditing ? (
|
|
112
|
+
<div className="xeplr-admin-action-btns">
|
|
113
|
+
<button onClick={handleSave} disabled={saving} className="xeplr-admin-btn-save" title="Save">
|
|
114
|
+
{saving ? '...' : '\u2713'}
|
|
115
|
+
</button>
|
|
116
|
+
<button onClick={cancelEdit} className="xeplr-admin-btn-cancel" title="Cancel">
|
|
117
|
+
{'\u2715'}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="xeplr-admin-action-btns">
|
|
122
|
+
<button onClick={function() { startEdit(item); }} className="xeplr-admin-btn-edit" title="Edit" disabled={editingItem !== null}>
|
|
123
|
+
{'\u270E'}
|
|
124
|
+
</button>
|
|
125
|
+
<button onClick={function() { if (confirm('Delete this item?')) handleDelete(item.id); }} className="xeplr-admin-btn-delete" title="Delete" disabled={saving || editingItem !== null}>
|
|
126
|
+
{'\u2715'}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
)}
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderField(field, value, onChange, groupNames) {
|
|
144
|
+
if (field.type === 'boolean') {
|
|
145
|
+
return (
|
|
146
|
+
<label className="xeplr-admin-toggle">
|
|
147
|
+
<input
|
|
148
|
+
type="checkbox"
|
|
149
|
+
checked={!!value}
|
|
150
|
+
onChange={function(e) { onChange(e.target.checked ? 1 : 0); }}
|
|
151
|
+
/>
|
|
152
|
+
<span className="xeplr-admin-checkmark" />
|
|
153
|
+
</label>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (field.type === 'group') {
|
|
158
|
+
return (
|
|
159
|
+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
160
|
+
<select
|
|
161
|
+
className="xeplr-admin-inline-input xeplr-admin-group-input"
|
|
162
|
+
style={{ flex: 1 }}
|
|
163
|
+
value={(groupNames || []).indexOf(value) !== -1 ? value : ''}
|
|
164
|
+
onChange={function(e) { onChange(e.target.value); }}
|
|
165
|
+
>
|
|
166
|
+
<option value="">-- select or type new --</option>
|
|
167
|
+
{(groupNames || []).map(function(name) {
|
|
168
|
+
return <option key={name} value={name}>{name}</option>;
|
|
169
|
+
})}
|
|
170
|
+
</select>
|
|
171
|
+
<input
|
|
172
|
+
type="text"
|
|
173
|
+
value={value || ''}
|
|
174
|
+
onChange={function(e) { onChange(e.target.value); }}
|
|
175
|
+
placeholder={field.placeholder || 'or type new group'}
|
|
176
|
+
className="xeplr-admin-inline-input xeplr-admin-group-input"
|
|
177
|
+
style={{ flex: 1 }}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
value={value || ''}
|
|
187
|
+
onChange={function(e) { onChange(e.target.value); }}
|
|
188
|
+
placeholder={field.placeholder || ''}
|
|
189
|
+
className="xeplr-admin-inline-input"
|
|
190
|
+
autoFocus={field.required}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderValue(field, value) {
|
|
196
|
+
if (field.type === 'boolean') {
|
|
197
|
+
return <span className={value ? 'xeplr-admin-badge-yes' : 'xeplr-admin-badge-no'}>{value ? 'Yes' : 'No'}</span>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var isGroup = field.key.toLowerCase().includes('group');
|
|
201
|
+
if (isGroup && value) {
|
|
202
|
+
return <span className="xeplr-admin-group-tag">{value}</span>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return <span className={field.key === 'name' ? 'xeplr-admin-item-name' : ''}>{value || '\u2014'}</span>;
|
|
206
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function NotActivatedSample() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="xeplr-auth-container">
|
|
7
|
+
<div className="xeplr-auth-not-activated">
|
|
8
|
+
<h1>Account Pending Activation</h1>
|
|
9
|
+
<p className="xeplr-auth-message">Please wait, someone will activate you.</p>
|
|
10
|
+
<p>Your account has been created but is not yet activated. An administrator will review and activate your account shortly.</p>
|
|
11
|
+
</div>
|
|
12
|
+
<div className="xeplr-auth-links">
|
|
13
|
+
<p><Link to="/auth/login">Back to Login</Link></p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import './auth.css';
|
|
2
|
+
|
|
3
|
+
export default function ProfileSample({
|
|
4
|
+
form, error, success, loading, fetching, handleChange, handleSubmit
|
|
5
|
+
}) {
|
|
6
|
+
if (fetching) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="xeplr-auth-container">
|
|
9
|
+
<p>Loading profile...</p>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="xeplr-auth-container">
|
|
16
|
+
<h1>Profile</h1>
|
|
17
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
18
|
+
{success && <div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>}
|
|
19
|
+
<form onSubmit={handleSubmit}>
|
|
20
|
+
<div className="xeplr-auth-form-group">
|
|
21
|
+
<label htmlFor="xeplr-profile-name">Name</label>
|
|
22
|
+
<input id="xeplr-profile-name" name="name" type="text" value={form.name} onChange={handleChange} placeholder="Your name" />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="xeplr-auth-form-group">
|
|
25
|
+
<label htmlFor="xeplr-profile-email">Email</label>
|
|
26
|
+
<input id="xeplr-profile-email" name="email" type="email" value={form.email} onChange={handleChange} placeholder="Your email" disabled />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="xeplr-auth-form-group">
|
|
29
|
+
<label htmlFor="xeplr-profile-phone">Phone Number</label>
|
|
30
|
+
<input id="xeplr-profile-phone" name="phoneNumber" type="text" value={form.phoneNumber} onChange={handleChange} placeholder="Your phone number" />
|
|
31
|
+
</div>
|
|
32
|
+
<button type="submit" disabled={loading}>{loading ? 'Saving...' : 'Save Changes'}</button>
|
|
33
|
+
</form>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function RegisterSample({ form, error, success, loading, handleChange, handleSubmit }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="xeplr-auth-container">
|
|
7
|
+
<h1>Register</h1>
|
|
8
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
9
|
+
{success && <div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>}
|
|
10
|
+
<form onSubmit={handleSubmit}>
|
|
11
|
+
<div className="xeplr-auth-form-group">
|
|
12
|
+
<label htmlFor="xeplr-name">Name</label>
|
|
13
|
+
<input id="xeplr-name" name="name" type="text" value={form.name} onChange={handleChange} placeholder="Enter your name" />
|
|
14
|
+
</div>
|
|
15
|
+
<div className="xeplr-auth-form-group">
|
|
16
|
+
<label htmlFor="xeplr-email">Email</label>
|
|
17
|
+
<input id="xeplr-email" name="email" type="email" value={form.email} onChange={handleChange} required placeholder="Enter your email" />
|
|
18
|
+
</div>
|
|
19
|
+
<div className="xeplr-auth-form-group">
|
|
20
|
+
<label htmlFor="xeplr-phone">Phone Number</label>
|
|
21
|
+
<input id="xeplr-phone" name="phoneNumber" type="text" value={form.phoneNumber} onChange={handleChange} placeholder="Enter your phone number" />
|
|
22
|
+
</div>
|
|
23
|
+
<div className="xeplr-auth-form-group">
|
|
24
|
+
<label htmlFor="xeplr-password">Password</label>
|
|
25
|
+
<input id="xeplr-password" name="password" type="password" value={form.password} onChange={handleChange} required placeholder="Enter your password" />
|
|
26
|
+
</div>
|
|
27
|
+
<button type="submit" disabled={loading}>{loading ? 'Registering...' : 'Register'}</button>
|
|
28
|
+
</form>
|
|
29
|
+
<div className="xeplr-auth-links">
|
|
30
|
+
<p>Already have an account? <Link to="/auth/login">Login</Link></p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import './auth.css';
|
|
3
|
+
|
|
4
|
+
export default function ResetPasswordSample({ token, password, setPassword, error, success, loading, handleSubmit }) {
|
|
5
|
+
if (!token && !success) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="xeplr-auth-container">
|
|
8
|
+
<div className="xeplr-auth-alert xeplr-auth-alert-error">Invalid reset link</div>
|
|
9
|
+
<div className="xeplr-auth-links">
|
|
10
|
+
<p><Link to="/auth/forgot-password">Request a new reset link</Link></p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="xeplr-auth-container">
|
|
18
|
+
<h1>Reset Password</h1>
|
|
19
|
+
{error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
|
|
20
|
+
{success && (
|
|
21
|
+
<>
|
|
22
|
+
<div className="xeplr-auth-alert xeplr-auth-alert-success">{success}</div>
|
|
23
|
+
<div className="xeplr-auth-links">
|
|
24
|
+
<p><Link to="/auth/login">Go to Login</Link></p>
|
|
25
|
+
</div>
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
{!success && (
|
|
29
|
+
<form onSubmit={handleSubmit}>
|
|
30
|
+
<div className="xeplr-auth-form-group">
|
|
31
|
+
<label htmlFor="xeplr-password">New Password</label>
|
|
32
|
+
<input id="xeplr-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required placeholder="Enter new password" />
|
|
33
|
+
</div>
|
|
34
|
+
<button type="submit" disabled={loading}>{loading ? 'Resetting...' : 'Reset Password'}</button>
|
|
35
|
+
</form>
|
|
36
|
+
)}
|
|
37
|
+
<div className="xeplr-auth-links">
|
|
38
|
+
<p><Link to="/auth/login">Back to Login</Link></p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|