@akinon/akitable 1.6.2 → 1.7.0-260914-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/dist/cjs/common/navigation.d.ts +59 -0
- package/dist/cjs/common/navigation.d.ts.map +1 -0
- package/dist/cjs/common/navigation.js +112 -0
- package/dist/cjs/components/datatable.css +8 -0
- package/dist/cjs/components/datatable.d.ts.map +1 -1
- package/dist/cjs/components/datatable.js +176 -8
- package/dist/cjs/components/pagination.d.ts.map +1 -1
- package/dist/cjs/components/pagination.js +15 -2
- package/dist/cjs/constants.d.ts +3 -0
- package/dist/cjs/constants.d.ts.map +1 -0
- package/dist/cjs/constants.js +4 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/types.d.ts +99 -3
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/common/navigation.d.ts +59 -0
- package/dist/esm/common/navigation.d.ts.map +1 -0
- package/dist/esm/common/navigation.js +107 -0
- package/dist/esm/components/datatable.css +8 -0
- package/dist/esm/components/datatable.d.ts.map +1 -1
- package/dist/esm/components/datatable.js +176 -8
- package/dist/esm/components/pagination.d.ts.map +1 -1
- package/dist/esm/components/pagination.js +15 -2
- package/dist/esm/constants.d.ts +3 -0
- package/dist/esm/constants.d.ts.map +1 -0
- package/dist/esm/constants.js +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/types.d.ts +99 -3
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers behind this package's new-tab support.
|
|
3
|
+
*
|
|
4
|
+
* DUPLICATE — an identical copy lives in `@akinon/ui-breadcrumb` as
|
|
5
|
+
* `src/navigation.ts`. The two are kept apart on purpose: neither package
|
|
6
|
+
* should depend on the other, and a shared package would have to put them in
|
|
7
|
+
* its public API. `isSafeHref` is a security check, so any change here — a
|
|
8
|
+
* newly discovered bypass shape, a scheme added to the allow list — MUST be
|
|
9
|
+
* applied to the other copy as well.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Returns `true` when a mouse event expresses the user's intent to open the
|
|
13
|
+
* target in a new tab or window: middle click, `Cmd` + click (macOS) / `Ctrl` +
|
|
14
|
+
* click (Windows, Linux), or `Shift` + click.
|
|
15
|
+
*
|
|
16
|
+
* `Alt` + click is deliberately **not** included: it is a download gesture
|
|
17
|
+
* rather than a new-tab one, and it does nothing at all for in-app routes, so
|
|
18
|
+
* treating it as browser-handled would swallow the navigation entirely.
|
|
19
|
+
*
|
|
20
|
+
* Call sites must return early **without** calling `preventDefault()` when this
|
|
21
|
+
* returns `true`, so the browser's native anchor behavior takes over. This only
|
|
22
|
+
* works on elements that render as a real `<a href="...">`.
|
|
23
|
+
*
|
|
24
|
+
* The `button` check only matters for a call site that listens to
|
|
25
|
+
* `mousedown`/`auxclick`: a middle click dispatches `auxclick`, not `click`, so
|
|
26
|
+
* React's `onClick` never sees it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const isNewTabRequest: (event: Pick<MouseEvent, "button" | "metaKey" | "ctrlKey" | "shiftKey">) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Returns `true` when `href` is safe to render as a real `<a href="...">`
|
|
31
|
+
* attribute.
|
|
32
|
+
*
|
|
33
|
+
* Relative URLs (`/products/1`, `products/1`, `#/products/1`, `?page=2`) and
|
|
34
|
+
* the `http`, `https`, `mailto` and `tel` schemes are allowed. Any other scheme
|
|
35
|
+
* — `javascript:` above all — is rejected, since following such a link executes
|
|
36
|
+
* script in the page's own origin. Note that an internal routing key shaped
|
|
37
|
+
* like `catalog:detail` parses as the scheme `catalog:` and is rejected as
|
|
38
|
+
* well.
|
|
39
|
+
*
|
|
40
|
+
* This matters because navigation targets are typically derived from server
|
|
41
|
+
* data: an `href` built from a record field is untrusted input, and a real
|
|
42
|
+
* anchor is followed natively on middle click, `Cmd`/`Ctrl` + click and through
|
|
43
|
+
* the right-click context menu, where no click handler runs to intercept it.
|
|
44
|
+
*
|
|
45
|
+
* An empty or whitespace-only value is rejected as well: `<a href="">` points at
|
|
46
|
+
* the current document and reloads the page.
|
|
47
|
+
*
|
|
48
|
+
* Protocol-relative URLs (`//evil.com`) are rejected too. They carry no scheme,
|
|
49
|
+
* so they read as relative, but the browser resolves them against the current
|
|
50
|
+
* one and navigates off-origin — and no in-app route ever needs that shape.
|
|
51
|
+
* Absolute `http`/`https` URLs stay allowed on purpose: a cross-origin target
|
|
52
|
+
* can be legitimate, so the caller owns that decision — including whether it
|
|
53
|
+
* can carry `rel="noopener"`, which is a per-call-site matter.
|
|
54
|
+
*
|
|
55
|
+
* A rejected value is dropped silently: nothing is logged, and the element
|
|
56
|
+
* renders without an anchor.
|
|
57
|
+
*/
|
|
58
|
+
export declare const isSafeHref: (href: string | undefined | null) => boolean;
|
|
59
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../src/common/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC,KACrE,OAIa,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,GAAG,SAAS,GAAG,IAAI,KAAG,OAkB5D,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Navigation helpers behind this package's new-tab support.
|
|
4
|
+
*
|
|
5
|
+
* DUPLICATE — an identical copy lives in `@akinon/ui-breadcrumb` as
|
|
6
|
+
* `src/navigation.ts`. The two are kept apart on purpose: neither package
|
|
7
|
+
* should depend on the other, and a shared package would have to put them in
|
|
8
|
+
* its public API. `isSafeHref` is a security check, so any change here — a
|
|
9
|
+
* newly discovered bypass shape, a scheme added to the allow list — MUST be
|
|
10
|
+
* applied to the other copy as well.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.isSafeHref = exports.isNewTabRequest = void 0;
|
|
14
|
+
/** Mouse button number for the middle (wheel) button in `MouseEvent.button`. */
|
|
15
|
+
const MIDDLE_MOUSE_BUTTON = 1;
|
|
16
|
+
/**
|
|
17
|
+
* Control characters (tab and newline included) are stripped by browsers while
|
|
18
|
+
* parsing a URL, so `java\nscript:alert(1)` is a working `javascript:` URL and
|
|
19
|
+
* must not slip through the scheme check below.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately wider than the browser, which strips tab/CR/LF anywhere but
|
|
22
|
+
* whitespace only from the ends. Widening can never let an unsupported scheme
|
|
23
|
+
* through — the stripped characters cannot spell one out — so it errs in the
|
|
24
|
+
* safe direction. The range covers whitespace as well, so no separate `trim()`
|
|
25
|
+
* is needed.
|
|
26
|
+
*/
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
const CONTROL_CHARACTERS = /[\x00-\x20\x7f]/g;
|
|
29
|
+
/**
|
|
30
|
+
* Schemes that are safe to put into an `href` attribute. Everything else —
|
|
31
|
+
* most importantly `javascript:`, but also `data:` and `vbscript:` — can run
|
|
32
|
+
* script in the page's own origin when the link is followed.
|
|
33
|
+
*/
|
|
34
|
+
const SAFE_SCHEMES = ['http:', 'https:', 'mailto:', 'tel:'];
|
|
35
|
+
/** Matches a leading URL scheme, e.g. `https:` or `javascript:`. */
|
|
36
|
+
const SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/;
|
|
37
|
+
/**
|
|
38
|
+
* Matches the leading slashes of a protocol-relative URL. Backslashes are
|
|
39
|
+
* folded in because browsers treat `\` as `/` while parsing a URL, which makes
|
|
40
|
+
* `\\evil.com` and `/\evil.com` protocol-relative just like `//evil.com`.
|
|
41
|
+
*/
|
|
42
|
+
const PROTOCOL_RELATIVE_PATTERN = /^[/\\]{2,}/;
|
|
43
|
+
/**
|
|
44
|
+
* Returns `true` when a mouse event expresses the user's intent to open the
|
|
45
|
+
* target in a new tab or window: middle click, `Cmd` + click (macOS) / `Ctrl` +
|
|
46
|
+
* click (Windows, Linux), or `Shift` + click.
|
|
47
|
+
*
|
|
48
|
+
* `Alt` + click is deliberately **not** included: it is a download gesture
|
|
49
|
+
* rather than a new-tab one, and it does nothing at all for in-app routes, so
|
|
50
|
+
* treating it as browser-handled would swallow the navigation entirely.
|
|
51
|
+
*
|
|
52
|
+
* Call sites must return early **without** calling `preventDefault()` when this
|
|
53
|
+
* returns `true`, so the browser's native anchor behavior takes over. This only
|
|
54
|
+
* works on elements that render as a real `<a href="...">`.
|
|
55
|
+
*
|
|
56
|
+
* The `button` check only matters for a call site that listens to
|
|
57
|
+
* `mousedown`/`auxclick`: a middle click dispatches `auxclick`, not `click`, so
|
|
58
|
+
* React's `onClick` never sees it.
|
|
59
|
+
*/
|
|
60
|
+
const isNewTabRequest = (event) => event.button === MIDDLE_MOUSE_BUTTON ||
|
|
61
|
+
event.metaKey ||
|
|
62
|
+
event.ctrlKey ||
|
|
63
|
+
event.shiftKey;
|
|
64
|
+
exports.isNewTabRequest = isNewTabRequest;
|
|
65
|
+
/**
|
|
66
|
+
* Returns `true` when `href` is safe to render as a real `<a href="...">`
|
|
67
|
+
* attribute.
|
|
68
|
+
*
|
|
69
|
+
* Relative URLs (`/products/1`, `products/1`, `#/products/1`, `?page=2`) and
|
|
70
|
+
* the `http`, `https`, `mailto` and `tel` schemes are allowed. Any other scheme
|
|
71
|
+
* — `javascript:` above all — is rejected, since following such a link executes
|
|
72
|
+
* script in the page's own origin. Note that an internal routing key shaped
|
|
73
|
+
* like `catalog:detail` parses as the scheme `catalog:` and is rejected as
|
|
74
|
+
* well.
|
|
75
|
+
*
|
|
76
|
+
* This matters because navigation targets are typically derived from server
|
|
77
|
+
* data: an `href` built from a record field is untrusted input, and a real
|
|
78
|
+
* anchor is followed natively on middle click, `Cmd`/`Ctrl` + click and through
|
|
79
|
+
* the right-click context menu, where no click handler runs to intercept it.
|
|
80
|
+
*
|
|
81
|
+
* An empty or whitespace-only value is rejected as well: `<a href="">` points at
|
|
82
|
+
* the current document and reloads the page.
|
|
83
|
+
*
|
|
84
|
+
* Protocol-relative URLs (`//evil.com`) are rejected too. They carry no scheme,
|
|
85
|
+
* so they read as relative, but the browser resolves them against the current
|
|
86
|
+
* one and navigates off-origin — and no in-app route ever needs that shape.
|
|
87
|
+
* Absolute `http`/`https` URLs stay allowed on purpose: a cross-origin target
|
|
88
|
+
* can be legitimate, so the caller owns that decision — including whether it
|
|
89
|
+
* can carry `rel="noopener"`, which is a per-call-site matter.
|
|
90
|
+
*
|
|
91
|
+
* A rejected value is dropped silently: nothing is logged, and the element
|
|
92
|
+
* renders without an anchor.
|
|
93
|
+
*/
|
|
94
|
+
const isSafeHref = (href) => {
|
|
95
|
+
var _a;
|
|
96
|
+
if (!href)
|
|
97
|
+
return false;
|
|
98
|
+
// Normalized the way the browser would parse it, so a value that only looks
|
|
99
|
+
// safe before the noise is stripped cannot get through.
|
|
100
|
+
const normalized = href.replace(CONTROL_CHARACTERS, '').toLowerCase();
|
|
101
|
+
if (!normalized)
|
|
102
|
+
return false;
|
|
103
|
+
// Checked before the scheme, since a protocol-relative URL carries none.
|
|
104
|
+
if (PROTOCOL_RELATIVE_PATTERN.test(normalized))
|
|
105
|
+
return false;
|
|
106
|
+
const scheme = (_a = SCHEME_PATTERN.exec(normalized)) === null || _a === void 0 ? void 0 : _a[1];
|
|
107
|
+
// No scheme at all — a relative URL, resolved against the current document.
|
|
108
|
+
if (!scheme)
|
|
109
|
+
return true;
|
|
110
|
+
return SAFE_SCHEMES.includes(`${scheme}:`);
|
|
111
|
+
};
|
|
112
|
+
exports.isSafeHref = isSafeHref;
|
|
@@ -167,3 +167,11 @@
|
|
|
167
167
|
opacity: 1 !important;
|
|
168
168
|
cursor: not-allowed;
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
/* Declarative action column items (`actionColumn.items`). Keeps the buttons on
|
|
172
|
+
one line instead of letting them wrap in a narrow action column. */
|
|
173
|
+
.akitable-action-items {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.25rem;
|
|
177
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"datatable.d.ts","sourceRoot":"","sources":["../../../src/components/datatable.tsx"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,CAAC;AAQzB,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"datatable.d.ts","sourceRoot":"","sources":["../../../src/components/datatable.tsx"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,CAAC;AAQzB,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,EAOL,sBAAsB,EAIvB,MAAM,UAAU,CAAC;AAKlB,eAAO,MAAM,iBAAiB,GAAI,OAAO,sBAAsB,sBA2hB9D,CAAC"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
14
|
exports.AkitableDataTable = void 0;
|
|
4
15
|
require("./datatable.css");
|
|
@@ -10,6 +21,7 @@ const antd_1 = require("antd");
|
|
|
10
21
|
const clsx_1 = require("clsx");
|
|
11
22
|
const react_1 = require("react");
|
|
12
23
|
const akitable_context_1 = require("../akitable-context");
|
|
24
|
+
const navigation_1 = require("../common/navigation");
|
|
13
25
|
const i18n_1 = require("../i18n");
|
|
14
26
|
const types_1 = require("../types");
|
|
15
27
|
const EditableCell_1 = require("./EditableCell");
|
|
@@ -26,12 +38,46 @@ const AkitableDataTable = (props) => {
|
|
|
26
38
|
if (!editingKey)
|
|
27
39
|
setHasFormErrors(false);
|
|
28
40
|
}, [editingKey]);
|
|
41
|
+
/** Keys of the misconfiguration reports already made by this instance. */
|
|
42
|
+
const warnedConfigKeysRef = react_1.default.useRef(new Set());
|
|
43
|
+
// Developer-facing misconfiguration reports, stripped from production builds
|
|
44
|
+
// the way antd's own `devUseWarning` is: the messages help nobody in a
|
|
45
|
+
// shipped app, and the guard lets a bundler drop the whole block.
|
|
46
|
+
//
|
|
47
|
+
// Re-checked when the props change rather than only on mount: an
|
|
48
|
+
// `actionColumn` is commonly built from data (permissions, an async config),
|
|
49
|
+
// so it can arrive after the first render. Each report is deduplicated per
|
|
50
|
+
// instance instead, since `actionColumn` is typically an inline object
|
|
51
|
+
// literal whose identity changes on every render.
|
|
29
52
|
react_1.default.useEffect(() => {
|
|
53
|
+
var _a;
|
|
54
|
+
if (process.env.NODE_ENV === 'production')
|
|
55
|
+
return;
|
|
56
|
+
const warnOnce = (key, message) => {
|
|
57
|
+
if (warnedConfigKeysRef.current.has(key))
|
|
58
|
+
return;
|
|
59
|
+
warnedConfigKeysRef.current.add(key);
|
|
60
|
+
console.warn(message);
|
|
61
|
+
};
|
|
30
62
|
if (onRowEdit && !actionColumn) {
|
|
31
|
-
|
|
63
|
+
warnOnce('onRowEdit-without-actionColumn', '[Akitable] onRowEdit is provided without actionColumn. ' +
|
|
32
64
|
'Please migrate to the actionColumn prop to customize the action column.');
|
|
33
65
|
}
|
|
34
|
-
|
|
66
|
+
if ((actionColumn === null || actionColumn === void 0 ? void 0 : actionColumn.render) && (actionColumn === null || actionColumn === void 0 ? void 0 : actionColumn.items)) {
|
|
67
|
+
warnOnce('render-and-items', '[Akitable] actionColumn.render and actionColumn.items are both provided. ' +
|
|
68
|
+
'render takes precedence and items is ignored.');
|
|
69
|
+
}
|
|
70
|
+
if (actionColumn && !actionColumn.render && !actionColumn.items) {
|
|
71
|
+
warnOnce('actionColumn-without-actions', '[Akitable] actionColumn is provided without render or items. ' +
|
|
72
|
+
'The column renders no actions — and it replaces the built-in edit ' +
|
|
73
|
+
'column, so there is no way to enter edit mode either. Provide one ' +
|
|
74
|
+
'of them.');
|
|
75
|
+
}
|
|
76
|
+
if (!onRowEdit && ((_a = actionColumn === null || actionColumn === void 0 ? void 0 : actionColumn.items) === null || _a === void 0 ? void 0 : _a.some(item => item.startEdit))) {
|
|
77
|
+
warnOnce('startEdit-without-onRowEdit', '[Akitable] actionColumn.items contains a startEdit item but onRowEdit ' +
|
|
78
|
+
'is not provided. Saving the edited row would fail.');
|
|
79
|
+
}
|
|
80
|
+
}, [actionColumn, onRowEdit]);
|
|
35
81
|
const [isOverflowing, setIsOverflowing] = react_1.default.useState(false);
|
|
36
82
|
const wrapperRef = react_1.default.useRef(null);
|
|
37
83
|
const hasActions = actions && actions.length > 0;
|
|
@@ -61,6 +107,15 @@ const AkitableDataTable = (props) => {
|
|
|
61
107
|
e === null || e === void 0 ? void 0 : e.stopPropagation();
|
|
62
108
|
},
|
|
63
109
|
handleSaveEditingItem: async () => {
|
|
110
|
+
// Without `onRowEdit` there is nowhere to save to. Keep the row editable
|
|
111
|
+
// rather than throwing and discarding what was typed, and report the
|
|
112
|
+
// misconfiguration outside production builds.
|
|
113
|
+
if (!onRowEdit) {
|
|
114
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
115
|
+
console.error('[Akitable] Cannot save the edited row because onRowEdit is not provided.');
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
64
119
|
try {
|
|
65
120
|
setIsLoadingSaveButton(true);
|
|
66
121
|
await onRowEdit(record, form.getFieldsValue());
|
|
@@ -73,7 +128,121 @@ const AkitableDataTable = (props) => {
|
|
|
73
128
|
},
|
|
74
129
|
handleCancelEditingItem: () => setEditingKey(null)
|
|
75
130
|
});
|
|
76
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The save/cancel pair shown while a row is being edited. Shared by the
|
|
133
|
+
* default edit column and the declarative `items` action column so both
|
|
134
|
+
* render an identical editing state.
|
|
135
|
+
*/
|
|
136
|
+
const renderEditingActions = ({ handleSaveEditingItem, handleCancelEditingItem }) => (react_1.default.createElement("span", null,
|
|
137
|
+
react_1.default.createElement(ui_button_1.Button, { size: "small", onClick: handleSaveEditingItem, loading: isLoadingSaveButton, disabled: hasFormErrors, className: (0, clsx_1.default)({
|
|
138
|
+
'akitable-save-button--has-errors': hasFormErrors
|
|
139
|
+
}) }, t('save')),
|
|
140
|
+
react_1.default.createElement(ui_typography_1.Text, Object.assign({ type: "danger", disabled: isLoadingSaveButton, className: "cursor-pointer ml-2" }, (!isLoadingSaveButton && { onClick: handleCancelEditingItem })), t('cancel'))));
|
|
141
|
+
const resolveActionItemValue = (value, record) => typeof value === 'function'
|
|
142
|
+
? value(record)
|
|
143
|
+
: value;
|
|
144
|
+
const renderActionItem = (item, index, onNavigate, params) => {
|
|
145
|
+
const { record } = params;
|
|
146
|
+
const { key, label, startEdit, href: hrefValue, hidden, disabled: disabledValue, onClick: onItemClick } = item, buttonProps = __rest(item, ["key", "label", "startEdit", "href", "hidden", "disabled", "onClick"]);
|
|
147
|
+
if (resolveActionItemValue(hidden, record))
|
|
148
|
+
return null;
|
|
149
|
+
const resolvedHref = resolveActionItemValue(hrefValue, record);
|
|
150
|
+
const disabled = resolveActionItemValue(disabledValue, record);
|
|
151
|
+
// An `href` is usually derived from record data, so it is validated rather
|
|
152
|
+
// than trusted — see `isSafeHref` for which values are accepted and why
|
|
153
|
+
// rendering a real anchor makes that necessary.
|
|
154
|
+
//
|
|
155
|
+
// A rejected value is dropped silently: it never reaches the anchor, the
|
|
156
|
+
// item degrades to a plain button and nothing is logged. A builder slip
|
|
157
|
+
// like `record => record.detailUrl ?? ''` therefore shows up as a button
|
|
158
|
+
// where a link was expected, not as a console message.
|
|
159
|
+
const href = (0, navigation_1.isSafeHref)(resolvedHref) ? resolvedHref : undefined;
|
|
160
|
+
const handleClick = (event) => {
|
|
161
|
+
// Action buttons live inside a row that may have its own `onRowClick`.
|
|
162
|
+
// The wrapper in `renderActionItems` stops the same propagation for the
|
|
163
|
+
// clicks antd swallows before this handler ever runs; this one covers the
|
|
164
|
+
// ordinary path.
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
if (startEdit) {
|
|
167
|
+
// A `startEdit` item never becomes an anchor (see the `href` prop
|
|
168
|
+
// below), so there is no default navigation to prevent here.
|
|
169
|
+
params.handleStartEdit(event);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!href) {
|
|
173
|
+
onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(record, event);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// New tab request (middle click, Cmd/Ctrl/Shift + click) — hand over to
|
|
177
|
+
// the browser. Never `preventDefault()` here, otherwise the native
|
|
178
|
+
// anchor behavior is destroyed.
|
|
179
|
+
if ((0, navigation_1.isNewTabRequest)(event) || event.defaultPrevented)
|
|
180
|
+
return;
|
|
181
|
+
// A per-item `onClick` wins over the column level `onNavigate` so a
|
|
182
|
+
// single item can route itself.
|
|
183
|
+
const navigate = onItemClick
|
|
184
|
+
? () => onItemClick(record, event)
|
|
185
|
+
: onNavigate
|
|
186
|
+
? () => onNavigate(href, record, event)
|
|
187
|
+
: undefined;
|
|
188
|
+
// With no handler there is nothing to intercept: let the browser follow
|
|
189
|
+
// the `href` instead of leaving a dead link behind.
|
|
190
|
+
if (!navigate)
|
|
191
|
+
return;
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
navigate();
|
|
194
|
+
};
|
|
195
|
+
return (react_1.default.createElement(ui_button_1.Button, Object.assign({}, buttonProps, { key: key !== null && key !== void 0 ? key : index,
|
|
196
|
+
// An `href` makes `@akinon/ui-button` render a real <a href>, which is
|
|
197
|
+
// what enables the browser's native new-tab gestures.
|
|
198
|
+
//
|
|
199
|
+
// Neither a disabled, a loading nor a `startEdit` item may become an
|
|
200
|
+
// anchor. antd swallows the click handler of a disabled Button, so
|
|
201
|
+
// `stopPropagation` would never run and the click would bubble up to
|
|
202
|
+
// `onRowClick`; a real <button disabled> dispatches no click at all.
|
|
203
|
+
//
|
|
204
|
+
// A loading Button is swallowed the same way but is *not* marked
|
|
205
|
+
// disabled in the DOM, so an anchor left in place would take a click
|
|
206
|
+
// that reaches neither the item nor `preventDefault` — antd calls that
|
|
207
|
+
// itself — leaving a dead left click whose event still bubbles to
|
|
208
|
+
// `onRowClick`, while a middle click would keep opening a new tab. That
|
|
209
|
+
// is exactly the "two gestures, two targets" split this API exists to
|
|
210
|
+
// avoid. `loading` is read as truthy, so the `{ delay }` form drops the
|
|
211
|
+
// anchor from the start rather than once the delay elapses.
|
|
212
|
+
//
|
|
213
|
+
// And a `startEdit` item is not a navigation target, so an anchor would
|
|
214
|
+
// be followed by the browser on a middle click or through the context
|
|
215
|
+
// menu — gestures that dispatch `auxclick` or no event at all, leaving
|
|
216
|
+
// no handler to stop.
|
|
217
|
+
href: disabled || startEdit || item.loading ? undefined : href, disabled: disabled, size: "small", onClick: handleClick }), label));
|
|
218
|
+
};
|
|
219
|
+
const renderActionItems = (items, onNavigate, params) => {
|
|
220
|
+
if (params.isEditing)
|
|
221
|
+
return renderEditingActions(params);
|
|
222
|
+
return (react_1.default.createElement("span", { className: "akitable-action-items",
|
|
223
|
+
// Safety net for the row's own `onRowClick`: antd never calls a
|
|
224
|
+
// Button's `onClick` while it is loading or disabled, so an item's own
|
|
225
|
+
// `stopPropagation` cannot be relied on to keep the click from bubbling
|
|
226
|
+
// up to the row.
|
|
227
|
+
onClick: event => event.stopPropagation() }, items.map((item, index) => renderActionItem(item, index, onNavigate, params))));
|
|
228
|
+
};
|
|
229
|
+
const buildActionColumn = (col) => {
|
|
230
|
+
// `items`/`onNavigate` are Akinon-only fields; keep them out of the object
|
|
231
|
+
// that is handed to antd's `Table` as a column definition.
|
|
232
|
+
const { items, onNavigate, render } = col, columnProps = __rest(col, ["items", "onNavigate", "render"]);
|
|
233
|
+
return Object.assign(Object.assign({}, columnProps), { key: '__akitable_action__',
|
|
234
|
+
// rc-table passes the whole record as the first argument when the column
|
|
235
|
+
// has no `dataIndex`.
|
|
236
|
+
render: (record) => {
|
|
237
|
+
const params = Object.assign({ record, isEditing: checkIsEditingRow(getRowKeyData(record)), isEditLoading: isLoadingSaveButton, hasEditFormError: hasFormErrors }, buildActionHandlers(record));
|
|
238
|
+
// `render` wins over `items` so existing consumers are untouched.
|
|
239
|
+
if (render)
|
|
240
|
+
return render(params);
|
|
241
|
+
if (items)
|
|
242
|
+
return renderActionItems(items, onNavigate, params);
|
|
243
|
+
return null;
|
|
244
|
+
} });
|
|
245
|
+
};
|
|
77
246
|
const handleFormFieldsChange = (_, allFields) => {
|
|
78
247
|
setHasFormErrors(allFields.some(f => { var _a, _b; return ((_b = (_a = f.errors) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; }));
|
|
79
248
|
};
|
|
@@ -84,11 +253,10 @@ const AkitableDataTable = (props) => {
|
|
|
84
253
|
render: (record) => {
|
|
85
254
|
const isRowEditing = checkIsEditingRow(getRowKeyData(record));
|
|
86
255
|
const { handleStartEdit, handleSaveEditingItem, handleCancelEditingItem } = buildActionHandlers(record);
|
|
87
|
-
return isRowEditing ? (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
react_1.default.createElement(ui_typography_1.Text, Object.assign({ type: "danger", disabled: isLoadingSaveButton, className: "cursor-pointer ml-2" }, (!isLoadingSaveButton && { onClick: handleCancelEditingItem })), t('cancel')))) : (react_1.default.createElement(ui_button_1.Button, { type: "primary", size: "small", onClick: handleStartEdit, className: (0, clsx_1.default)({
|
|
256
|
+
return isRowEditing ? (renderEditingActions({
|
|
257
|
+
handleSaveEditingItem,
|
|
258
|
+
handleCancelEditingItem
|
|
259
|
+
})) : (react_1.default.createElement(ui_button_1.Button, { type: "primary", size: "small", onClick: handleStartEdit, className: (0, clsx_1.default)({
|
|
92
260
|
'akinon-table-edit-button--disable': isLoadingSaveButton
|
|
93
261
|
}) }, t('edit')));
|
|
94
262
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../../src/components/pagination.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../../src/components/pagination.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AA6B1B,eAAO,MAAM,kBAAkB,yBA8C9B,CAAC"}
|
|
@@ -4,12 +4,25 @@ exports.AkitablePagination = void 0;
|
|
|
4
4
|
const ui_pagination_1 = require("@akinon/ui-pagination");
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const akitable_context_1 = require("../akitable-context");
|
|
7
|
+
const constants_1 = require("../constants");
|
|
7
8
|
const i18n_1 = require("../i18n");
|
|
8
|
-
const
|
|
9
|
+
const resolvePageSizeOptions = (requestedOptions) => [
|
|
10
|
+
...new Set((requestedOptions === null || requestedOptions === void 0 ? void 0 : requestedOptions.length)
|
|
11
|
+
? requestedOptions
|
|
12
|
+
: constants_1.AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS)
|
|
13
|
+
];
|
|
14
|
+
const resolvePageSize = (requestedSize, pageSizeOptions) => {
|
|
15
|
+
const isPositiveInteger = typeof requestedSize === 'number' &&
|
|
16
|
+
Number.isInteger(requestedSize) &&
|
|
17
|
+
requestedSize > 0;
|
|
18
|
+
return isPositiveInteger ? requestedSize : pageSizeOptions[0];
|
|
19
|
+
};
|
|
9
20
|
const AkitablePagination = () => {
|
|
10
21
|
const { isLoading, pagination, onPaginationChanged, paginationShowTotal } = (0, akitable_context_1.useAkitableContext)();
|
|
11
22
|
const { t } = i18n_1.i18n;
|
|
12
23
|
const totalPagination = (pagination === null || pagination === void 0 ? void 0 : pagination.total) || 0;
|
|
24
|
+
const pageSizeOptions = resolvePageSizeOptions(pagination === null || pagination === void 0 ? void 0 : pagination.pageSizeOptions);
|
|
25
|
+
const pageSize = resolvePageSize(pagination === null || pagination === void 0 ? void 0 : pagination.size, pageSizeOptions);
|
|
13
26
|
const hasItems = totalPagination > 0;
|
|
14
27
|
const locale = {
|
|
15
28
|
items_per_page: t('itemsPerPage')
|
|
@@ -29,6 +42,6 @@ const AkitablePagination = () => {
|
|
|
29
42
|
return null;
|
|
30
43
|
};
|
|
31
44
|
return (react_1.default.createElement("div", { className: "akitable-pagination" },
|
|
32
|
-
react_1.default.createElement(ui_pagination_1.Pagination, { disabled: isLoading || !hasItems, current: pagination === null || pagination === void 0 ? void 0 : pagination.page, pageSize:
|
|
45
|
+
react_1.default.createElement(ui_pagination_1.Pagination, { disabled: isLoading || !hasItems, current: pagination === null || pagination === void 0 ? void 0 : pagination.page, pageSize: pageSize, pageSizeOptions: pageSizeOptions, total: totalPagination, showTotal: handleShowTotal, showSizeChanger: true, onChange: handleOnChange, locale: locale, theme: "light" })));
|
|
33
46
|
};
|
|
34
47
|
exports.AkitablePagination = AkitablePagination;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,eAAO,MAAM,kCAAkC,EAAE,SAAS,iBAAiB,EACxC,CAAC"}
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./akitable"), exports);
|
|
18
|
+
__exportStar(require("./constants"), exports);
|
|
18
19
|
__exportStar(require("./types"), exports);
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IButtonProps } from '@akinon/ui-button';
|
|
1
2
|
import type { InputProps } from '@akinon/ui-input';
|
|
2
3
|
import { FormItemProps, TableColumnType } from 'antd';
|
|
3
4
|
import React, { ReactNode } from 'react';
|
|
@@ -30,6 +31,63 @@ export interface AkitableActionColumnRenderParams {
|
|
|
30
31
|
/** Cancels the current edit and exits edit mode without saving. */
|
|
31
32
|
handleCancelEditingItem: () => void;
|
|
32
33
|
}
|
|
34
|
+
/** A value that is either static or derived from the row it belongs to. */
|
|
35
|
+
export type AkitableActionItemValue<T> = T | ((record: AkitableData) => T);
|
|
36
|
+
/**
|
|
37
|
+
* A single declarative action rendered in the action column. Use it instead of
|
|
38
|
+
* a custom `render` when you want navigating actions to open in a new tab on
|
|
39
|
+
* middle click, `Cmd`/`Ctrl` + click or through the right-click context menu.
|
|
40
|
+
*
|
|
41
|
+
* Every `@akinon/ui-button` prop (`icon`, `iconSize`, `type`, `danger`,
|
|
42
|
+
* `tooltip`, `loading`, …) is accepted and forwarded to the rendered button.
|
|
43
|
+
* The exceptions are the ones this API redefines below, plus `size`, which the
|
|
44
|
+
* action column fixes to `small`.
|
|
45
|
+
*/
|
|
46
|
+
export interface AkitableActionItem extends Omit<IButtonProps, 'children' | 'disabled' | 'hidden' | 'href' | 'onClick' | 'size'> {
|
|
47
|
+
/** Stable key for the item. Falls back to the array index. */
|
|
48
|
+
key?: string;
|
|
49
|
+
/** Button label. Omit for icon-only buttons. */
|
|
50
|
+
label?: React.ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* Navigation target. When provided the item renders as a real `<a href>`, so
|
|
53
|
+
* middle click, `Cmd`/`Ctrl` + click and the right-click context menu open it
|
|
54
|
+
* in a new tab through native browser behavior.
|
|
55
|
+
*
|
|
56
|
+
* A plain left click is intercepted and delegated to this item's `onClick`
|
|
57
|
+
* if it has one, otherwise to the column's `onNavigate`. When neither is
|
|
58
|
+
* defined, the click is left alone and the browser follows the `href` as a
|
|
59
|
+
* regular link.
|
|
60
|
+
*
|
|
61
|
+
* No `target` is set on the anchor — that would make a plain left click open
|
|
62
|
+
* a new tab as well. The value must be a URL the browser can resolve on its
|
|
63
|
+
* own: a relative URL (`/products/1`, `products/1`, `#/products/1`, `?page=2`)
|
|
64
|
+
* or an `http`, `https`, `mailto` or `tel` one. Anything else is rejected —
|
|
65
|
+
* `javascript:` above all, but also a protocol-relative URL (`//evil.com`),
|
|
66
|
+
* an empty string, and an internal routing key shaped like `catalog:detail`,
|
|
67
|
+
* which parses as the scheme `catalog:`.
|
|
68
|
+
*
|
|
69
|
+
* A rejected value is dropped silently — nothing is logged and the item
|
|
70
|
+
* renders as a plain button — so check the values a builder can produce
|
|
71
|
+
* rather than waiting for a console message. An `href` is ignored altogether
|
|
72
|
+
* for a `startEdit` item.
|
|
73
|
+
*/
|
|
74
|
+
href?: AkitableActionItemValue<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Click handler. For a non-navigating action (delete, toggles, …) this is the
|
|
77
|
+
* action itself. For an item that also has an `href` it is the per-item
|
|
78
|
+
* navigation handler and takes precedence over the column's `onNavigate`.
|
|
79
|
+
*/
|
|
80
|
+
onClick?: (record: AkitableData, event: React.MouseEvent) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Puts the row into edit mode. Shorthand for `handleStartEdit`. Requires the
|
|
83
|
+
* table's `onRowEdit` to be defined, since that is what saves the row.
|
|
84
|
+
*/
|
|
85
|
+
startEdit?: boolean;
|
|
86
|
+
/** Whether the button is disabled. */
|
|
87
|
+
disabled?: AkitableActionItemValue<boolean>;
|
|
88
|
+
/** When `true` the item is not rendered for that row. */
|
|
89
|
+
hidden?: AkitableActionItemValue<boolean>;
|
|
90
|
+
}
|
|
33
91
|
export interface AkitableActionColumn {
|
|
34
92
|
/** Column header content. */
|
|
35
93
|
title?: React.ReactNode;
|
|
@@ -41,8 +99,35 @@ export interface AkitableActionColumn {
|
|
|
41
99
|
className?: string;
|
|
42
100
|
/** Text alignment within the column. */
|
|
43
101
|
align?: 'left' | 'right' | 'center';
|
|
44
|
-
/**
|
|
45
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Render function receiving row edit state and handler functions.
|
|
104
|
+
*
|
|
105
|
+
* Takes precedence over `items` when both are provided, so existing usages
|
|
106
|
+
* keep working unchanged. Note that anything rendered here is opaque to
|
|
107
|
+
* Akitable: to get new-tab support for navigating actions, either use `items`
|
|
108
|
+
* or render your own real `<a href>` elements.
|
|
109
|
+
*/
|
|
110
|
+
render?: (params: AkitableActionColumnRenderParams) => React.ReactNode;
|
|
111
|
+
/**
|
|
112
|
+
* Declarative action items. Items with an `href` render as real anchors, so
|
|
113
|
+
* middle click, `Cmd`/`Ctrl` + click and the right-click context menu open
|
|
114
|
+
* them in a new tab.
|
|
115
|
+
*
|
|
116
|
+
* Ignored when `render` is also provided.
|
|
117
|
+
*/
|
|
118
|
+
items?: AkitableActionItem[];
|
|
119
|
+
/**
|
|
120
|
+
* Called on a plain left click of an item that has an `href`, after
|
|
121
|
+
* `preventDefault()`. Perform the client-side navigation here (e.g.
|
|
122
|
+
* react-router's `navigate`). Not called for new-tab requests, nor for an
|
|
123
|
+
* item that defines its own `onClick`.
|
|
124
|
+
*
|
|
125
|
+
* The `href` is not necessarily an in-app route: an absolute `http(s)` URL
|
|
126
|
+
* and the `mailto:` / `tel:` schemes are accepted as well. Handing such a
|
|
127
|
+
* value straight to a client-side router would break the navigation, so
|
|
128
|
+
* check for them here before routing.
|
|
129
|
+
*/
|
|
130
|
+
onNavigate?: (href: string, record: AkitableData, event: React.MouseEvent) => void;
|
|
46
131
|
}
|
|
47
132
|
export interface AkitableProps {
|
|
48
133
|
/**
|
|
@@ -247,7 +332,12 @@ export interface AkitableEmptyStateProps {
|
|
|
247
332
|
*/
|
|
248
333
|
description?: ReactNode;
|
|
249
334
|
}
|
|
250
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Number of rows per page. The default options are kept as literals so editors
|
|
337
|
+
* still suggest them, but any number is accepted so apps can supply their own
|
|
338
|
+
* `pageSizeOptions`.
|
|
339
|
+
*/
|
|
340
|
+
export type AkitablePageSizes = 20 | 50 | 100 | 250 | (number & NonNullable<unknown>);
|
|
251
341
|
export interface AkitablePaginationProps {
|
|
252
342
|
/**
|
|
253
343
|
* Current page number.
|
|
@@ -257,6 +347,12 @@ export interface AkitablePaginationProps {
|
|
|
257
347
|
* Number of rows per page.
|
|
258
348
|
*/
|
|
259
349
|
size: AkitablePageSizes;
|
|
350
|
+
/**
|
|
351
|
+
* Page sizes offered by the "items per page" dropdown. Defaults to
|
|
352
|
+
* `AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS` ([20, 50, 100, 250]) when omitted or
|
|
353
|
+
* empty, so existing tables keep their current options.
|
|
354
|
+
*/
|
|
355
|
+
pageSizeOptions?: readonly AkitablePageSizes[];
|
|
260
356
|
}
|
|
261
357
|
export interface CustomEditRenderParams {
|
|
262
358
|
/** The data record for the current row. */
|