@mono-lit/helper 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +55 -55
- package/README.md +35 -35
- package/dist/data-source-controller-B_rgHCjw.js +241 -0
- package/dist/{data-source-controller-C50Qyfzh.js → data-source-controller-Dbb4fd8I.js} +69 -6
- package/dist/{data-source-options-BTpfrOZ_.js → data-source-options-CP-X62Pa.js} +2 -2
- package/dist/{data-source-options-CQSTZHPE.js → data-source-options-Jqj7LU3Z.js} +1 -1
- package/dist/{data-source-read-CUX-lgrQ.js → data-source-read-B4DGp6UQ.js} +1 -1
- package/dist/{export-BvlBx_GZ.js → export-BiGVyxt6.js} +2 -2
- package/dist/{field-icons-BoOG6KrL.js → field-icons-DELrSdqJ.js} +36 -1
- package/dist/index.js +7 -7
- package/dist/{index.node-DjvYOL-J.js → index.node-BIqEGehH.js} +5 -4
- package/dist/index.node.js +7 -7
- package/dist/{mono-button-CoVnBLav.js → mono-button-simcTQdW.js} +2 -2
- package/dist/{mono-button.shadow-FfP_hMH4.js → mono-button.shadow-B_rKOY9N.js} +2 -2
- package/dist/{mono-data-chart-ChU4Ii3p.js → mono-data-chart-CEv29_NS.js} +53 -12
- package/dist/{mono-data-dropdown-BAUl58kW.js → mono-data-dropdown-Wnoke6zO.js} +1 -1
- package/dist/{mono-data-grid-CLBK8clE.js → mono-data-grid-CjkEqxgE.js} +5 -232
- package/dist/{mono-data-grid-Db80FzdZ.js → mono-data-grid-YjR3n9Ar.js} +3 -230
- package/dist/{data-search-Bwo5bg8K.js → normalize-error-Gcgmgh5i.js} +299 -1
- package/dist/{reactive-BHsqVjRh.js → reactive-Di3mTMnV.js} +299 -1
- package/dist/src/components/card/card-core.d.ts +1 -0
- package/dist/src/components/chart/chart-types.d.ts +19 -0
- package/dist/src/components/select/select-types.d.ts +26 -0
- package/dist/src/components/tag-input/tag-input-types.d.ts +26 -0
- package/dist/src/composables/data-source-controller.d.ts +29 -0
- package/dist/src/composables/field-icons.d.ts +8 -0
- package/dist/src/utils/normalize-error.d.ts +30 -0
- package/dist/{tooltip-CysgRgBi.js → tooltip-DQqNpqk2.js} +62 -27
- package/dist/tooltip.js +1 -1
- package/dist/ui/alert.js +1 -1
- package/dist/ui/button-dropdown.js +1 -1
- package/dist/ui/button.js +1 -1
- package/dist/ui/card.js +15 -2
- package/dist/ui/chart.js +81 -4
- package/dist/ui/dropdown-table.js +2 -2
- package/dist/ui/index.css +181 -18
- package/dist/ui/modal.js +3 -2
- package/dist/ui/select.js +153 -19
- package/dist/ui/shadow/alert.js +1 -1
- package/dist/ui/shadow/button-dropdown.js +1 -1
- package/dist/ui/shadow/button.js +1 -1
- package/dist/ui/shadow/card.js +16 -2
- package/dist/ui/shadow/chart.js +133 -17
- package/dist/ui/shadow/dropdown-table.js +2 -2
- package/dist/ui/shadow/modal.js +3 -2
- package/dist/ui/shadow/select.js +153 -19
- package/dist/ui/shadow/table.js +4 -4
- package/dist/ui/shadow/tag-input.js +154 -26
- package/dist/ui/table.js +3 -3
- package/dist/ui/tag-input.js +153 -25
- package/package.json +1 -1
|
@@ -570,4 +570,302 @@ var MonoSourceSearch = class {
|
|
|
570
570
|
}
|
|
571
571
|
};
|
|
572
572
|
//#endregion
|
|
573
|
-
|
|
573
|
+
//#region src/utils/normalize-error.ts
|
|
574
|
+
/** Fields worth reading off an object that is not an `Error`. */
|
|
575
|
+
var MESSAGE_KEYS = [
|
|
576
|
+
"message",
|
|
577
|
+
"statusText",
|
|
578
|
+
"errorMessage",
|
|
579
|
+
"error",
|
|
580
|
+
"detail"
|
|
581
|
+
];
|
|
582
|
+
var text = (value) => typeof value === "string" && value.trim() ? value.trim() : "";
|
|
583
|
+
var isObject = (value) => value !== null && typeof value === "object";
|
|
584
|
+
/**
|
|
585
|
+
* A readable message for `raw`, or `fallback` when nothing legible can be found.
|
|
586
|
+
*
|
|
587
|
+
* Never throws, and never returns an empty string — a caller rendering this into
|
|
588
|
+
* an error bar has to have something to render, and a blank bar is worse than a
|
|
589
|
+
* generic one.
|
|
590
|
+
*/
|
|
591
|
+
function normalizeError(raw, fallback = "Request failed") {
|
|
592
|
+
if (raw == null) return fallback;
|
|
593
|
+
if (raw instanceof Error) {
|
|
594
|
+
const own = text(raw.message);
|
|
595
|
+
if (own) return own;
|
|
596
|
+
} else {
|
|
597
|
+
if (typeof raw === "string") return text(raw) || fallback;
|
|
598
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
599
|
+
}
|
|
600
|
+
if (isObject(raw)) {
|
|
601
|
+
const obj = raw;
|
|
602
|
+
for (const key of MESSAGE_KEYS) {
|
|
603
|
+
const hit = text(obj[key]);
|
|
604
|
+
if (hit) return hit;
|
|
605
|
+
}
|
|
606
|
+
const response = obj.response;
|
|
607
|
+
if (isObject(response)) {
|
|
608
|
+
const nested = text(response.statusText) || text(response.message);
|
|
609
|
+
if (nested) return nested;
|
|
610
|
+
}
|
|
611
|
+
const status = errorStatus(obj);
|
|
612
|
+
if (status !== void 0 && status > 0) return `HTTP ${status}`;
|
|
613
|
+
if (raw instanceof Error) return text(raw.name) || fallback;
|
|
614
|
+
return fallback;
|
|
615
|
+
}
|
|
616
|
+
return text(String(raw)) || fallback;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* What the bar says for a status. English; an app puts its own language in
|
|
620
|
+
* ONCE with `setErrorMessages()` rather than on every table.
|
|
621
|
+
*/
|
|
622
|
+
var DEFAULT_ERROR_MESSAGES = Object.freeze({
|
|
623
|
+
network: "Could not reach the server. Check your connection and try again.",
|
|
624
|
+
400: "The server rejected the request.",
|
|
625
|
+
401: "Your session has expired. Please sign in again.",
|
|
626
|
+
403: "You do not have permission to view this data.",
|
|
627
|
+
404: "The requested data could not be found.",
|
|
628
|
+
408: "The server took too long to respond. Please try again.",
|
|
629
|
+
409: "The request conflicts with the current state of the data.",
|
|
630
|
+
422: "The server could not process the request.",
|
|
631
|
+
429: "Too many requests. Please wait a moment and try again.",
|
|
632
|
+
500: "Something went wrong on the server. Please try again later.",
|
|
633
|
+
502: "The server is temporarily unavailable. Please try again later.",
|
|
634
|
+
503: "The server is temporarily unavailable. Please try again later.",
|
|
635
|
+
504: "The server took too long to respond. Please try again later.",
|
|
636
|
+
default: "Request failed"
|
|
637
|
+
});
|
|
638
|
+
var globalMessages = null;
|
|
639
|
+
/**
|
|
640
|
+
* App-wide wording, merged over the defaults; `null` restores them. A grid's own
|
|
641
|
+
* `errorMessages` merges over this in turn, so a single table can still differ.
|
|
642
|
+
*/
|
|
643
|
+
function setErrorMessages(messages) {
|
|
644
|
+
globalMessages = messages ? { ...messages } : null;
|
|
645
|
+
}
|
|
646
|
+
/** Defaults ← app-wide ← `local`, same shape as the filter builder's `resolveTexts`. */
|
|
647
|
+
function resolveErrorMessages(local) {
|
|
648
|
+
const merged = { ...DEFAULT_ERROR_MESSAGES };
|
|
649
|
+
for (const layer of [globalMessages, local]) {
|
|
650
|
+
if (!layer) continue;
|
|
651
|
+
for (const key of Object.keys(layer)) {
|
|
652
|
+
const value = layer[key];
|
|
653
|
+
if (typeof value === "string" && value.trim()) merged[key] = value;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return merged;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The standard reason phrases. Not shown to anyone — they are what `describeError`
|
|
660
|
+
* uses to recognise that an error's text is ONLY the phrase for its status
|
|
661
|
+
* (`Error('Forbidden')` + `httpStatus: 403`), which adds nothing to the preset and
|
|
662
|
+
* so is not a detail worth appending.
|
|
663
|
+
*/
|
|
664
|
+
var REASON_PHRASES = {
|
|
665
|
+
400: "Bad Request",
|
|
666
|
+
401: "Unauthorized",
|
|
667
|
+
402: "Payment Required",
|
|
668
|
+
403: "Forbidden",
|
|
669
|
+
404: "Not Found",
|
|
670
|
+
405: "Method Not Allowed",
|
|
671
|
+
408: "Request Timeout",
|
|
672
|
+
409: "Conflict",
|
|
673
|
+
410: "Gone",
|
|
674
|
+
412: "Precondition Failed",
|
|
675
|
+
413: "Payload Too Large",
|
|
676
|
+
415: "Unsupported Media Type",
|
|
677
|
+
422: "Unprocessable Entity",
|
|
678
|
+
429: "Too Many Requests",
|
|
679
|
+
500: "Internal Server Error",
|
|
680
|
+
501: "Not Implemented",
|
|
681
|
+
502: "Bad Gateway",
|
|
682
|
+
503: "Service Unavailable",
|
|
683
|
+
504: "Gateway Timeout"
|
|
684
|
+
};
|
|
685
|
+
/** Texts a transport mints for "no answer at all" — a status of 0 in words. */
|
|
686
|
+
var NETWORK_TEXTS = [
|
|
687
|
+
"failed to fetch",
|
|
688
|
+
"networkerror when attempting to fetch resource",
|
|
689
|
+
"load failed",
|
|
690
|
+
"network error",
|
|
691
|
+
"unspecified network error",
|
|
692
|
+
"network connection timeout",
|
|
693
|
+
"network request failed"
|
|
694
|
+
];
|
|
695
|
+
var toStatus = (value) => {
|
|
696
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
697
|
+
const n = Number(value);
|
|
698
|
+
return Number.isInteger(n) && n >= 0 && n < 1e3 ? n : void 0;
|
|
699
|
+
};
|
|
700
|
+
/**
|
|
701
|
+
* The HTTP status an error carries, `0` for a network failure, `undefined` when
|
|
702
|
+
* it has none.
|
|
703
|
+
*
|
|
704
|
+
* Reads the fields the common transports use — devextreme's `httpStatus`, a
|
|
705
|
+
* fetch `Response` / axios error's `status` and `response.status`, ofetch's
|
|
706
|
+
* `statusCode`, an OData error body's numeric `code` — on an `Error` instance as
|
|
707
|
+
* readily as on a plain object, since devextreme `extend()`s them onto an
|
|
708
|
+
* `Error`.
|
|
709
|
+
*/
|
|
710
|
+
function errorStatus(raw) {
|
|
711
|
+
if (!isObject(raw)) return void 0;
|
|
712
|
+
const response = isObject(raw.response) ? raw.response : void 0;
|
|
713
|
+
const direct = toStatus(raw.httpStatus) ?? toStatus(raw.status) ?? toStatus(raw.statusCode) ?? toStatus(response?.status) ?? toStatus(response?.statusCode);
|
|
714
|
+
if (direct !== void 0) return direct;
|
|
715
|
+
const code = toStatus((isObject(raw.errorDetails) ? raw.errorDetails : void 0)?.code);
|
|
716
|
+
if (code !== void 0 && code >= 400) return code;
|
|
717
|
+
const own = (raw instanceof Error ? text(raw.message) : text(raw.message)).toLowerCase();
|
|
718
|
+
if (own && NETWORK_TEXTS.some((t) => own.includes(t))) return 0;
|
|
719
|
+
}
|
|
720
|
+
/** A JSON response body handed over as text (an XHR's `responseText`), parsed; else undefined. */
|
|
721
|
+
function parseBody(value) {
|
|
722
|
+
if (typeof value !== "string") return void 0;
|
|
723
|
+
const s = value.trim();
|
|
724
|
+
if (!s.startsWith("{") && !s.startsWith("[")) return void 0;
|
|
725
|
+
try {
|
|
726
|
+
return JSON.parse(s);
|
|
727
|
+
} catch {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** The "same message" key: case, spacing and trailing punctuation do not make two texts different. */
|
|
732
|
+
var messageKey = (value) => value.toLowerCase().replace(/\s+/g, " ").replace(/[\s.!:;,]+$/, "").trim();
|
|
733
|
+
/**
|
|
734
|
+
* Keep each message once. Two texts are the same message when they match after
|
|
735
|
+
* {@link messageKey}, and a text another kept one already CONTAINS adds nothing
|
|
736
|
+
* ("Not found" next to "Not found: Order 42") — the longer, more specific one wins
|
|
737
|
+
* and keeps its place in the order the messages were found.
|
|
738
|
+
*/
|
|
739
|
+
function uniqueMessages(found) {
|
|
740
|
+
const kept = [];
|
|
741
|
+
for (const t of found) {
|
|
742
|
+
const key = messageKey(t);
|
|
743
|
+
if (!key) continue;
|
|
744
|
+
if (kept.some((k) => k.key.includes(key))) continue;
|
|
745
|
+
const swallowed = kept.findIndex((k) => key.includes(k.key));
|
|
746
|
+
if (swallowed >= 0) {
|
|
747
|
+
kept[swallowed] = {
|
|
748
|
+
text: t,
|
|
749
|
+
key
|
|
750
|
+
};
|
|
751
|
+
for (let i = kept.length - 1; i > swallowed; i--) if (key.includes(kept[i].key)) kept.splice(i, 1);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
kept.push({
|
|
755
|
+
text: t,
|
|
756
|
+
key
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return kept.map((k) => k.text);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Every message an error carries, combined the way `monoFetch`'s
|
|
763
|
+
* `extractErrorMessage` does it: each key whose name contains "message"
|
|
764
|
+
* (`message`, `errorMessage`, `Message`, …) holding text — or OData's
|
|
765
|
+
* `{ message: { value } }` — at any depth up to 3, plus a JSON body still in
|
|
766
|
+
* `responseText` / `responseJSON` / `response`, plus an `Error`'s own
|
|
767
|
+
* (non-enumerable) `message`. The usual body fields `detail` / `title` and a
|
|
768
|
+
* plain-text `response.data` count too.
|
|
769
|
+
*
|
|
770
|
+
* Deduplicated by {@link uniqueMessages}, transport boilerplate ("Bad Request",
|
|
771
|
+
* "Failed to fetch") dropped, in the order found.
|
|
772
|
+
*/
|
|
773
|
+
function collectErrorMessages(raw, status = errorStatus(raw)) {
|
|
774
|
+
const found = [];
|
|
775
|
+
const add = (value) => {
|
|
776
|
+
const t = text(value);
|
|
777
|
+
if (t) found.push(t);
|
|
778
|
+
};
|
|
779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
780
|
+
const visit = (val, depth) => {
|
|
781
|
+
if (!isObject(val) || depth > 3 || seen.has(val)) return;
|
|
782
|
+
seen.add(val);
|
|
783
|
+
if (val instanceof Error) add(val.message);
|
|
784
|
+
for (const [k, v] of Object.entries(val)) {
|
|
785
|
+
if (/message/i.test(k)) {
|
|
786
|
+
if (typeof v === "string") add(v);
|
|
787
|
+
else if (isObject(v) && typeof v.value === "string") add(v.value);
|
|
788
|
+
} else if (k === "detail" || k === "title" || k === "errorDetail") add(v);
|
|
789
|
+
else if (k === "responseText" || k === "response" || k === "data" || k === "_data") {
|
|
790
|
+
if (typeof v === "string") {
|
|
791
|
+
const body = parseBody(v);
|
|
792
|
+
if (body !== void 0) visit(body, depth + 1);
|
|
793
|
+
else if (k !== "response") add(v);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (isObject(v)) visit(v, depth + 1);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
if (typeof raw === "string") add(raw);
|
|
800
|
+
else visit(raw, 0);
|
|
801
|
+
return uniqueMessages(found).filter((t) => !isBoilerplate(t, status));
|
|
802
|
+
}
|
|
803
|
+
/** Is `value` nothing more than the reason phrase / a generic label for `status`? */
|
|
804
|
+
function isBoilerplate(value, status) {
|
|
805
|
+
const v = value.toLowerCase();
|
|
806
|
+
if (!v || v === "error" || v === "unknown error" || v === "request failed") return true;
|
|
807
|
+
if (/^http\s*\d{3}$/.test(v)) return true;
|
|
808
|
+
if (NETWORK_TEXTS.some((t) => v.includes(t))) return true;
|
|
809
|
+
if (status !== void 0) {
|
|
810
|
+
const phrase = REASON_PHRASES[status]?.toLowerCase();
|
|
811
|
+
if (phrase && (v === phrase || v === `${status} ${phrase}` || v === `${status}`)) return true;
|
|
812
|
+
if (v === String(status)) return true;
|
|
813
|
+
}
|
|
814
|
+
return Object.values(REASON_PHRASES).some((p) => p.toLowerCase() === v);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* The one line a field shows for `err`: what the server said, every distinct
|
|
818
|
+
* message combined (`monoFetch` style), or the headline when it said nothing.
|
|
819
|
+
*/
|
|
820
|
+
function errorText(err, separator = " · ") {
|
|
821
|
+
return err.messages.length ? err.messages.join(separator) : err.message;
|
|
822
|
+
}
|
|
823
|
+
/** The hover text: the headline, then everything the server said, each once. */
|
|
824
|
+
function errorTitle(err) {
|
|
825
|
+
return uniqueMessages([err.message, ...err.messages]).join("\n");
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* The line a person should read for `raw`.
|
|
829
|
+
*
|
|
830
|
+
* Order: a preset for the status (`'network'` for 0) → the error's own text →
|
|
831
|
+
* `messages.default`. When a preset is the headline, whatever the server said
|
|
832
|
+
* on top of the status becomes `detail`; without a preset, the raw text IS the
|
|
833
|
+
* headline and only a distinct server body is a detail. Never throws, never
|
|
834
|
+
* returns an empty `message`.
|
|
835
|
+
*/
|
|
836
|
+
function describeError(raw, messages) {
|
|
837
|
+
const texts = resolveErrorMessages(messages);
|
|
838
|
+
const status = errorStatus(raw);
|
|
839
|
+
const fallback = texts.default || DEFAULT_ERROR_MESSAGES.default;
|
|
840
|
+
const rawText = normalizeError(raw, fallback);
|
|
841
|
+
const preset = status === void 0 ? void 0 : status === 0 ? texts.network : texts[status];
|
|
842
|
+
const said = collectErrorMessages(raw, status).filter((m) => m !== preset);
|
|
843
|
+
if (preset) {
|
|
844
|
+
const out = {
|
|
845
|
+
message: preset,
|
|
846
|
+
status,
|
|
847
|
+
messages: said
|
|
848
|
+
};
|
|
849
|
+
if (said.length) out.detail = said.join("\n\n");
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
const message = rawText || fallback;
|
|
853
|
+
const rest = said.filter((m) => messageKey(m) !== messageKey(message));
|
|
854
|
+
const out = {
|
|
855
|
+
message,
|
|
856
|
+
messages: said
|
|
857
|
+
};
|
|
858
|
+
if (status !== void 0) out.status = status;
|
|
859
|
+
if (rest.length) out.detail = rest.join("\n\n");
|
|
860
|
+
return out;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* A load a newer load superseded (devextreme rejects it with `'canceled'`).
|
|
864
|
+
* Not a failure — nothing should be shown for it.
|
|
865
|
+
*/
|
|
866
|
+
function isCanceled(err) {
|
|
867
|
+
if (!err) return false;
|
|
868
|
+
return err === "canceled" || err.name === "canceled" || err.message === "canceled" || err.__id === "canceled";
|
|
869
|
+
}
|
|
870
|
+
//#endregion
|
|
871
|
+
export { resolveSearchEntries as A, customRemoteClause as C, isWildcardPattern as D, hasWildcardSearch as E, normalizeSearchExpr as O, customPredicate as S, hasCustomSearch as T, plainSearchFields as _, errorTitle as a, searchRemoteFilter as b, resolveErrorMessages as c, DEFAULT_SEARCH_OPERATION as d, MonoSourceSearch as f, parseSearchValue as g, mergeSearchFields as h, errorText as i, searchEntryFor as j, plainSearchColumns as k, setErrorMessages as l, isFoldableSearch as m, describeError as n, isCanceled as o, SEARCH_FIELDS_KEYS as p, errorStatus as r, normalizeError as s, DEFAULT_ERROR_MESSAGES as t, DEFAULT_SEARCH_FIELDS as u, readSearchField as v, expandWildcard as w, searchRowPredicate as x, resolveSearchFields as y };
|
|
@@ -740,6 +740,304 @@ var MonoSourceSearch = class {
|
|
|
740
740
|
}
|
|
741
741
|
};
|
|
742
742
|
//#endregion
|
|
743
|
+
//#region src/utils/normalize-error.ts
|
|
744
|
+
/** Fields worth reading off an object that is not an `Error`. */
|
|
745
|
+
var MESSAGE_KEYS = [
|
|
746
|
+
"message",
|
|
747
|
+
"statusText",
|
|
748
|
+
"errorMessage",
|
|
749
|
+
"error",
|
|
750
|
+
"detail"
|
|
751
|
+
];
|
|
752
|
+
var text = (value) => typeof value === "string" && value.trim() ? value.trim() : "";
|
|
753
|
+
var isObject = (value) => value !== null && typeof value === "object";
|
|
754
|
+
/**
|
|
755
|
+
* A readable message for `raw`, or `fallback` when nothing legible can be found.
|
|
756
|
+
*
|
|
757
|
+
* Never throws, and never returns an empty string — a caller rendering this into
|
|
758
|
+
* an error bar has to have something to render, and a blank bar is worse than a
|
|
759
|
+
* generic one.
|
|
760
|
+
*/
|
|
761
|
+
function normalizeError(raw, fallback = "Request failed") {
|
|
762
|
+
if (raw == null) return fallback;
|
|
763
|
+
if (raw instanceof Error) {
|
|
764
|
+
const own = text(raw.message);
|
|
765
|
+
if (own) return own;
|
|
766
|
+
} else {
|
|
767
|
+
if (typeof raw === "string") return text(raw) || fallback;
|
|
768
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
769
|
+
}
|
|
770
|
+
if (isObject(raw)) {
|
|
771
|
+
const obj = raw;
|
|
772
|
+
for (const key of MESSAGE_KEYS) {
|
|
773
|
+
const hit = text(obj[key]);
|
|
774
|
+
if (hit) return hit;
|
|
775
|
+
}
|
|
776
|
+
const response = obj.response;
|
|
777
|
+
if (isObject(response)) {
|
|
778
|
+
const nested = text(response.statusText) || text(response.message);
|
|
779
|
+
if (nested) return nested;
|
|
780
|
+
}
|
|
781
|
+
const status = errorStatus(obj);
|
|
782
|
+
if (status !== void 0 && status > 0) return `HTTP ${status}`;
|
|
783
|
+
if (raw instanceof Error) return text(raw.name) || fallback;
|
|
784
|
+
return fallback;
|
|
785
|
+
}
|
|
786
|
+
return text(String(raw)) || fallback;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* What the bar says for a status. English; an app puts its own language in
|
|
790
|
+
* ONCE with `setErrorMessages()` rather than on every table.
|
|
791
|
+
*/
|
|
792
|
+
var DEFAULT_ERROR_MESSAGES = Object.freeze({
|
|
793
|
+
network: "Could not reach the server. Check your connection and try again.",
|
|
794
|
+
400: "The server rejected the request.",
|
|
795
|
+
401: "Your session has expired. Please sign in again.",
|
|
796
|
+
403: "You do not have permission to view this data.",
|
|
797
|
+
404: "The requested data could not be found.",
|
|
798
|
+
408: "The server took too long to respond. Please try again.",
|
|
799
|
+
409: "The request conflicts with the current state of the data.",
|
|
800
|
+
422: "The server could not process the request.",
|
|
801
|
+
429: "Too many requests. Please wait a moment and try again.",
|
|
802
|
+
500: "Something went wrong on the server. Please try again later.",
|
|
803
|
+
502: "The server is temporarily unavailable. Please try again later.",
|
|
804
|
+
503: "The server is temporarily unavailable. Please try again later.",
|
|
805
|
+
504: "The server took too long to respond. Please try again later.",
|
|
806
|
+
default: "Request failed"
|
|
807
|
+
});
|
|
808
|
+
var globalMessages = null;
|
|
809
|
+
/**
|
|
810
|
+
* App-wide wording, merged over the defaults; `null` restores them. A grid's own
|
|
811
|
+
* `errorMessages` merges over this in turn, so a single table can still differ.
|
|
812
|
+
*/
|
|
813
|
+
function setErrorMessages(messages) {
|
|
814
|
+
globalMessages = messages ? { ...messages } : null;
|
|
815
|
+
}
|
|
816
|
+
/** Defaults ← app-wide ← `local`, same shape as the filter builder's `resolveTexts`. */
|
|
817
|
+
function resolveErrorMessages(local) {
|
|
818
|
+
const merged = { ...DEFAULT_ERROR_MESSAGES };
|
|
819
|
+
for (const layer of [globalMessages, local]) {
|
|
820
|
+
if (!layer) continue;
|
|
821
|
+
for (const key of Object.keys(layer)) {
|
|
822
|
+
const value = layer[key];
|
|
823
|
+
if (typeof value === "string" && value.trim()) merged[key] = value;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return merged;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The standard reason phrases. Not shown to anyone — they are what `describeError`
|
|
830
|
+
* uses to recognise that an error's text is ONLY the phrase for its status
|
|
831
|
+
* (`Error('Forbidden')` + `httpStatus: 403`), which adds nothing to the preset and
|
|
832
|
+
* so is not a detail worth appending.
|
|
833
|
+
*/
|
|
834
|
+
var REASON_PHRASES = {
|
|
835
|
+
400: "Bad Request",
|
|
836
|
+
401: "Unauthorized",
|
|
837
|
+
402: "Payment Required",
|
|
838
|
+
403: "Forbidden",
|
|
839
|
+
404: "Not Found",
|
|
840
|
+
405: "Method Not Allowed",
|
|
841
|
+
408: "Request Timeout",
|
|
842
|
+
409: "Conflict",
|
|
843
|
+
410: "Gone",
|
|
844
|
+
412: "Precondition Failed",
|
|
845
|
+
413: "Payload Too Large",
|
|
846
|
+
415: "Unsupported Media Type",
|
|
847
|
+
422: "Unprocessable Entity",
|
|
848
|
+
429: "Too Many Requests",
|
|
849
|
+
500: "Internal Server Error",
|
|
850
|
+
501: "Not Implemented",
|
|
851
|
+
502: "Bad Gateway",
|
|
852
|
+
503: "Service Unavailable",
|
|
853
|
+
504: "Gateway Timeout"
|
|
854
|
+
};
|
|
855
|
+
/** Texts a transport mints for "no answer at all" — a status of 0 in words. */
|
|
856
|
+
var NETWORK_TEXTS = [
|
|
857
|
+
"failed to fetch",
|
|
858
|
+
"networkerror when attempting to fetch resource",
|
|
859
|
+
"load failed",
|
|
860
|
+
"network error",
|
|
861
|
+
"unspecified network error",
|
|
862
|
+
"network connection timeout",
|
|
863
|
+
"network request failed"
|
|
864
|
+
];
|
|
865
|
+
var toStatus = (value) => {
|
|
866
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
867
|
+
const n = Number(value);
|
|
868
|
+
return Number.isInteger(n) && n >= 0 && n < 1e3 ? n : void 0;
|
|
869
|
+
};
|
|
870
|
+
/**
|
|
871
|
+
* The HTTP status an error carries, `0` for a network failure, `undefined` when
|
|
872
|
+
* it has none.
|
|
873
|
+
*
|
|
874
|
+
* Reads the fields the common transports use — devextreme's `httpStatus`, a
|
|
875
|
+
* fetch `Response` / axios error's `status` and `response.status`, ofetch's
|
|
876
|
+
* `statusCode`, an OData error body's numeric `code` — on an `Error` instance as
|
|
877
|
+
* readily as on a plain object, since devextreme `extend()`s them onto an
|
|
878
|
+
* `Error`.
|
|
879
|
+
*/
|
|
880
|
+
function errorStatus(raw) {
|
|
881
|
+
if (!isObject(raw)) return void 0;
|
|
882
|
+
const response = isObject(raw.response) ? raw.response : void 0;
|
|
883
|
+
const direct = toStatus(raw.httpStatus) ?? toStatus(raw.status) ?? toStatus(raw.statusCode) ?? toStatus(response?.status) ?? toStatus(response?.statusCode);
|
|
884
|
+
if (direct !== void 0) return direct;
|
|
885
|
+
const code = toStatus((isObject(raw.errorDetails) ? raw.errorDetails : void 0)?.code);
|
|
886
|
+
if (code !== void 0 && code >= 400) return code;
|
|
887
|
+
const own = (raw instanceof Error ? text(raw.message) : text(raw.message)).toLowerCase();
|
|
888
|
+
if (own && NETWORK_TEXTS.some((t) => own.includes(t))) return 0;
|
|
889
|
+
}
|
|
890
|
+
/** A JSON response body handed over as text (an XHR's `responseText`), parsed; else undefined. */
|
|
891
|
+
function parseBody(value) {
|
|
892
|
+
if (typeof value !== "string") return void 0;
|
|
893
|
+
const s = value.trim();
|
|
894
|
+
if (!s.startsWith("{") && !s.startsWith("[")) return void 0;
|
|
895
|
+
try {
|
|
896
|
+
return JSON.parse(s);
|
|
897
|
+
} catch {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/** The "same message" key: case, spacing and trailing punctuation do not make two texts different. */
|
|
902
|
+
var messageKey = (value) => value.toLowerCase().replace(/\s+/g, " ").replace(/[\s.!:;,]+$/, "").trim();
|
|
903
|
+
/**
|
|
904
|
+
* Keep each message once. Two texts are the same message when they match after
|
|
905
|
+
* {@link messageKey}, and a text another kept one already CONTAINS adds nothing
|
|
906
|
+
* ("Not found" next to "Not found: Order 42") — the longer, more specific one wins
|
|
907
|
+
* and keeps its place in the order the messages were found.
|
|
908
|
+
*/
|
|
909
|
+
function uniqueMessages(found) {
|
|
910
|
+
const kept = [];
|
|
911
|
+
for (const t of found) {
|
|
912
|
+
const key = messageKey(t);
|
|
913
|
+
if (!key) continue;
|
|
914
|
+
if (kept.some((k) => k.key.includes(key))) continue;
|
|
915
|
+
const swallowed = kept.findIndex((k) => key.includes(k.key));
|
|
916
|
+
if (swallowed >= 0) {
|
|
917
|
+
kept[swallowed] = {
|
|
918
|
+
text: t,
|
|
919
|
+
key
|
|
920
|
+
};
|
|
921
|
+
for (let i = kept.length - 1; i > swallowed; i--) if (key.includes(kept[i].key)) kept.splice(i, 1);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
kept.push({
|
|
925
|
+
text: t,
|
|
926
|
+
key
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return kept.map((k) => k.text);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Every message an error carries, combined the way `monoFetch`'s
|
|
933
|
+
* `extractErrorMessage` does it: each key whose name contains "message"
|
|
934
|
+
* (`message`, `errorMessage`, `Message`, …) holding text — or OData's
|
|
935
|
+
* `{ message: { value } }` — at any depth up to 3, plus a JSON body still in
|
|
936
|
+
* `responseText` / `responseJSON` / `response`, plus an `Error`'s own
|
|
937
|
+
* (non-enumerable) `message`. The usual body fields `detail` / `title` and a
|
|
938
|
+
* plain-text `response.data` count too.
|
|
939
|
+
*
|
|
940
|
+
* Deduplicated by {@link uniqueMessages}, transport boilerplate ("Bad Request",
|
|
941
|
+
* "Failed to fetch") dropped, in the order found.
|
|
942
|
+
*/
|
|
943
|
+
function collectErrorMessages(raw, status = errorStatus(raw)) {
|
|
944
|
+
const found = [];
|
|
945
|
+
const add = (value) => {
|
|
946
|
+
const t = text(value);
|
|
947
|
+
if (t) found.push(t);
|
|
948
|
+
};
|
|
949
|
+
const seen = /* @__PURE__ */ new Set();
|
|
950
|
+
const visit = (val, depth) => {
|
|
951
|
+
if (!isObject(val) || depth > 3 || seen.has(val)) return;
|
|
952
|
+
seen.add(val);
|
|
953
|
+
if (val instanceof Error) add(val.message);
|
|
954
|
+
for (const [k, v] of Object.entries(val)) {
|
|
955
|
+
if (/message/i.test(k)) {
|
|
956
|
+
if (typeof v === "string") add(v);
|
|
957
|
+
else if (isObject(v) && typeof v.value === "string") add(v.value);
|
|
958
|
+
} else if (k === "detail" || k === "title" || k === "errorDetail") add(v);
|
|
959
|
+
else if (k === "responseText" || k === "response" || k === "data" || k === "_data") {
|
|
960
|
+
if (typeof v === "string") {
|
|
961
|
+
const body = parseBody(v);
|
|
962
|
+
if (body !== void 0) visit(body, depth + 1);
|
|
963
|
+
else if (k !== "response") add(v);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (isObject(v)) visit(v, depth + 1);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
if (typeof raw === "string") add(raw);
|
|
970
|
+
else visit(raw, 0);
|
|
971
|
+
return uniqueMessages(found).filter((t) => !isBoilerplate(t, status));
|
|
972
|
+
}
|
|
973
|
+
/** Is `value` nothing more than the reason phrase / a generic label for `status`? */
|
|
974
|
+
function isBoilerplate(value, status) {
|
|
975
|
+
const v = value.toLowerCase();
|
|
976
|
+
if (!v || v === "error" || v === "unknown error" || v === "request failed") return true;
|
|
977
|
+
if (/^http\s*\d{3}$/.test(v)) return true;
|
|
978
|
+
if (NETWORK_TEXTS.some((t) => v.includes(t))) return true;
|
|
979
|
+
if (status !== void 0) {
|
|
980
|
+
const phrase = REASON_PHRASES[status]?.toLowerCase();
|
|
981
|
+
if (phrase && (v === phrase || v === `${status} ${phrase}` || v === `${status}`)) return true;
|
|
982
|
+
if (v === String(status)) return true;
|
|
983
|
+
}
|
|
984
|
+
return Object.values(REASON_PHRASES).some((p) => p.toLowerCase() === v);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The one line a field shows for `err`: what the server said, every distinct
|
|
988
|
+
* message combined (`monoFetch` style), or the headline when it said nothing.
|
|
989
|
+
*/
|
|
990
|
+
function errorText(err, separator = " · ") {
|
|
991
|
+
return err.messages.length ? err.messages.join(separator) : err.message;
|
|
992
|
+
}
|
|
993
|
+
/** The hover text: the headline, then everything the server said, each once. */
|
|
994
|
+
function errorTitle(err) {
|
|
995
|
+
return uniqueMessages([err.message, ...err.messages]).join("\n");
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* The line a person should read for `raw`.
|
|
999
|
+
*
|
|
1000
|
+
* Order: a preset for the status (`'network'` for 0) → the error's own text →
|
|
1001
|
+
* `messages.default`. When a preset is the headline, whatever the server said
|
|
1002
|
+
* on top of the status becomes `detail`; without a preset, the raw text IS the
|
|
1003
|
+
* headline and only a distinct server body is a detail. Never throws, never
|
|
1004
|
+
* returns an empty `message`.
|
|
1005
|
+
*/
|
|
1006
|
+
function describeError(raw, messages) {
|
|
1007
|
+
const texts = resolveErrorMessages(messages);
|
|
1008
|
+
const status = errorStatus(raw);
|
|
1009
|
+
const fallback = texts.default || DEFAULT_ERROR_MESSAGES.default;
|
|
1010
|
+
const rawText = normalizeError(raw, fallback);
|
|
1011
|
+
const preset = status === void 0 ? void 0 : status === 0 ? texts.network : texts[status];
|
|
1012
|
+
const said = collectErrorMessages(raw, status).filter((m) => m !== preset);
|
|
1013
|
+
if (preset) {
|
|
1014
|
+
const out = {
|
|
1015
|
+
message: preset,
|
|
1016
|
+
status,
|
|
1017
|
+
messages: said
|
|
1018
|
+
};
|
|
1019
|
+
if (said.length) out.detail = said.join("\n\n");
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
1022
|
+
const message = rawText || fallback;
|
|
1023
|
+
const rest = said.filter((m) => messageKey(m) !== messageKey(message));
|
|
1024
|
+
const out = {
|
|
1025
|
+
message,
|
|
1026
|
+
messages: said
|
|
1027
|
+
};
|
|
1028
|
+
if (status !== void 0) out.status = status;
|
|
1029
|
+
if (rest.length) out.detail = rest.join("\n\n");
|
|
1030
|
+
return out;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* A load a newer load superseded (devextreme rejects it with `'canceled'`).
|
|
1034
|
+
* Not a failure — nothing should be shown for it.
|
|
1035
|
+
*/
|
|
1036
|
+
function isCanceled(err) {
|
|
1037
|
+
if (!err) return false;
|
|
1038
|
+
return err === "canceled" || err.name === "canceled" || err.message === "canceled" || err.__id === "canceled";
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
743
1041
|
//#region src/composables/reactive.ts
|
|
744
1042
|
/**
|
|
745
1043
|
* Unwrap a `ref()` / `computed()` wrapper and a `reactive()` proxy to the raw
|
|
@@ -786,4 +1084,4 @@ function unwrapReactiveDeep(value) {
|
|
|
786
1084
|
return changed ? items : out;
|
|
787
1085
|
}
|
|
788
1086
|
//#endregion
|
|
789
|
-
export {
|
|
1087
|
+
export { compileFilterPredicate as A, isWildcardPattern as C, searchEntryFor as D, resolveSearchEntries as E, andFilters as O, hasWildcardSearch as S, plainSearchColumns as T, searchRowPredicate as _, describeError as a, expandWildcard as b, errorTitle as c, resolveErrorMessages as d, setErrorMessages as f, resolveSearchFields as g, mergeSearchFields as h, DEFAULT_ERROR_MESSAGES as i, joinFilters as j, andPredicates as k, isCanceled as l, MonoSourceSearch as m, unwrapReactive as n, errorStatus as o, DEFAULT_SEARCH_FIELDS as p, unwrapReactiveDeep as r, errorText as s, resolveMaybeReactive as t, normalizeError as u, customPredicate as v, normalizeSearchExpr as w, hasCustomSearch as x, customRemoteClause as y };
|
|
@@ -55,6 +55,7 @@ export declare class MonoCardCoreInterface {
|
|
|
55
55
|
protected get _isInteractive(): boolean;
|
|
56
56
|
protected get _isDisabled(): boolean;
|
|
57
57
|
protected get _hasHeader(): boolean;
|
|
58
|
+
protected get _noSubtitle(): boolean;
|
|
58
59
|
protected get _cardClasses(): string;
|
|
59
60
|
protected _handleClick(event: MouseEvent): void;
|
|
60
61
|
protected _handleKeyDown(event: KeyboardEvent): void;
|
|
@@ -2,6 +2,7 @@ import { CssSizeValue } from '../../composables/css-size';
|
|
|
2
2
|
import { MonoGridSource } from '../table/mono-data-grid.js';
|
|
3
3
|
import { MaybeReactive } from '../../composables/reactive.js';
|
|
4
4
|
import { MonoDataSourceOptions, MonoOdataOptions } from '../../utils/data-source-options.js';
|
|
5
|
+
import { DescribedError } from '../../utils/normalize-error.js';
|
|
5
6
|
/** Chart.js chart types the addon exposes. */
|
|
6
7
|
export type MonoChartType = 'bar' | 'line' | 'pie' | 'doughnut' | 'radar' | 'polarArea' | 'scatter' | 'bubble';
|
|
7
8
|
/** Aggregate applied to a series' values when rows are bucketed by `groupBy`. */
|
|
@@ -200,6 +201,13 @@ export interface MonoChartController<T = any> {
|
|
|
200
201
|
readonly items: T[];
|
|
201
202
|
/** True while the bound source is loading. */
|
|
202
203
|
loading: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* The last load failure, described for a person (`describeError`); null once
|
|
206
|
+
* a load succeeds. The chart element paints it with a ↻ that calls {@link retry}.
|
|
207
|
+
*/
|
|
208
|
+
readonly error: DescribedError | null;
|
|
209
|
+
/** True once the source has answered at least once (or raw data was set). */
|
|
210
|
+
readonly hasLoaded: boolean;
|
|
203
211
|
/** Current chart type. */
|
|
204
212
|
type: MonoChartType;
|
|
205
213
|
/** The bound source, or null. */
|
|
@@ -234,6 +242,8 @@ export interface MonoChartController<T = any> {
|
|
|
234
242
|
reload(): Promise<void>;
|
|
235
243
|
/** Alias of `reload()` — the call to make after the reactive state behind a base getter changed. */
|
|
236
244
|
refresh(): Promise<void>;
|
|
245
|
+
/** `reload()` after a failure; never rejects (the failure lands on {@link error}). */
|
|
246
|
+
retry(): Promise<void>;
|
|
237
247
|
/** Replace `dataSourceOptions` and reload. */
|
|
238
248
|
setDataSourceOptions(next: MaybeReactive<MonoDataSourceOptions> | undefined): Promise<void>;
|
|
239
249
|
/** Replace `odataOptions` and reload. */
|
|
@@ -277,6 +287,15 @@ export interface ChartCssClass {
|
|
|
277
287
|
* itself, so setting them *from* the controller would be circular.
|
|
278
288
|
*/
|
|
279
289
|
export interface MonoChartProps {
|
|
290
|
+
/** Spinner + `loadingText` over the plot — the app is still fetching what it shows. */
|
|
291
|
+
loading?: boolean;
|
|
292
|
+
loadingText?: string;
|
|
293
|
+
'loading-text'?: string;
|
|
294
|
+
/** The app's fetch failed: a string or the error itself. Painted with ↻, which fires `retry`. */
|
|
295
|
+
loadingError?: unknown;
|
|
296
|
+
'loading-error'?: unknown;
|
|
297
|
+
retryLabel?: string;
|
|
298
|
+
'retry-label'?: string;
|
|
280
299
|
/**
|
|
281
300
|
* Chart type. Ignored by the presets, which fix their own. When a controller
|
|
282
301
|
* is bound this overrides the controller's `type`.
|