nucleus-core-ts 0.9.884 → 0.9.886
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/dist/.build-ok +1 -1
- package/dist/fe/components/Captcha/components/Captcha.d.ts +1 -1
- package/dist/fe/components/Captcha/components/Captcha.js +26 -24
- package/dist/fe/components/Captcha/index.d.ts +1 -0
- package/dist/fe/components/Captcha/index.js +1 -0
- package/dist/fe/components/Captcha/labels.d.ts +44 -0
- package/dist/fe/components/Captcha/labels.js +33 -0
- package/dist/fe/components/Captcha/types.d.ts +4 -0
- package/dist/fe/components/ChatPanel/components/ChatPanel.js +1 -0
- package/dist/fe/components/ChatPanel/components/NewConversationModal.d.ts +3 -4
- package/dist/fe/components/ChatPanel/components/NewConversationModal.js +21 -10
- package/dist/fe/components/ChatPanel/labels.d.ts +8 -0
- package/dist/fe/components/ChatPanel/labels.js +4 -1
- package/dist/fe/components/SelectBox/components/SelectBox.js +8 -1
- package/dist/fe/components/UsersPage/components/RoleAssignmentModal.js +6 -1
- package/dist/fe/components/UsersPage/components/UsersPage.js +8 -2
- package/dist/fe/index.d.ts +3 -2
- package/dist/fe/index.js +5 -1
- package/dist/fe/utils/searchFold.d.ts +40 -0
- package/dist/fe/utils/searchFold.js +60 -0
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.886
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ReactElement } from 'react';
|
|
2
2
|
import type { CaptchaProps } from '../types';
|
|
3
|
-
export declare function Captcha({ generateAction, config, onChallengeLoad, onAnswerChange, disabled, autoGenerate, className, label, errorMessage, themeOverride, }: CaptchaProps): ReactElement;
|
|
3
|
+
export declare function Captcha({ generateAction, config, onChallengeLoad, onAnswerChange, disabled, autoGenerate, className, label, labels: labelOverrides, errorMessage, themeOverride, }: CaptchaProps): ReactElement;
|
|
@@ -4,6 +4,7 @@ import { useGSAP } from '@gsap/react';
|
|
|
4
4
|
import gsap from 'gsap';
|
|
5
5
|
import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../../utils/cn';
|
|
7
|
+
import { DEFAULT_CAPTCHA_LABELS } from '../labels';
|
|
7
8
|
import { captchaTheme } from '../theme';
|
|
8
9
|
gsap.registerPlugin(useGSAP);
|
|
9
10
|
function mergeTheme(base, override) {
|
|
@@ -62,7 +63,14 @@ const RefreshIcon = ({ className, iconRef })=>/*#__PURE__*/ _jsxs("svg", {
|
|
|
62
63
|
})
|
|
63
64
|
]
|
|
64
65
|
});
|
|
65
|
-
export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChange, disabled = false, autoGenerate = true, className, label
|
|
66
|
+
export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChange, disabled = false, autoGenerate = true, className, label, labels: labelOverrides, errorMessage, themeOverride }) {
|
|
67
|
+
// `label` came first and some installs pass it; it still wins for the
|
|
68
|
+
// heading so nothing that already works changes.
|
|
69
|
+
const labels = {
|
|
70
|
+
...DEFAULT_CAPTCHA_LABELS,
|
|
71
|
+
...labelOverrides
|
|
72
|
+
};
|
|
73
|
+
const heading = label ?? labels.heading;
|
|
66
74
|
const theme = themeOverride ? mergeTheme(captchaTheme, themeOverride) : captchaTheme;
|
|
67
75
|
const containerRef = useRef(null);
|
|
68
76
|
const challengeRef = useRef(null);
|
|
@@ -89,9 +97,8 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
89
97
|
},
|
|
90
98
|
onAfterHandle: (response)=>{
|
|
91
99
|
const { data } = response;
|
|
92
|
-
console.log('[DEBUG]:', response);
|
|
93
100
|
if (!data || data.rateLimited) {
|
|
94
|
-
setRateLimitError(response.message ??
|
|
101
|
+
setRateLimitError(response.message ?? labels.tooManyRequests);
|
|
95
102
|
setChallengeData(null);
|
|
96
103
|
return;
|
|
97
104
|
}
|
|
@@ -111,7 +118,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
111
118
|
},
|
|
112
119
|
onErrorHandle: (_error, code)=>{
|
|
113
120
|
if (code === 429) {
|
|
114
|
-
setRateLimitError(
|
|
121
|
+
setRateLimitError(labels.tooManyRequests);
|
|
115
122
|
}
|
|
116
123
|
setChallengeData(null);
|
|
117
124
|
}
|
|
@@ -186,17 +193,17 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
186
193
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
187
194
|
};
|
|
188
195
|
const getPlaceholder = ()=>{
|
|
189
|
-
if (!challenge) return
|
|
196
|
+
if (!challenge) return labels.enterAnswer;
|
|
190
197
|
switch(challenge.type){
|
|
191
198
|
case 'math':
|
|
192
|
-
return
|
|
199
|
+
return labels.enterResult;
|
|
193
200
|
case 'image':
|
|
194
201
|
case 'text':
|
|
195
|
-
return
|
|
202
|
+
return labels.enterTextShown;
|
|
196
203
|
case 'puzzle':
|
|
197
|
-
return
|
|
204
|
+
return labels.enterPieceOrder;
|
|
198
205
|
default:
|
|
199
|
-
return
|
|
206
|
+
return labels.enterAnswer;
|
|
200
207
|
}
|
|
201
208
|
};
|
|
202
209
|
if (config?.enabled === false) {
|
|
@@ -210,7 +217,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
210
217
|
children: [
|
|
211
218
|
/*#__PURE__*/ _jsx("span", {
|
|
212
219
|
className: theme.label.base,
|
|
213
|
-
children:
|
|
220
|
+
children: heading
|
|
214
221
|
}),
|
|
215
222
|
/*#__PURE__*/ _jsx("div", {
|
|
216
223
|
ref: challengeRef,
|
|
@@ -234,7 +241,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
234
241
|
className: theme.challenge.image.wrapper,
|
|
235
242
|
children: /*#__PURE__*/ _jsx("img", {
|
|
236
243
|
src: challenge.imageData,
|
|
237
|
-
alt:
|
|
244
|
+
alt: labels.imageAlt,
|
|
238
245
|
className: theme.challenge.image.img,
|
|
239
246
|
draggable: false
|
|
240
247
|
})
|
|
@@ -242,14 +249,9 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
242
249
|
challenge.type === 'puzzle' && challenge.puzzleData && /*#__PURE__*/ _jsxs("div", {
|
|
243
250
|
className: theme.challenge.puzzle.wrapper,
|
|
244
251
|
children: [
|
|
245
|
-
/*#__PURE__*/
|
|
252
|
+
/*#__PURE__*/ _jsx("p", {
|
|
246
253
|
className: theme.challenge.puzzle.instruction,
|
|
247
|
-
children:
|
|
248
|
-
"Click the pieces in the correct order (0 to",
|
|
249
|
-
' ',
|
|
250
|
-
challenge.puzzleData.pieces.length - 1,
|
|
251
|
-
")"
|
|
252
|
-
]
|
|
254
|
+
children: labels.puzzleInstruction.replace('{last}', String(challenge.puzzleData.pieces.length - 1))
|
|
253
255
|
}),
|
|
254
256
|
/*#__PURE__*/ _jsx("div", {
|
|
255
257
|
className: theme.challenge.puzzle.grid,
|
|
@@ -272,7 +274,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
272
274
|
type: "button",
|
|
273
275
|
onClick: handleClearPuzzleOrder,
|
|
274
276
|
className: theme.challenge.puzzle.clearButton,
|
|
275
|
-
children:
|
|
277
|
+
children: labels.clearOrder
|
|
276
278
|
})
|
|
277
279
|
]
|
|
278
280
|
})
|
|
@@ -283,7 +285,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
283
285
|
className: theme.loading.wrapper,
|
|
284
286
|
children: /*#__PURE__*/ _jsx("p", {
|
|
285
287
|
className: "text-sm text-zinc-500",
|
|
286
|
-
children:
|
|
288
|
+
children: labels.clickRefresh
|
|
287
289
|
})
|
|
288
290
|
})
|
|
289
291
|
}),
|
|
@@ -293,7 +295,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
293
295
|
/*#__PURE__*/ _jsx("label", {
|
|
294
296
|
htmlFor: "captcha-answer",
|
|
295
297
|
className: "sr-only",
|
|
296
|
-
children:
|
|
298
|
+
children: heading
|
|
297
299
|
}),
|
|
298
300
|
/*#__PURE__*/ _jsx("input", {
|
|
299
301
|
id: "captcha-answer",
|
|
@@ -320,13 +322,13 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
320
322
|
iconRef: iconRef
|
|
321
323
|
}),
|
|
322
324
|
/*#__PURE__*/ _jsx("span", {
|
|
323
|
-
children:
|
|
325
|
+
children: labels.newChallenge
|
|
324
326
|
})
|
|
325
327
|
]
|
|
326
328
|
}),
|
|
327
329
|
timeRemaining !== null && /*#__PURE__*/ _jsx("span", {
|
|
328
330
|
className: isExpired ? theme.actions.timerExpired : theme.actions.timer,
|
|
329
|
-
children: isExpired ?
|
|
331
|
+
children: isExpired ? labels.expired : labels.expiresIn.replace('{time}', formatTime(timeRemaining))
|
|
330
332
|
})
|
|
331
333
|
]
|
|
332
334
|
}),
|
|
@@ -334,7 +336,7 @@ export function Captcha({ generateAction, config, onChallengeLoad, onAnswerChang
|
|
|
334
336
|
className: theme.error.container,
|
|
335
337
|
children: /*#__PURE__*/ _jsx("p", {
|
|
336
338
|
className: theme.error.text,
|
|
337
|
-
children: rateLimitError ? rateLimitError : isExpired ?
|
|
339
|
+
children: rateLimitError ? rateLimitError : isExpired ? labels.expiredMessage : errorMessage
|
|
338
340
|
})
|
|
339
341
|
})
|
|
340
342
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Captcha } from './components/Captcha';
|
|
2
|
+
export { type CaptchaLabels, DEFAULT_CAPTCHA_LABELS } from './labels';
|
|
2
3
|
export { captchaTheme } from './theme';
|
|
3
4
|
export type { CaptchaAction, CaptchaChallenge, CaptchaChallengeData, CaptchaConfig, CaptchaDifficulty, CaptchaProps, CaptchaState, CaptchaThemeOverride, CaptchaType, } from './types';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every word the security check says.
|
|
3
|
+
*
|
|
4
|
+
* This is the most-read component in the kit and the one nobody thought to
|
|
5
|
+
* translate: it sits on the sign-in form, so on an installation of 879 people
|
|
6
|
+
* it is shown several hundred times a morning. A portal could pass `label` and
|
|
7
|
+
* change the heading — and the box under the heading still said "Enter the
|
|
8
|
+
* result", "New challenge", "Expires in 1:30" and, when somebody was slow,
|
|
9
|
+
* "Captcha expired. Please refresh." Four sentences of English on the first
|
|
10
|
+
* screen of a portal that is otherwise entirely in its own language.
|
|
11
|
+
*
|
|
12
|
+
* The placeholders are per challenge type because the instruction is different
|
|
13
|
+
* for each: a sum wants its result, an image wants the letters in it, and the
|
|
14
|
+
* puzzle wants an order. `expiresIn` takes the formatted clock as `{time}` so a
|
|
15
|
+
* translation can put it where its own grammar wants it.
|
|
16
|
+
*
|
|
17
|
+
* Defaults stay English, so an installation that passes nothing is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export type CaptchaLabels = {
|
|
20
|
+
/** The heading over the box, and the input's screen-reader label. */
|
|
21
|
+
heading: string;
|
|
22
|
+
/** Before a challenge has been asked for. */
|
|
23
|
+
clickRefresh: string;
|
|
24
|
+
/** The answer box, by challenge type. */
|
|
25
|
+
enterAnswer: string;
|
|
26
|
+
enterResult: string;
|
|
27
|
+
enterTextShown: string;
|
|
28
|
+
enterPieceOrder: string;
|
|
29
|
+
/** The picture challenge's alternative text. */
|
|
30
|
+
imageAlt: string;
|
|
31
|
+
/** The puzzle: its instruction takes the highest piece number as `{last}`. */
|
|
32
|
+
puzzleInstruction: string;
|
|
33
|
+
clearOrder: string;
|
|
34
|
+
/** Asking for a different challenge. */
|
|
35
|
+
newChallenge: string;
|
|
36
|
+
/** The countdown. `expiresIn` takes the clock as `{time}`. */
|
|
37
|
+
expiresIn: string;
|
|
38
|
+
expired: string;
|
|
39
|
+
/** What the box says once the clock has run out. */
|
|
40
|
+
expiredMessage: string;
|
|
41
|
+
/** Asked for too many challenges too quickly. */
|
|
42
|
+
tooManyRequests: string;
|
|
43
|
+
};
|
|
44
|
+
export declare const DEFAULT_CAPTCHA_LABELS: CaptchaLabels;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every word the security check says.
|
|
3
|
+
*
|
|
4
|
+
* This is the most-read component in the kit and the one nobody thought to
|
|
5
|
+
* translate: it sits on the sign-in form, so on an installation of 879 people
|
|
6
|
+
* it is shown several hundred times a morning. A portal could pass `label` and
|
|
7
|
+
* change the heading — and the box under the heading still said "Enter the
|
|
8
|
+
* result", "New challenge", "Expires in 1:30" and, when somebody was slow,
|
|
9
|
+
* "Captcha expired. Please refresh." Four sentences of English on the first
|
|
10
|
+
* screen of a portal that is otherwise entirely in its own language.
|
|
11
|
+
*
|
|
12
|
+
* The placeholders are per challenge type because the instruction is different
|
|
13
|
+
* for each: a sum wants its result, an image wants the letters in it, and the
|
|
14
|
+
* puzzle wants an order. `expiresIn` takes the formatted clock as `{time}` so a
|
|
15
|
+
* translation can put it where its own grammar wants it.
|
|
16
|
+
*
|
|
17
|
+
* Defaults stay English, so an installation that passes nothing is unchanged.
|
|
18
|
+
*/ export const DEFAULT_CAPTCHA_LABELS = {
|
|
19
|
+
heading: 'Security Check',
|
|
20
|
+
clickRefresh: 'Click refresh to load captcha',
|
|
21
|
+
enterAnswer: 'Enter answer',
|
|
22
|
+
enterResult: 'Enter the result',
|
|
23
|
+
enterTextShown: 'Enter the text shown',
|
|
24
|
+
enterPieceOrder: 'Enter piece order (e.g., 0,1,2,3)',
|
|
25
|
+
imageAlt: 'Captcha',
|
|
26
|
+
puzzleInstruction: 'Click the pieces in the correct order (0 to {last})',
|
|
27
|
+
clearOrder: 'Clear',
|
|
28
|
+
newChallenge: 'New challenge',
|
|
29
|
+
expiresIn: 'Expires in {time}',
|
|
30
|
+
expired: 'Expired',
|
|
31
|
+
expiredMessage: 'Captcha expired. Please refresh.',
|
|
32
|
+
tooManyRequests: 'Too many requests. Please try again later.'
|
|
33
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CaptchaLabels } from './labels';
|
|
1
2
|
export type CaptchaType = 'math' | 'image' | 'puzzle' | 'text';
|
|
2
3
|
export type CaptchaDifficulty = 'easy' | 'medium' | 'hard';
|
|
3
4
|
export interface CaptchaChallengeData {
|
|
@@ -110,7 +111,10 @@ export interface CaptchaProps {
|
|
|
110
111
|
disabled?: boolean;
|
|
111
112
|
autoGenerate?: boolean;
|
|
112
113
|
className?: string;
|
|
114
|
+
/** The heading only. Kept because installs already pass it; `labels` covers the rest. */
|
|
113
115
|
label?: string;
|
|
116
|
+
/** Every other word the box says — see `../labels`. */
|
|
117
|
+
labels?: Partial<CaptchaLabels>;
|
|
114
118
|
errorMessage?: string;
|
|
115
119
|
themeOverride?: CaptchaThemeOverride;
|
|
116
120
|
}
|
|
@@ -130,6 +130,7 @@ export function ChatPanel(props) {
|
|
|
130
130
|
currentUserId: props.currentUserId,
|
|
131
131
|
theme: theme,
|
|
132
132
|
isCreating: props.actions.createConversation.state.isPending,
|
|
133
|
+
labels: props.labels,
|
|
133
134
|
onClose: ()=>setPickerOpen(false),
|
|
134
135
|
onStartDirect: handleStartDirect
|
|
135
136
|
}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactElement } from 'react';
|
|
2
|
+
import { type ChatPanelLabels } from '../labels';
|
|
2
3
|
import type { ChatPanelTheme } from '../theme';
|
|
3
4
|
import type { ChatDirectoryUser } from '../types';
|
|
4
5
|
type NewConversationModalProps = {
|
|
@@ -7,11 +8,9 @@ type NewConversationModalProps = {
|
|
|
7
8
|
currentUserId: string;
|
|
8
9
|
theme: ChatPanelTheme;
|
|
9
10
|
isCreating: boolean;
|
|
10
|
-
|
|
11
|
-
searchPlaceholder?: string;
|
|
12
|
-
emptyLabel?: string;
|
|
11
|
+
labels?: Partial<ChatPanelLabels>;
|
|
13
12
|
onClose: () => void;
|
|
14
13
|
onStartDirect: (userId: string) => void;
|
|
15
14
|
};
|
|
16
|
-
export declare function NewConversationModal({ open, directory, currentUserId, theme, isCreating,
|
|
15
|
+
export declare function NewConversationModal({ open, directory, currentUserId, theme, isCreating, labels: labelOverrides, onClose, onStartDirect, }: NewConversationModalProps): ReactElement | null;
|
|
17
16
|
export {};
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { cn } from '../../../utils/cn';
|
|
5
|
+
import { matchesQuery } from '../../../utils/searchFold';
|
|
5
6
|
import { initialsOf } from '../helpers';
|
|
6
|
-
|
|
7
|
+
import { DEFAULT_CHAT_LABELS } from '../labels';
|
|
8
|
+
export function NewConversationModal({ open, directory, currentUserId, theme, isCreating, labels: labelOverrides, onClose, onStartDirect }) {
|
|
7
9
|
const [query, setQuery] = useState('');
|
|
10
|
+
const labels = {
|
|
11
|
+
...DEFAULT_CHAT_LABELS,
|
|
12
|
+
...labelOverrides
|
|
13
|
+
};
|
|
8
14
|
if (!open) return null;
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
// Folded, not lower-cased. A directory exported from a personnel system
|
|
16
|
+
// arrives in capitals, and `İSMAİL`.toLowerCase() is an `i` followed by a
|
|
17
|
+
// combining dot — so typing `ismail` used to find nobody at all.
|
|
18
|
+
const people = directory.filter((person)=>person.userId !== currentUserId).filter((person)=>matchesQuery(query, [
|
|
19
|
+
person.name,
|
|
20
|
+
person.subtitle
|
|
21
|
+
]));
|
|
11
22
|
return /*#__PURE__*/ _jsxs("div", {
|
|
12
23
|
className: theme.picker.overlay,
|
|
13
24
|
children: [
|
|
14
25
|
/*#__PURE__*/ _jsx("button", {
|
|
15
26
|
type: "button",
|
|
16
|
-
"aria-label":
|
|
27
|
+
"aria-label": labels.close,
|
|
17
28
|
className: "absolute inset-0 h-full w-full cursor-default",
|
|
18
29
|
onClick: onClose
|
|
19
30
|
}),
|
|
@@ -21,20 +32,20 @@ export function NewConversationModal({ open, directory, currentUserId, theme, is
|
|
|
21
32
|
className: cn('relative', theme.picker.panel),
|
|
22
33
|
role: "dialog",
|
|
23
34
|
"aria-modal": "true",
|
|
24
|
-
"aria-label":
|
|
35
|
+
"aria-label": labels.newMessageTitle,
|
|
25
36
|
children: [
|
|
26
37
|
/*#__PURE__*/ _jsxs("div", {
|
|
27
38
|
className: theme.picker.header,
|
|
28
39
|
children: [
|
|
29
40
|
/*#__PURE__*/ _jsx("span", {
|
|
30
41
|
className: theme.picker.title,
|
|
31
|
-
children:
|
|
42
|
+
children: labels.newMessageTitle
|
|
32
43
|
}),
|
|
33
44
|
/*#__PURE__*/ _jsx("button", {
|
|
34
45
|
type: "button",
|
|
35
46
|
className: theme.picker.closeButton,
|
|
36
47
|
onClick: onClose,
|
|
37
|
-
"aria-label":
|
|
48
|
+
"aria-label": labels.close,
|
|
38
49
|
children: /*#__PURE__*/ _jsxs("svg", {
|
|
39
50
|
className: "h-4 w-4",
|
|
40
51
|
fill: "none",
|
|
@@ -42,7 +53,7 @@ export function NewConversationModal({ open, directory, currentUserId, theme, is
|
|
|
42
53
|
stroke: "currentColor",
|
|
43
54
|
children: [
|
|
44
55
|
/*#__PURE__*/ _jsx("title", {
|
|
45
|
-
children:
|
|
56
|
+
children: labels.close
|
|
46
57
|
}),
|
|
47
58
|
/*#__PURE__*/ _jsx("path", {
|
|
48
59
|
strokeLinecap: "round",
|
|
@@ -60,7 +71,7 @@ export function NewConversationModal({ open, directory, currentUserId, theme, is
|
|
|
60
71
|
children: /*#__PURE__*/ _jsx("input", {
|
|
61
72
|
type: "text",
|
|
62
73
|
className: theme.picker.search,
|
|
63
|
-
placeholder:
|
|
74
|
+
placeholder: labels.searchPeople,
|
|
64
75
|
value: query,
|
|
65
76
|
onChange: (event)=>setQuery(event.target.value)
|
|
66
77
|
})
|
|
@@ -69,7 +80,7 @@ export function NewConversationModal({ open, directory, currentUserId, theme, is
|
|
|
69
80
|
className: theme.picker.list,
|
|
70
81
|
children: people.length === 0 ? /*#__PURE__*/ _jsx("div", {
|
|
71
82
|
className: theme.picker.empty,
|
|
72
|
-
children:
|
|
83
|
+
children: labels.noPeopleFound
|
|
73
84
|
}) : people.map((person)=>/*#__PURE__*/ _jsxs("button", {
|
|
74
85
|
type: "button",
|
|
75
86
|
className: theme.picker.item,
|
|
@@ -28,5 +28,13 @@ export type ChatPanelLabels = {
|
|
|
28
28
|
removeFile: string;
|
|
29
29
|
newConversation: string;
|
|
30
30
|
close: string;
|
|
31
|
+
/**
|
|
32
|
+
* The people-picker that opens from `newConversation`. Its three strings were
|
|
33
|
+
* props on the modal and on nothing else, so a portal could translate the
|
|
34
|
+
* button that opens it and not a word inside it.
|
|
35
|
+
*/
|
|
36
|
+
newMessageTitle: string;
|
|
37
|
+
searchPeople: string;
|
|
38
|
+
noPeopleFound: string;
|
|
31
39
|
};
|
|
32
40
|
export declare const DEFAULT_CHAT_LABELS: ChatPanelLabels;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useGSAP } from '@gsap/react';
|
|
4
4
|
import gsap from 'gsap';
|
|
5
5
|
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { matchesQuery } from '../../../utils/searchFold';
|
|
6
7
|
import { selectBoxTheme } from '../theme';
|
|
7
8
|
import { cn } from '../utils/cn';
|
|
8
9
|
import { SelectDropdown } from './SelectDropdown';
|
|
@@ -23,7 +24,13 @@ export function SelectBox({ options, value: controlledValue, defaultValue, place
|
|
|
23
24
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
24
25
|
const isControlled = controlledValue !== undefined;
|
|
25
26
|
const selectedOption = options.find((opt)=>opt.value === value);
|
|
26
|
-
|
|
27
|
+
// Folded rather than lower-cased: the labels in these dropdowns are whatever
|
|
28
|
+
// language the install runs in, and a plain `toLowerCase()` cannot match
|
|
29
|
+
// `Şube` to `sube` or `İzmir` to `izmir` — which is every second option in a
|
|
30
|
+
// Turkish portal.
|
|
31
|
+
const filteredOptions = searchable && searchQuery ? options.filter((opt)=>matchesQuery(searchQuery, [
|
|
32
|
+
opt.label
|
|
33
|
+
])) : options;
|
|
27
34
|
const calculatePosition = ()=>{
|
|
28
35
|
if (position !== 'auto') {
|
|
29
36
|
setDropdownPosition(position);
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { cn } from '../../../utils/cn';
|
|
6
|
+
import { matchesQuery } from '../../../utils/searchFold';
|
|
6
7
|
export function RoleAssignmentModal({ isOpen, roles, userRoles, userId, onClose, onAssignRole, onRemoveRole }) {
|
|
7
8
|
const modalRef = useRef(null);
|
|
8
9
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -37,7 +38,11 @@ export function RoleAssignmentModal({ isOpen, roles, userRoles, userId, onClose,
|
|
|
37
38
|
]);
|
|
38
39
|
if (!isOpen || !mounted) return null;
|
|
39
40
|
const assignedRoleIds = userRoles.filter((ur)=>ur.userId === userId).map((ur)=>ur.roleId);
|
|
40
|
-
|
|
41
|
+
// Role names are written in the portal's own language, so the query is
|
|
42
|
+
// folded rather than lower-cased.
|
|
43
|
+
const filteredRoles = roles.filter((role)=>matchesQuery(searchQuery, [
|
|
44
|
+
role.name
|
|
45
|
+
]));
|
|
41
46
|
const handleToggleRole = (role)=>{
|
|
42
47
|
const isAssigned = assignedRoleIds.includes(role.id);
|
|
43
48
|
if (isAssigned) {
|
|
@@ -4,6 +4,7 @@ import { useGSAP } from '@gsap/react';
|
|
|
4
4
|
import gsap from 'gsap';
|
|
5
5
|
import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../../utils/cn';
|
|
7
|
+
import { matchesQuery } from '../../../utils/searchFold';
|
|
7
8
|
import { Button } from '../../Button';
|
|
8
9
|
import { useUsersStore } from '../store';
|
|
9
10
|
import { usersPageTheme } from '../theme';
|
|
@@ -353,8 +354,13 @@ export function UsersPage({ title = 'User Management', subtitle, className, them
|
|
|
353
354
|
};
|
|
354
355
|
const filteredUsers = store.users.filter((user)=>{
|
|
355
356
|
const profile = store.profiles.find((p)=>p.userId === user.id);
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
// People's names, so the comparison is folded: a roster written `İSMAİL`
|
|
358
|
+
// has to answer to `ismail`, which lower-casing alone never manages.
|
|
359
|
+
const matchesSearch = matchesQuery(store.searchQuery, [
|
|
360
|
+
user.email,
|
|
361
|
+
profile?.firstName,
|
|
362
|
+
profile?.lastName
|
|
363
|
+
]);
|
|
358
364
|
const matchesStatus = store.statusFilter === 'all' || store.statusFilter === 'locked' && user.isLocked || store.statusFilter === 'active' && !user.isLocked && user.verifiedAt || store.statusFilter === 'unverified' && !user.verifiedAt;
|
|
359
365
|
const matchesRole = store.roleFilter === 'all' || store.userRoles.some((ur)=>ur.userId === user.id && ur.roleId === store.roleFilter);
|
|
360
366
|
return matchesSearch && matchesStatus && matchesRole;
|
package/dist/fe/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { LoginChecker, RoleChecker, useAuthGuardStore, usePermission } from './c
|
|
|
4
4
|
export type { AuthorizationPageProps, AuthorizationPageTheme, ClaimListProps, RoleClaimEditorProps, RoleListProps, } from './components/AuthorizationPage';
|
|
5
5
|
export { AuthorizationPage, authorizationPageTheme, ClaimList, extendAuthorizationPageTheme, RoleClaimEditor, RoleList, useAuthorizationStore, } from './components/AuthorizationPage';
|
|
6
6
|
export { Button } from './components/Button';
|
|
7
|
-
export type { CaptchaAction, CaptchaChallenge, CaptchaConfig, CaptchaDifficulty, CaptchaProps, CaptchaState, CaptchaThemeOverride, CaptchaType, } from './components/Captcha';
|
|
8
|
-
export { Captcha, captchaTheme } from './components/Captcha';
|
|
7
|
+
export type { CaptchaAction, CaptchaChallenge, CaptchaConfig, CaptchaDifficulty, CaptchaLabels, CaptchaProps, CaptchaState, CaptchaThemeOverride, CaptchaType, } from './components/Captcha';
|
|
8
|
+
export { Captcha, captchaTheme, DEFAULT_CAPTCHA_LABELS } from './components/Captcha';
|
|
9
9
|
export type { ChangePasswordAction, ChangePasswordFormProps, ChangePasswordHeaderProps, ChangePasswordPageConfig, ChangePasswordStep, } from './components/ChangePasswordPage';
|
|
10
10
|
export { ChangePasswordPage, useChangePasswordStore, } from './components/ChangePasswordPage';
|
|
11
11
|
export type { ChatDirectoryUser, ChatPanelActions, ChatPanelLabels, ChatPanelProps, ChatPanelState, ChatPanelTheme, TypingIndicator as ChatTypingIndicator, UseChatReturn, } from './components/ChatPanel';
|
|
@@ -60,5 +60,6 @@ export type { PubSubConfig, PubSubConnectionState, PubSubEvent } from './hooks/u
|
|
|
60
60
|
export { usePubSub, usePubSubStore } from './hooks/usePubSub';
|
|
61
61
|
export type { BulkDeleteResponse, ColumnConfig, ColumnEnum, ColumnReference, ColumnType, ColumnValidation, DeleteResponse, FieldConfig, FilterCondition, ListResponse, MutationResponse, NucleusColumn, NucleusEntity, NucleusEntityShowcaseProps, PaginationMeta, QueryParams, SingleResponse, SortCondition, SortDirection, UseNucleusEntityOptions, UseNucleusEntityReturn, } from './types';
|
|
62
62
|
export { cn } from './utils/cn';
|
|
63
|
+
export { foldForSearch, matchesQuery } from './utils/searchFold';
|
|
63
64
|
export { formatLabel, getColumnType, getDefaultValue, isArrayColumn, isJsonColumn, isReferenceColumn, shouldExcludeColumn, shouldExcludeFromForm, } from './utils/columnUtils';
|
|
64
65
|
export { generateBulkEndpointKey, generateDistinctEndpointKey, generateEntityEndpointKey, generateGetByIdEndpointKey, singularize, toUpperSnakeCase, } from './utils/endpointKeys';
|
package/dist/fe/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { AbstractAnimatedBackground } from './components/AbstractAnimatedBackgro
|
|
|
4
4
|
export { LoginChecker, RoleChecker, useAuthGuardStore, usePermission } from './components/AuthGuard';
|
|
5
5
|
export { AuthorizationPage, authorizationPageTheme, ClaimList, extendAuthorizationPageTheme, RoleClaimEditor, RoleList, useAuthorizationStore } from './components/AuthorizationPage';
|
|
6
6
|
export { Button } from './components/Button';
|
|
7
|
-
export { Captcha, captchaTheme } from './components/Captcha';
|
|
7
|
+
export { Captcha, captchaTheme, DEFAULT_CAPTCHA_LABELS } from './components/Captcha';
|
|
8
8
|
export { ChangePasswordPage, useChangePasswordStore } from './components/ChangePasswordPage';
|
|
9
9
|
// Chat
|
|
10
10
|
export { ChatPanel, chatPanelTheme, DEFAULT_CHAT_LABELS, extendChatPanelTheme, NewConversationModal, useChat, useChatStore } from './components/ChatPanel';
|
|
@@ -36,5 +36,9 @@ export { useVerifyEmailStore, VerifyEmailPage } from './components/VerifyEmailPa
|
|
|
36
36
|
export { useNucleusEntity } from './hooks/useNucleusEntity';
|
|
37
37
|
export { usePubSub, usePubSubStore } from './hooks/usePubSub';
|
|
38
38
|
export { cn } from './utils/cn';
|
|
39
|
+
// Exported because every portal that lists people needs it and none of them
|
|
40
|
+
// should write it twice: matching a typed query to a name is not a lower-case
|
|
41
|
+
// comparison in any alphabet with marks in it.
|
|
42
|
+
export { foldForSearch, matchesQuery } from './utils/searchFold';
|
|
39
43
|
export { formatLabel, getColumnType, getDefaultValue, isArrayColumn, isJsonColumn, isReferenceColumn, shouldExcludeColumn, shouldExcludeFromForm } from './utils/columnUtils';
|
|
40
44
|
export { generateBulkEndpointKey, generateDistinctEndpointKey, generateEntityEndpointKey, generateGetByIdEndpointKey, singularize, toUpperSnakeCase } from './utils/endpointKeys';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folding a string down to what a person typing into a search box meant.
|
|
3
|
+
*
|
|
4
|
+
* A search box that compares `toLowerCase()` to `toLowerCase()` only works for
|
|
5
|
+
* the alphabet it was written in. Everywhere else it fails in two directions at
|
|
6
|
+
* once, and the dotted i is the clearest case: `İSMAİL`.toLowerCase() is not
|
|
7
|
+
* `ismail` but `i̇smai̇l` — an `i` followed by a combining dot, which is a
|
|
8
|
+
* character nobody's keyboard produces. Type `ismail` into a directory of eight
|
|
9
|
+
* hundred people exported in capitals and it finds none of them. The same box
|
|
10
|
+
* cannot find `Şafak` from `safak`, or `Café` from `cafe`, which is how people
|
|
11
|
+
* search when the marks are a keystroke away rather than a key.
|
|
12
|
+
*
|
|
13
|
+
* The fold is three passes and no per-language table:
|
|
14
|
+
*
|
|
15
|
+
* 1. Lower-case with the INVARIANT rules. Turkish's own casing would send `I`
|
|
16
|
+
* to `ı`, which is right for writing and wrong here — a search box wants
|
|
17
|
+
* `I`, `İ`, `ı` and `i` to end up as the same letter, and they do once the
|
|
18
|
+
* marks come off.
|
|
19
|
+
* 2. Decompose (NFD) and drop the combining marks. This is what turns the
|
|
20
|
+
* stray dot above into nothing, and `é`, `ş`, `ğ`, `ü`, `ö`, `ç` into their
|
|
21
|
+
* plain letters, for every script that composes that way.
|
|
22
|
+
* 3. Map the few letters that are their own character rather than a letter
|
|
23
|
+
* plus a mark, so NFD has nothing to strip: the dotless `ı`, and `ø`, `ł`,
|
|
24
|
+
* `đ`, `ß`, `æ`, `œ`.
|
|
25
|
+
*
|
|
26
|
+
* Language-neutral by construction, which is the point: the kit ships to
|
|
27
|
+
* whoever installs it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function foldForSearch(value: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* True when every word typed appears somewhere in the given fields.
|
|
32
|
+
*
|
|
33
|
+
* Word by word rather than as one string, because the reader should not have to
|
|
34
|
+
* guess the order a row prints its parts in: `yalgi genel` finds the general
|
|
35
|
+
* manager whether the list renders the name before the post or after it.
|
|
36
|
+
*
|
|
37
|
+
* An empty query matches everything, so a caller can hand the raw input over
|
|
38
|
+
* without checking it first.
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchesQuery(query: string, fields: readonly (string | null | undefined)[]): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folding a string down to what a person typing into a search box meant.
|
|
3
|
+
*
|
|
4
|
+
* A search box that compares `toLowerCase()` to `toLowerCase()` only works for
|
|
5
|
+
* the alphabet it was written in. Everywhere else it fails in two directions at
|
|
6
|
+
* once, and the dotted i is the clearest case: `İSMAİL`.toLowerCase() is not
|
|
7
|
+
* `ismail` but `i̇smai̇l` — an `i` followed by a combining dot, which is a
|
|
8
|
+
* character nobody's keyboard produces. Type `ismail` into a directory of eight
|
|
9
|
+
* hundred people exported in capitals and it finds none of them. The same box
|
|
10
|
+
* cannot find `Şafak` from `safak`, or `Café` from `cafe`, which is how people
|
|
11
|
+
* search when the marks are a keystroke away rather than a key.
|
|
12
|
+
*
|
|
13
|
+
* The fold is three passes and no per-language table:
|
|
14
|
+
*
|
|
15
|
+
* 1. Lower-case with the INVARIANT rules. Turkish's own casing would send `I`
|
|
16
|
+
* to `ı`, which is right for writing and wrong here — a search box wants
|
|
17
|
+
* `I`, `İ`, `ı` and `i` to end up as the same letter, and they do once the
|
|
18
|
+
* marks come off.
|
|
19
|
+
* 2. Decompose (NFD) and drop the combining marks. This is what turns the
|
|
20
|
+
* stray dot above into nothing, and `é`, `ş`, `ğ`, `ü`, `ö`, `ç` into their
|
|
21
|
+
* plain letters, for every script that composes that way.
|
|
22
|
+
* 3. Map the few letters that are their own character rather than a letter
|
|
23
|
+
* plus a mark, so NFD has nothing to strip: the dotless `ı`, and `ø`, `ł`,
|
|
24
|
+
* `đ`, `ß`, `æ`, `œ`.
|
|
25
|
+
*
|
|
26
|
+
* Language-neutral by construction, which is the point: the kit ships to
|
|
27
|
+
* whoever installs it.
|
|
28
|
+
*/ /** Letters with no decomposition, so step 2 cannot reach them. */ const STANDALONE = {
|
|
29
|
+
ı: 'i',
|
|
30
|
+
ø: 'o',
|
|
31
|
+
ł: 'l',
|
|
32
|
+
đ: 'd',
|
|
33
|
+
ð: 'd',
|
|
34
|
+
þ: 'th',
|
|
35
|
+
ß: 'ss',
|
|
36
|
+
æ: 'ae',
|
|
37
|
+
œ: 'oe'
|
|
38
|
+
};
|
|
39
|
+
const COMBINING_MARKS = /\p{M}/gu;
|
|
40
|
+
export function foldForSearch(value) {
|
|
41
|
+
const lowered = value.toLowerCase().normalize('NFD').replace(COMBINING_MARKS, '');
|
|
42
|
+
let out = '';
|
|
43
|
+
for (const char of lowered)out += STANDALONE[char] ?? char;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when every word typed appears somewhere in the given fields.
|
|
48
|
+
*
|
|
49
|
+
* Word by word rather than as one string, because the reader should not have to
|
|
50
|
+
* guess the order a row prints its parts in: `yalgi genel` finds the general
|
|
51
|
+
* manager whether the list renders the name before the post or after it.
|
|
52
|
+
*
|
|
53
|
+
* An empty query matches everything, so a caller can hand the raw input over
|
|
54
|
+
* without checking it first.
|
|
55
|
+
*/ export function matchesQuery(query, fields) {
|
|
56
|
+
const words = foldForSearch(query).split(/\s+/).filter(Boolean);
|
|
57
|
+
if (words.length === 0) return true;
|
|
58
|
+
const haystack = foldForSearch(fields.filter(Boolean).join(' '));
|
|
59
|
+
return words.every((word)=>haystack.includes(word));
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.886",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|