nucleus-core-ts 0.9.883 → 0.9.885
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/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/ForgotPasswordPage/components/ForgotPasswordForm.js +2 -2
- package/dist/fe/components/ForgotPasswordPage/labels.d.ts +4 -0
- package/dist/fe/components/ForgotPasswordPage/labels.js +3 -1
- package/dist/fe/components/ResetPasswordPage/components/ResetPasswordForm.js +7 -7
- package/dist/fe/components/ResetPasswordPage/labels.d.ts +9 -0
- package/dist/fe/components/ResetPasswordPage/labels.js +8 -1
- package/dist/fe/components/SelectBox/components/SelectBox.js +8 -1
- package/dist/fe/components/SetPasswordPage/components/SetPasswordForm.js +6 -6
- package/dist/fe/components/SetPasswordPage/labels.d.ts +7 -0
- package/dist/fe/components/SetPasswordPage/labels.js +7 -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 +1 -0
- package/dist/fe/index.js +4 -0
- 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.885
|
|
@@ -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;
|
|
@@ -56,7 +56,7 @@ export function ForgotPasswordForm({ labels, forgotPasswordAction, onBackToLogin
|
|
|
56
56
|
const handleSubmit = contextSafe((e)=>{
|
|
57
57
|
e.preventDefault();
|
|
58
58
|
if (!store.email) {
|
|
59
|
-
store.setError(
|
|
59
|
+
store.setError(say.emailRequired);
|
|
60
60
|
shakeForm();
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -197,7 +197,7 @@ export function ForgotPasswordForm({ labels, forgotPasswordAction, onBackToLogin
|
|
|
197
197
|
fullWidth: true,
|
|
198
198
|
loading: isLoading,
|
|
199
199
|
disabled: isLoading,
|
|
200
|
-
children:
|
|
200
|
+
children: say.submit
|
|
201
201
|
})
|
|
202
202
|
}),
|
|
203
203
|
/*#__PURE__*/ _jsx("div", {
|
|
@@ -15,5 +15,9 @@ export type ForgotPasswordLabels = {
|
|
|
15
15
|
sentTo: string;
|
|
16
16
|
backToLogin: string;
|
|
17
17
|
pageLabel: string;
|
|
18
|
+
/** The button that sends the link. */
|
|
19
|
+
submit: string;
|
|
20
|
+
/** Said when the box is empty. */
|
|
21
|
+
emailRequired: string;
|
|
18
22
|
};
|
|
19
23
|
export declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordLabels;
|
|
@@ -12,5 +12,7 @@
|
|
|
12
12
|
checkYourEmail: 'Check your email',
|
|
13
13
|
sentTo: "We've sent a password reset link to {email}. Please check your inbox and follow the instructions.",
|
|
14
14
|
backToLogin: 'Back to login',
|
|
15
|
-
pageLabel: 'Forgot password page'
|
|
15
|
+
pageLabel: 'Forgot password page',
|
|
16
|
+
submit: 'Send Reset Link',
|
|
17
|
+
emailRequired: 'Please enter your email address'
|
|
16
18
|
};
|
|
@@ -95,28 +95,28 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
|
|
|
95
95
|
if (policy.requireUppercase) {
|
|
96
96
|
reqs.push({
|
|
97
97
|
key: 'uppercase',
|
|
98
|
-
label:
|
|
98
|
+
label: say.ruleUppercase,
|
|
99
99
|
test: (p)=>/[A-Z]/.test(p)
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
if (policy.requireLowercase) {
|
|
103
103
|
reqs.push({
|
|
104
104
|
key: 'lowercase',
|
|
105
|
-
label:
|
|
105
|
+
label: say.ruleLowercase,
|
|
106
106
|
test: (p)=>/[a-z]/.test(p)
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
if (policy.requireNumber) {
|
|
110
110
|
reqs.push({
|
|
111
111
|
key: 'number',
|
|
112
|
-
label:
|
|
112
|
+
label: say.ruleNumber,
|
|
113
113
|
test: (p)=>/\d/.test(p)
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
if (policy.requireSpecialChar) {
|
|
117
117
|
reqs.push({
|
|
118
118
|
key: 'special',
|
|
119
|
-
label:
|
|
119
|
+
label: say.ruleSpecial,
|
|
120
120
|
test: (p)=>{
|
|
121
121
|
const escapedChars = policy.specialChars.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
122
122
|
return new RegExp(`[${escapedChars}]`).test(p);
|
|
@@ -139,18 +139,18 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
|
|
|
139
139
|
const handleSubmit = contextSafe((e)=>{
|
|
140
140
|
e.preventDefault();
|
|
141
141
|
if (!store.newPassword || !store.confirmPassword) {
|
|
142
|
-
store.setError(
|
|
142
|
+
store.setError(say.fillAllFields);
|
|
143
143
|
shakeForm();
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
if (store.newPassword !== store.confirmPassword) {
|
|
147
|
-
store.setError(
|
|
147
|
+
store.setError(say.passwordsDoNotMatch);
|
|
148
148
|
shakeForm();
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
const allRequirementsMet = passwordRequirements.every((req)=>req.test(store.newPassword));
|
|
152
152
|
if (!allRequirementsMet) {
|
|
153
|
-
store.setError(
|
|
153
|
+
store.setError(say.requirementsNotMet);
|
|
154
154
|
shakeForm();
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
@@ -10,5 +10,14 @@ export type ResetPasswordLabels = {
|
|
|
10
10
|
success: string;
|
|
11
11
|
backToLogin: string;
|
|
12
12
|
pageLabel: string;
|
|
13
|
+
/** The rules listed under the box, one line each. */
|
|
14
|
+
ruleUppercase: string;
|
|
15
|
+
ruleLowercase: string;
|
|
16
|
+
ruleNumber: string;
|
|
17
|
+
ruleSpecial: string;
|
|
18
|
+
/** Said when something is wrong, at the moment it is wrong. */
|
|
19
|
+
fillAllFields: string;
|
|
20
|
+
passwordsDoNotMatch: string;
|
|
21
|
+
requirementsNotMet: string;
|
|
13
22
|
};
|
|
14
23
|
export declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordLabels;
|
|
@@ -8,5 +8,12 @@
|
|
|
8
8
|
passwordRequirements: 'Password requirements:',
|
|
9
9
|
success: 'Password Reset Successful',
|
|
10
10
|
backToLogin: 'Back to login',
|
|
11
|
-
pageLabel: 'Reset password page'
|
|
11
|
+
pageLabel: 'Reset password page',
|
|
12
|
+
ruleUppercase: 'One uppercase letter',
|
|
13
|
+
ruleLowercase: 'One lowercase letter',
|
|
14
|
+
ruleNumber: 'One number',
|
|
15
|
+
ruleSpecial: 'One special character',
|
|
16
|
+
fillAllFields: 'Please fill in all fields',
|
|
17
|
+
passwordsDoNotMatch: 'Passwords do not match',
|
|
18
|
+
requirementsNotMet: 'Password does not meet all requirements'
|
|
12
19
|
};
|
|
@@ -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);
|
|
@@ -19,7 +19,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
|
|
|
19
19
|
const activeAction = isInvite && passwordSetAction ? passwordSetAction : passwordChangeAction;
|
|
20
20
|
const validateForm = ()=>{
|
|
21
21
|
if (!store.newPassword) {
|
|
22
|
-
return
|
|
22
|
+
return say.passwordRequired;
|
|
23
23
|
}
|
|
24
24
|
if (store.newPassword.length < policy.minLength) {
|
|
25
25
|
return `Password must be at least ${policy.minLength} characters`;
|
|
@@ -34,7 +34,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
|
|
|
34
34
|
return 'Password must contain at least one lowercase letter';
|
|
35
35
|
}
|
|
36
36
|
if (policy.requireNumber && !/[0-9]/.test(store.newPassword)) {
|
|
37
|
-
return
|
|
37
|
+
return say.passwordNeedsNumber;
|
|
38
38
|
}
|
|
39
39
|
if (policy.requireSpecialChar) {
|
|
40
40
|
const specialCharsRegex = new RegExp(`[${policy.specialChars.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}]`);
|
|
@@ -43,10 +43,10 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
if (!store.confirmPassword) {
|
|
46
|
-
return
|
|
46
|
+
return say.confirmRequired;
|
|
47
47
|
}
|
|
48
48
|
if (store.newPassword !== store.confirmPassword) {
|
|
49
|
-
return
|
|
49
|
+
return say.passwordsDoNotMatch;
|
|
50
50
|
}
|
|
51
51
|
return null;
|
|
52
52
|
};
|
|
@@ -59,7 +59,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
|
|
|
59
59
|
}
|
|
60
60
|
store.setError(null);
|
|
61
61
|
if (!activeAction) {
|
|
62
|
-
store.setError(
|
|
62
|
+
store.setError(say.noActionConfigured);
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
const onAfterHandle = ()=>{
|
|
@@ -71,7 +71,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
|
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
const onErrorHandle = (error)=>{
|
|
74
|
-
store.setError(error?.message ||
|
|
74
|
+
store.setError(error?.message || say.failed);
|
|
75
75
|
};
|
|
76
76
|
if (isInvite && passwordSetAction && store.userId) {
|
|
77
77
|
passwordSetAction.start({
|
|
@@ -21,5 +21,12 @@ export type SetPasswordLabels = {
|
|
|
21
21
|
passwordStrength: string;
|
|
22
22
|
passwordRequirements: string;
|
|
23
23
|
pageLabel: string;
|
|
24
|
+
/** Said when something is wrong, at the moment it is wrong. */
|
|
25
|
+
passwordRequired: string;
|
|
26
|
+
passwordNeedsNumber: string;
|
|
27
|
+
confirmRequired: string;
|
|
28
|
+
passwordsDoNotMatch: string;
|
|
29
|
+
noActionConfigured: string;
|
|
30
|
+
failed: string;
|
|
24
31
|
};
|
|
25
32
|
export declare const DEFAULT_SET_PASSWORD_LABELS: SetPasswordLabels;
|
|
@@ -19,5 +19,11 @@
|
|
|
19
19
|
confirmPasswordPlaceholder: 'Confirm your new password',
|
|
20
20
|
passwordStrength: 'Password strength',
|
|
21
21
|
passwordRequirements: 'Password requirements',
|
|
22
|
-
pageLabel: 'Set password page'
|
|
22
|
+
pageLabel: 'Set password page',
|
|
23
|
+
passwordRequired: 'Password is required',
|
|
24
|
+
passwordNeedsNumber: 'Password must contain at least one number',
|
|
25
|
+
confirmRequired: 'Please confirm your password',
|
|
26
|
+
passwordsDoNotMatch: 'Passwords do not match',
|
|
27
|
+
noActionConfigured: 'No action configured',
|
|
28
|
+
failed: 'Failed to set password'
|
|
23
29
|
};
|
|
@@ -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
|
@@ -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
|
@@ -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.885",
|
|
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",
|