@visns-studio/visns-components 6.0.3 → 6.0.5
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/package.json +1 -1
- package/src/components/DataGrid.jsx +30 -12
- package/src/components/auth/Login.jsx +203 -175
- package/src/components/columns/ColumnRenderers.jsx +34 -0
- package/src/components/generic/GenericFormBuilder.jsx +550 -25
- package/src/components/generic/GenericIndex.jsx +15 -5
- package/src/components/styles/DataGrid.module.scss +38 -22
- package/src/components/styles/Form.module.scss +32 -33
- package/src/components/styles/GenericDetail.module.scss +28 -30
- package/src/components/styles/GenericDynamic.module.scss +7 -21
- package/src/components/styles/GenericEditableTable.module.scss +5 -22
- package/src/components/styles/GenericFormBuilder.module.scss +545 -30
- package/src/components/styles/GenericIndex.module.scss +17 -32
- package/src/components/styles/GenericMain.module.scss +18 -6
- package/src/components/styles/GenericQuote.module.scss +7 -22
- package/src/components/styles/GenericReport.module.scss +5 -37
- package/src/components/styles/Login.module.scss +415 -239
- package/src/components/styles/Navigation.module.scss +124 -35
- package/src/components/styles/Profile.module.scss +5 -26
- package/src/components/styles/QuickAction.module.scss +34 -29
- package/src/components/styles/_controls.scss +139 -0
- package/src/components/styles/global.css +54 -15
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.0.
|
|
94
|
+
"version": "6.0.5",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -236,6 +236,34 @@ const loadData = async (
|
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* How tall the grid should be.
|
|
241
|
+
*
|
|
242
|
+
* It used to claim 70% of the viewport (or 550px) for any result set larger
|
|
243
|
+
* than eight rows, whatever the actual row count — so 18 rows on a tall
|
|
244
|
+
* monitor left several hundred pixels of empty white below the last one.
|
|
245
|
+
*
|
|
246
|
+
* Height now follows the content and only falls back to a viewport share when
|
|
247
|
+
* the rows genuinely overflow, which is the point at which a tall scroller is
|
|
248
|
+
* useful rather than just empty.
|
|
249
|
+
*/
|
|
250
|
+
const ROW_HEIGHT = 34;
|
|
251
|
+
const GRID_CHROME = ROW_HEIGHT * 2 + 60; // header + filter row + footer
|
|
252
|
+
|
|
253
|
+
function fitToRows(dataCount, windowHeight) {
|
|
254
|
+
const roomy = Math.max(windowHeight * 0.7, 550);
|
|
255
|
+
|
|
256
|
+
if (!dataCount || dataCount <= 0) {
|
|
257
|
+
// Nothing to show: enough for the empty state, not a blank screen.
|
|
258
|
+
return 300;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const needed = dataCount * ROW_HEIGHT + GRID_CHROME;
|
|
262
|
+
|
|
263
|
+
// Never taller than the rows require, never so short it feels cramped.
|
|
264
|
+
return Math.min(roomy, Math.max(needed, 260));
|
|
265
|
+
}
|
|
266
|
+
|
|
239
267
|
const DataGrid = forwardRef(
|
|
240
268
|
(
|
|
241
269
|
{
|
|
@@ -4380,13 +4408,7 @@ const DataGrid = forwardRef(
|
|
|
4380
4408
|
minHeight:
|
|
4381
4409
|
gridHeight && gridHeight > 0
|
|
4382
4410
|
? gridHeight
|
|
4383
|
-
: dataCount
|
|
4384
|
-
? dataCount > 8
|
|
4385
|
-
? windowHeight * 0.7 > 550
|
|
4386
|
-
? windowHeight * 0.7
|
|
4387
|
-
: 550
|
|
4388
|
-
: 450
|
|
4389
|
-
: 400,
|
|
4411
|
+
: fitToRows(dataCount, windowHeight),
|
|
4390
4412
|
boxShadow: 'none',
|
|
4391
4413
|
...(hoverColor ? { '--hover-color': hoverColor } : {}),
|
|
4392
4414
|
});
|
|
@@ -4397,11 +4419,7 @@ const DataGrid = forwardRef(
|
|
|
4397
4419
|
useEffect(() => {
|
|
4398
4420
|
const getMinimumHeight = () => {
|
|
4399
4421
|
if (gridHeight > 0) return gridHeight;
|
|
4400
|
-
|
|
4401
|
-
if (dataCount <= 5) return 400;
|
|
4402
|
-
if (dataCount <= 8) return 450;
|
|
4403
|
-
}
|
|
4404
|
-
return Math.max(window.innerHeight * 0.7, 550);
|
|
4422
|
+
return fitToRows(dataCount, window.innerHeight);
|
|
4405
4423
|
};
|
|
4406
4424
|
|
|
4407
4425
|
const minHeight = getMinimumHeight();
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
5
|
-
import Reveal from '../utils/Reveal';
|
|
6
5
|
import { toast } from 'react-toastify';
|
|
7
|
-
import {
|
|
6
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
8
7
|
|
|
9
8
|
import CustomFetch from '../Fetch';
|
|
10
9
|
|
|
11
10
|
import styles from '../styles/Login.module.scss';
|
|
12
11
|
|
|
13
|
-
const Login = ({
|
|
12
|
+
const Login = ({
|
|
13
|
+
logo,
|
|
14
|
+
loginBg,
|
|
15
|
+
providers,
|
|
16
|
+
setSystemAuth,
|
|
17
|
+
setUserProfile,
|
|
18
|
+
config = {},
|
|
19
|
+
}) => {
|
|
14
20
|
const location = useLocation();
|
|
15
21
|
const navigate = useNavigate();
|
|
16
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
23
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Collapsed behind a button, since Microsoft is how nearly everyone signs
|
|
26
|
+
// in. Open from the start when there is no SSO provider configured —
|
|
27
|
+
// otherwise the only way in would be hidden behind a disclosure.
|
|
28
|
+
const ssoAvailable = Array.isArray(providers) && providers.length > 0;
|
|
29
|
+
const [showEmail, setShowEmail] = useState(!ssoAvailable);
|
|
30
|
+
const emailRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
// Opening the form should put the cursor where typing starts, rather than
|
|
33
|
+
// leaving it on the button that opened it.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (showEmail && ssoAvailable) {
|
|
36
|
+
emailRef.current?.focus();
|
|
37
|
+
}
|
|
38
|
+
}, [showEmail, ssoAvailable]);
|
|
17
39
|
|
|
18
40
|
const [auth, setAuth] = useState({
|
|
19
41
|
email: '',
|
|
@@ -32,43 +54,37 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
|
|
|
32
54
|
return null;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
let content =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</>
|
|
67
|
-
);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
57
|
+
let content = null;
|
|
58
|
+
|
|
59
|
+
providers.forEach((provider) => {
|
|
60
|
+
switch (provider) {
|
|
61
|
+
case 'azure':
|
|
62
|
+
content = (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className={styles.sso}
|
|
66
|
+
onClick={(e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
window.location.href = '/auth/azure';
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{/* The Microsoft mark keeps its own light tile.
|
|
72
|
+
Its four squares are drawn for a white ground
|
|
73
|
+
and sit awkwardly straight on navy. */}
|
|
74
|
+
<span className={styles.ssoMark}>
|
|
75
|
+
<img
|
|
76
|
+
src="https://d16lktya8ojp5z.cloudfront.net/generic/images/microsoft.png"
|
|
77
|
+
width="18"
|
|
78
|
+
height="18"
|
|
79
|
+
alt=""
|
|
80
|
+
/>
|
|
81
|
+
</span>
|
|
82
|
+
Sign in with Microsoft
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
72
88
|
|
|
73
89
|
return content;
|
|
74
90
|
};
|
|
@@ -162,141 +178,153 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
|
|
|
162
178
|
};
|
|
163
179
|
|
|
164
180
|
return (
|
|
165
|
-
<div className={styles.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
181
|
+
<div className={styles.auth}>
|
|
182
|
+
{/* The company's own work, not decoration. It is the reason the
|
|
183
|
+
rest of this system exists, so it carries the brand side and
|
|
184
|
+
the form is left to be a form. */}
|
|
185
|
+
<aside
|
|
186
|
+
className={styles.brand}
|
|
187
|
+
style={
|
|
188
|
+
loginBg ? { backgroundImage: `url(${loginBg})` } : undefined
|
|
189
|
+
}
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
>
|
|
192
|
+
<div className={styles.brandInk}>
|
|
193
|
+
<p className={styles.eyebrow}>Prime Builders</p>
|
|
194
|
+
<p className={styles.brandLine}>Prime Projects</p>
|
|
195
|
+
<p className={styles.brandSub}>
|
|
196
|
+
Job tracking, inspections, site forms and labour
|
|
197
|
+
scheduling — in one place.
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</aside>
|
|
201
|
+
|
|
202
|
+
<main className={styles.panel}>
|
|
203
|
+
<div className={styles.panelInner}>
|
|
204
|
+
<img src={logo} alt="Prime Builders" className={styles.logo} />
|
|
205
|
+
|
|
206
|
+
<h1 className={styles.title}>Sign in</h1>
|
|
207
|
+
<p className={styles.subtitle}>
|
|
208
|
+
Use your Prime Microsoft account to continue.
|
|
209
|
+
</p>
|
|
210
|
+
|
|
211
|
+
{/* Primary: this is how Prime staff actually sign in. The
|
|
212
|
+
email form stays available underneath for accounts
|
|
213
|
+
without a Microsoft sign-in. */}
|
|
214
|
+
{renderSsoButton()}
|
|
215
|
+
|
|
216
|
+
{ssoAvailable && !showEmail && (
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
className={styles.altToggle}
|
|
220
|
+
onClick={() => setShowEmail(true)}
|
|
221
|
+
aria-expanded="false"
|
|
222
|
+
aria-controls="email-signin"
|
|
223
|
+
>
|
|
224
|
+
Sign in with email instead
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{showEmail && ssoAvailable && (
|
|
229
|
+
<div className={styles.divider}>
|
|
230
|
+
<span>or sign in with email</span>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* A grid row animating 0fr -> 1fr, so it opens to whatever
|
|
235
|
+
height the form needs without hardcoding one. */}
|
|
236
|
+
<div
|
|
237
|
+
id="email-signin"
|
|
238
|
+
className={`${styles.collapse} ${
|
|
239
|
+
showEmail ? styles.collapseOpen : ''
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
<div className={styles.collapseInner}>
|
|
243
|
+
<form onSubmit={handleSubmit} className={styles.form}>
|
|
244
|
+
<div className={styles.field}>
|
|
245
|
+
<input
|
|
246
|
+
id="login-email"
|
|
247
|
+
ref={emailRef}
|
|
248
|
+
type="text"
|
|
249
|
+
name="email"
|
|
250
|
+
value={auth.email}
|
|
251
|
+
onChange={handleChange}
|
|
252
|
+
tabIndex="1"
|
|
253
|
+
className={authClass.username}
|
|
254
|
+
autoComplete="username"
|
|
255
|
+
placeholder=" "
|
|
256
|
+
/>
|
|
257
|
+
<label htmlFor="login-email">Email</label>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className={styles.field}>
|
|
261
|
+
<input
|
|
262
|
+
id="login-password"
|
|
263
|
+
type={showPassword ? 'text' : 'password'}
|
|
264
|
+
name="password"
|
|
265
|
+
value={auth.password}
|
|
266
|
+
onChange={handleChange}
|
|
267
|
+
tabIndex="2"
|
|
268
|
+
className={authClass.password}
|
|
269
|
+
autoComplete="current-password"
|
|
270
|
+
placeholder=" "
|
|
271
|
+
/>
|
|
272
|
+
<label htmlFor="login-password">Password</label>
|
|
273
|
+
{/* Typing a password blind on a phone in the sun is
|
|
274
|
+
where most failed sign-ins come from. */}
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
className={styles.reveal}
|
|
278
|
+
onClick={() => setShowPassword((on) => !on)}
|
|
279
|
+
aria-label={
|
|
280
|
+
showPassword
|
|
281
|
+
? 'Hide password'
|
|
282
|
+
: 'Show password'
|
|
283
|
+
}
|
|
284
|
+
tabIndex="-1"
|
|
285
|
+
>
|
|
286
|
+
{showPassword ? (
|
|
287
|
+
<EyeOff size={18} strokeWidth={1.9} />
|
|
288
|
+
) : (
|
|
289
|
+
<Eye size={18} strokeWidth={1.9} />
|
|
290
|
+
)}
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div className={styles.row}>
|
|
295
|
+
<label className={styles.remember}>
|
|
296
|
+
<input
|
|
297
|
+
type="checkbox"
|
|
298
|
+
name="remember"
|
|
299
|
+
checked={auth.remember}
|
|
300
|
+
onChange={handleChange}
|
|
301
|
+
/>
|
|
302
|
+
Remember me
|
|
303
|
+
</label>
|
|
304
|
+
|
|
305
|
+
{config.passwordReset !== false && (
|
|
306
|
+
<Link to="/reset" className={styles.link}>
|
|
307
|
+
Forgot password?
|
|
308
|
+
</Link>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<button
|
|
313
|
+
className={styles.submit}
|
|
314
|
+
type="submit"
|
|
315
|
+
disabled={isLoading}
|
|
316
|
+
>
|
|
317
|
+
{isLoading ? 'Signing in…' : 'Sign in with email'}
|
|
318
|
+
</button>
|
|
319
|
+
</form>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<p className={styles.foot}>
|
|
324
|
+
Prime Builders · Perth, Western Australia
|
|
325
|
+
</p>
|
|
298
326
|
</div>
|
|
299
|
-
</
|
|
327
|
+
</main>
|
|
300
328
|
</div>
|
|
301
329
|
);
|
|
302
330
|
};
|
|
@@ -152,6 +152,40 @@ export const renderBooleanColumn = ({
|
|
|
152
152
|
render: ({ data }) => {
|
|
153
153
|
const rawValue = data[column.id];
|
|
154
154
|
const formattedValue = formatCellContent(rawValue, { ...column, type: 'boolean' });
|
|
155
|
+
|
|
156
|
+
// Coloured pill variant — opt in via column.badge, matching the
|
|
157
|
+
// option renderer. A yes/no column rendered as plain text reads
|
|
158
|
+
// the same as every other cell, so the state has to be read
|
|
159
|
+
// rather than scanned. Without column.badge nothing changes.
|
|
160
|
+
if (column.badge) {
|
|
161
|
+
const on = Boolean(rawValue) && rawValue !== '0';
|
|
162
|
+
const palette = on
|
|
163
|
+
? (column.badgeOn || { bg: '#eaf6ef', fg: '#1f6b41' })
|
|
164
|
+
: (column.badgeOff || { bg: '#f1f1ee', fg: '#6b7688' });
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<CellWithTooltip value={rawValue} columnType="boolean">
|
|
168
|
+
<span
|
|
169
|
+
style={{
|
|
170
|
+
display: 'inline-block',
|
|
171
|
+
padding: '2px 10px',
|
|
172
|
+
borderRadius: '999px',
|
|
173
|
+
fontSize: '11px',
|
|
174
|
+
fontWeight: 600,
|
|
175
|
+
lineHeight: 1.6,
|
|
176
|
+
letterSpacing: '0.02em',
|
|
177
|
+
textTransform: 'uppercase',
|
|
178
|
+
whiteSpace: 'nowrap',
|
|
179
|
+
backgroundColor: palette.bg,
|
|
180
|
+
color: palette.fg,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{formattedValue}
|
|
184
|
+
</span>
|
|
185
|
+
</CellWithTooltip>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
return (
|
|
156
190
|
<CellWithTooltip value={rawValue} columnType="boolean">
|
|
157
191
|
<span>{formattedValue}</span>
|