@fctc/widget-logic 1.8.3 → 1.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +36 -149
- package/dist/index.mjs +42 -160
- package/dist/widget.d.mts +1 -1
- package/dist/widget.d.ts +1 -1
- package/dist/widget.js +30 -149
- package/dist/widget.mjs +35 -160
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6533,35 +6533,22 @@ var tableGroupController = (props) => {
|
|
|
6533
6533
|
// src/widget/advance/search/controller.ts
|
|
6534
6534
|
var import_constants3 = require("@fctc/interface-logic/constants");
|
|
6535
6535
|
var import_utils6 = require("@fctc/interface-logic/utils");
|
|
6536
|
-
var import_moment = __toESM(require_moment());
|
|
6537
6536
|
var import_react18 = require("react");
|
|
6538
|
-
|
|
6539
|
-
// src/provider.ts
|
|
6540
|
-
var provider_exports = {};
|
|
6541
|
-
__reExport(provider_exports, require("@fctc/interface-logic/provider"));
|
|
6542
|
-
|
|
6543
|
-
// src/widget/advance/search/controller.ts
|
|
6544
6537
|
var searchController = ({
|
|
6545
6538
|
viewData,
|
|
6546
6539
|
actionData,
|
|
6547
6540
|
fieldsList,
|
|
6548
|
-
|
|
6541
|
+
contextSearch,
|
|
6542
|
+
setSearchMap,
|
|
6543
|
+
searchMap
|
|
6549
6544
|
}) => {
|
|
6550
|
-
const { env } = (0, provider_exports.useEnv)();
|
|
6551
|
-
const { context } = actionData || {};
|
|
6552
|
-
const actionContext = typeof context === "string" ? (0, import_utils6.evalJSONContext)(context) : context;
|
|
6553
|
-
const contextSearch = { ...env.context, ...actionContext };
|
|
6554
6545
|
const [filterBy, setFilterBy] = (0, import_react18.useState)(null);
|
|
6555
6546
|
const [searchBy, setSearchBy] = (0, import_react18.useState)(null);
|
|
6556
6547
|
const [groupBy, setGroupBy] = (0, import_react18.useState)(null);
|
|
6557
6548
|
const [selectedTags, setSelectedTags] = (0, import_react18.useState)(null);
|
|
6558
6549
|
const [searchString, setSearchString] = (0, import_react18.useState)("");
|
|
6559
|
-
const [searchMap, setSearchMap] = (0, import_react18.useState)({});
|
|
6560
|
-
const [isReadyFormatDomain, setIsReadyFormatDomain] = (0, import_react18.useState)(false);
|
|
6561
|
-
const [didInit, setDidInit] = (0, import_react18.useState)(false);
|
|
6562
6550
|
const aid = actionData?.id;
|
|
6563
6551
|
const model = actionData?.res_model;
|
|
6564
|
-
const domainAction = actionData?.domain ? Array.isArray(actionData?.domain) ? [...actionData?.domain] : (0, import_utils6.evalJSONDomain)(actionData?.domain, contextSearch) : [];
|
|
6565
6552
|
const clearSearch = () => {
|
|
6566
6553
|
setFilterBy([]);
|
|
6567
6554
|
setGroupBy([]);
|
|
@@ -6671,16 +6658,16 @@ var searchController = ({
|
|
|
6671
6658
|
const contexts = [];
|
|
6672
6659
|
let groupValues = [];
|
|
6673
6660
|
objValues?.forEach((objValue) => {
|
|
6674
|
-
const { context
|
|
6661
|
+
const { context, value, active, groupIndex: groupIndex2, isDefault } = objValue;
|
|
6675
6662
|
const indexAppend = groupIndex2 != null ? groupIndex2 : viewData?.views?.search?.filters_by?.length ?? 0;
|
|
6676
6663
|
contexts.push(
|
|
6677
|
-
...Array.isArray(
|
|
6664
|
+
...Array.isArray(context?.group_by) ? context.group_by.map((item) => ({ group_by: item })) : [context]
|
|
6678
6665
|
);
|
|
6679
6666
|
groupValues[indexAppend] = {
|
|
6680
6667
|
contexts: [
|
|
6681
|
-
...Array.isArray(
|
|
6668
|
+
...Array.isArray(context?.group_by) ? context.group_by.map((item) => ({
|
|
6682
6669
|
group_by: item
|
|
6683
|
-
})) : [
|
|
6670
|
+
})) : [context]
|
|
6684
6671
|
],
|
|
6685
6672
|
strings: isDefault ? [value] : [...groupValues[indexAppend]?.strings ?? [], value]
|
|
6686
6673
|
};
|
|
@@ -6710,51 +6697,6 @@ var searchController = ({
|
|
|
6710
6697
|
setSelectedTags(null);
|
|
6711
6698
|
setTagSearch(searchMap);
|
|
6712
6699
|
}, [searchMap]);
|
|
6713
|
-
const formatDomain = () => {
|
|
6714
|
-
if (domainAction) {
|
|
6715
|
-
const domain = [];
|
|
6716
|
-
if (domainAction?.length > 0) {
|
|
6717
|
-
if (Object.keys(searchMap).length > 0) {
|
|
6718
|
-
domain.push("&");
|
|
6719
|
-
}
|
|
6720
|
-
domainAction.forEach((domainItem) => {
|
|
6721
|
-
domain.push(domainItem);
|
|
6722
|
-
});
|
|
6723
|
-
}
|
|
6724
|
-
Object.keys(searchMap).forEach((key, keyIndex, keys) => {
|
|
6725
|
-
if (!key?.includes(import_constants3.SearchType.GROUP)) {
|
|
6726
|
-
if (keys.length > 1 && keyIndex < keys.length - 1) {
|
|
6727
|
-
domain.push("&");
|
|
6728
|
-
}
|
|
6729
|
-
const valuesOfKey = searchMap[key];
|
|
6730
|
-
valuesOfKey.forEach((value, index) => {
|
|
6731
|
-
if (index < valuesOfKey.length - 1) {
|
|
6732
|
-
domain.push("|");
|
|
6733
|
-
}
|
|
6734
|
-
if (value.domain) {
|
|
6735
|
-
domain.push(...value.domain);
|
|
6736
|
-
return;
|
|
6737
|
-
}
|
|
6738
|
-
let valueDomainItem = value?.value;
|
|
6739
|
-
if (value?.modelType === "date") {
|
|
6740
|
-
valueDomainItem = (0, import_utils6.validateAndParseDate)(value?.value);
|
|
6741
|
-
} else if (value?.modelType === "datetime") {
|
|
6742
|
-
if (value?.operator === "<=" || value?.operator === "<") {
|
|
6743
|
-
const parsedDate = (0, import_utils6.validateAndParseDate)(value?.value, true);
|
|
6744
|
-
const hasTime = (0, import_moment.default)(value?.value).format("HH:mm:ss") !== "00:00:00";
|
|
6745
|
-
valueDomainItem = hasTime ? (0, import_moment.default)(parsedDate).format("YYYY-MM-DD HH:mm:ss") : (0, import_moment.default)(parsedDate).add(1, "day").subtract(1, "second").format("YYYY-MM-DD HH:mm:ss");
|
|
6746
|
-
} else {
|
|
6747
|
-
valueDomainItem = (0, import_utils6.validateAndParseDate)(value?.value, true);
|
|
6748
|
-
}
|
|
6749
|
-
}
|
|
6750
|
-
const operator = value?.modelType === "date" || value?.modelType === "datetime" || value?.modelType === "boolean" || value?.modelType === "integer" ? value?.operator ?? "=" : value.operator ?? "ilike";
|
|
6751
|
-
domain.push([value.name, operator, valueDomainItem]);
|
|
6752
|
-
});
|
|
6753
|
-
}
|
|
6754
|
-
});
|
|
6755
|
-
return [...domain];
|
|
6756
|
-
}
|
|
6757
|
-
};
|
|
6758
6700
|
const handleAddTagSearch = (tag) => {
|
|
6759
6701
|
const {
|
|
6760
6702
|
domain,
|
|
@@ -6762,7 +6704,7 @@ var searchController = ({
|
|
|
6762
6704
|
value,
|
|
6763
6705
|
type,
|
|
6764
6706
|
title,
|
|
6765
|
-
context
|
|
6707
|
+
context,
|
|
6766
6708
|
active,
|
|
6767
6709
|
dataIndex
|
|
6768
6710
|
} = tag;
|
|
@@ -6770,13 +6712,13 @@ var searchController = ({
|
|
|
6770
6712
|
if (type === import_constants3.SearchType.FILTER) {
|
|
6771
6713
|
addSearchItems(`${import_constants3.SearchType.FILTER}_${groupIndex}`, {
|
|
6772
6714
|
...tag,
|
|
6773
|
-
domain: domain ? domainFormat.toList(
|
|
6715
|
+
domain: domain ? domainFormat.toList(context) : null
|
|
6774
6716
|
});
|
|
6775
6717
|
} else if (type === import_constants3.SearchType.SEARCH) {
|
|
6776
6718
|
addSearchItems(`${import_constants3.SearchType.SEARCH}_${String(dataIndex)}`, {
|
|
6777
6719
|
...tag,
|
|
6778
6720
|
domain: domain ? domainFormat.toList({
|
|
6779
|
-
...
|
|
6721
|
+
...context,
|
|
6780
6722
|
self: value
|
|
6781
6723
|
}) : null
|
|
6782
6724
|
});
|
|
@@ -6784,73 +6726,12 @@ var searchController = ({
|
|
|
6784
6726
|
addSearchItems(`${import_constants3.SearchType.GROUP}`, {
|
|
6785
6727
|
...tag,
|
|
6786
6728
|
domain: domain ? domainFormat.toList({
|
|
6787
|
-
context
|
|
6729
|
+
context,
|
|
6788
6730
|
self: value
|
|
6789
6731
|
}) : null
|
|
6790
6732
|
});
|
|
6791
6733
|
}
|
|
6792
6734
|
};
|
|
6793
|
-
(0, import_react18.useEffect)(() => {
|
|
6794
|
-
if (isReadyFormatDomain) {
|
|
6795
|
-
(0, store_exports.setPage)(0);
|
|
6796
|
-
(0, store_exports.setSelectedRowKeys)([]);
|
|
6797
|
-
const containSearchFilter = selectedTags?.length > 0 && selectedTags?.find(
|
|
6798
|
-
(item) => item?.type === import_constants3.SearchType.FILTER || item?.type === import_constants3.SearchType.SEARCH || item?.type === import_constants3.SearchType.GROUP
|
|
6799
|
-
);
|
|
6800
|
-
if (containSearchFilter || Array.isArray(selectedTags) && selectedTags?.length === 0) {
|
|
6801
|
-
setDomain(formatDomain());
|
|
6802
|
-
}
|
|
6803
|
-
}
|
|
6804
|
-
return () => {
|
|
6805
|
-
setDidInit(false);
|
|
6806
|
-
setIsReadyFormatDomain(false);
|
|
6807
|
-
};
|
|
6808
|
-
}, [selectedTags, isReadyFormatDomain]);
|
|
6809
|
-
(0, import_react18.useEffect)(() => {
|
|
6810
|
-
if (didInit || selectedTags?.length > 0 || !fieldsList || fieldsList?.length === 0)
|
|
6811
|
-
return;
|
|
6812
|
-
const searchDefaults = Object.entries(actionContext || {}).filter(
|
|
6813
|
-
([key]) => key.startsWith("search_default_")
|
|
6814
|
-
);
|
|
6815
|
-
const hasGroupBy = viewData?.views?.search?.filters_by?.length > 0;
|
|
6816
|
-
if (searchDefaults.length === 0 && !hasGroupBy) {
|
|
6817
|
-
setIsReadyFormatDomain(true);
|
|
6818
|
-
setDidInit(true);
|
|
6819
|
-
return;
|
|
6820
|
-
}
|
|
6821
|
-
const updatedFilter = filterBy?.map((item) => {
|
|
6822
|
-
const matched = searchDefaults.find(
|
|
6823
|
-
([key]) => key.split("search_default_")[1] === item.name
|
|
6824
|
-
);
|
|
6825
|
-
if (matched && !item.active) {
|
|
6826
|
-
handleAddTagSearch?.({
|
|
6827
|
-
name: item?.name,
|
|
6828
|
-
value: item?.string ?? item?.help,
|
|
6829
|
-
domain: item?.domain,
|
|
6830
|
-
groupIndex: item?.group_index,
|
|
6831
|
-
type: import_constants3.SearchType.FILTER
|
|
6832
|
-
});
|
|
6833
|
-
return { ...item, active: true };
|
|
6834
|
-
}
|
|
6835
|
-
return item;
|
|
6836
|
-
});
|
|
6837
|
-
if (updatedFilter) setFilterBy(updatedFilter);
|
|
6838
|
-
if (hasGroupBy) {
|
|
6839
|
-
viewData?.views?.search?.filters_by?.forEach((item, idx) => {
|
|
6840
|
-
const groupCtx = (0, import_utils6.evalJSONContext)(item?.context);
|
|
6841
|
-
handleAddTagSearch?.({
|
|
6842
|
-
name: item?.name,
|
|
6843
|
-
value: item?.display_name,
|
|
6844
|
-
type: import_constants3.SearchType.GROUP,
|
|
6845
|
-
context: groupCtx,
|
|
6846
|
-
groupIndex: idx,
|
|
6847
|
-
isDefault: true
|
|
6848
|
-
});
|
|
6849
|
-
});
|
|
6850
|
-
setDidInit(true);
|
|
6851
|
-
}
|
|
6852
|
-
setIsReadyFormatDomain(true);
|
|
6853
|
-
}, [aid, fieldsList]);
|
|
6854
6735
|
return {
|
|
6855
6736
|
groupBy,
|
|
6856
6737
|
searchBy,
|
|
@@ -6870,7 +6751,7 @@ var searchController = ({
|
|
|
6870
6751
|
|
|
6871
6752
|
// src/widget/basic/many2many-field/controller.ts
|
|
6872
6753
|
var import_environment8 = require("@fctc/interface-logic/environment");
|
|
6873
|
-
var
|
|
6754
|
+
var import_store12 = require("@fctc/interface-logic/store");
|
|
6874
6755
|
var import_utils7 = require("@fctc/interface-logic/utils");
|
|
6875
6756
|
var many2manyFieldController = (props) => {
|
|
6876
6757
|
const {
|
|
@@ -6880,7 +6761,7 @@ var many2manyFieldController = (props) => {
|
|
|
6880
6761
|
tab,
|
|
6881
6762
|
model,
|
|
6882
6763
|
aid,
|
|
6883
|
-
setSelectedRowKeys:
|
|
6764
|
+
setSelectedRowKeys: setSelectedRowKeys4,
|
|
6884
6765
|
fields,
|
|
6885
6766
|
setFields,
|
|
6886
6767
|
groupByDomain,
|
|
@@ -6888,14 +6769,14 @@ var many2manyFieldController = (props) => {
|
|
|
6888
6769
|
options,
|
|
6889
6770
|
sessionStorageUtils
|
|
6890
6771
|
} = props;
|
|
6891
|
-
const appDispatch = (0,
|
|
6772
|
+
const appDispatch = (0, import_store12.useAppDispatch)();
|
|
6892
6773
|
const actionData = sessionStorageUtils.getActionData();
|
|
6893
6774
|
const [debouncedPage] = useDebounce(page, 500);
|
|
6894
6775
|
const [order, setOrder] = (0, import_react19.useState)();
|
|
6895
6776
|
const [isLoadedData, setIsLoadedData] = (0, import_react19.useState)(false);
|
|
6896
6777
|
const [domainMany2Many, setDomainMany2Many] = (0, import_react19.useState)(domain);
|
|
6897
6778
|
const env = (0, import_environment8.getEnv)();
|
|
6898
|
-
const { selectedTags } = (0,
|
|
6779
|
+
const { selectedTags } = (0, import_store12.useAppSelector)(import_store12.selectSearch);
|
|
6899
6780
|
const viewParams = {
|
|
6900
6781
|
model: relation,
|
|
6901
6782
|
views: [
|
|
@@ -6938,8 +6819,8 @@ var many2manyFieldController = (props) => {
|
|
|
6938
6819
|
const fetchData = async () => {
|
|
6939
6820
|
try {
|
|
6940
6821
|
setDomainMany2Many(domain);
|
|
6941
|
-
appDispatch((0,
|
|
6942
|
-
appDispatch((0,
|
|
6822
|
+
appDispatch((0, import_store12.setFirstDomain)(domain));
|
|
6823
|
+
appDispatch((0, import_store12.setViewDataStore)(viewResponse));
|
|
6943
6824
|
const modalData = viewResponse?.views?.list?.fields.map((field) => ({
|
|
6944
6825
|
...viewResponse?.models?.[String(model)]?.[field?.name],
|
|
6945
6826
|
...field
|
|
@@ -6950,7 +6831,7 @@ var many2manyFieldController = (props) => {
|
|
|
6950
6831
|
[`${aid}_${relation}_popupmany2many`]: modalData
|
|
6951
6832
|
});
|
|
6952
6833
|
}
|
|
6953
|
-
appDispatch((0,
|
|
6834
|
+
appDispatch((0, import_store12.setPage)(0));
|
|
6954
6835
|
} catch (err) {
|
|
6955
6836
|
console.log(err);
|
|
6956
6837
|
}
|
|
@@ -6986,13 +6867,13 @@ var many2manyFieldController = (props) => {
|
|
|
6986
6867
|
fetchData();
|
|
6987
6868
|
}
|
|
6988
6869
|
return () => {
|
|
6989
|
-
appDispatch((0,
|
|
6870
|
+
appDispatch((0, import_store12.setGroupByDomain)(null));
|
|
6990
6871
|
setFields((prevFields) => ({
|
|
6991
6872
|
...prevFields,
|
|
6992
6873
|
[`${aid}_${relation}_popupmany2many`]: null
|
|
6993
6874
|
}));
|
|
6994
|
-
appDispatch((0,
|
|
6995
|
-
|
|
6875
|
+
appDispatch((0, import_store12.setPage)(0));
|
|
6876
|
+
setSelectedRowKeys4([]);
|
|
6996
6877
|
setDomainMany2Many(null);
|
|
6997
6878
|
setIsLoadedData(false);
|
|
6998
6879
|
};
|
|
@@ -7104,7 +6985,7 @@ var many2manyTagsController = (props) => {
|
|
|
7104
6985
|
// src/widget/basic/status-bar-field/controller.ts
|
|
7105
6986
|
var import_react21 = require("react");
|
|
7106
6987
|
var import_hooks16 = require("@fctc/interface-logic/hooks");
|
|
7107
|
-
var
|
|
6988
|
+
var import_store13 = require("@fctc/interface-logic/store");
|
|
7108
6989
|
var import_utils9 = require("@fctc/interface-logic/utils");
|
|
7109
6990
|
var durationController = (props) => {
|
|
7110
6991
|
const {
|
|
@@ -7124,7 +7005,7 @@ var durationController = (props) => {
|
|
|
7124
7005
|
};
|
|
7125
7006
|
const [disabled, setDisabled] = (0, import_react21.useState)(false);
|
|
7126
7007
|
const [modelStatus, setModalStatus] = (0, import_react21.useState)(false);
|
|
7127
|
-
const { context } = (0,
|
|
7008
|
+
const { context } = (0, import_store13.useAppSelector)(import_store13.selectEnv);
|
|
7128
7009
|
const queryKey = [`data-status-duration`, specification];
|
|
7129
7010
|
const listDataProps = {
|
|
7130
7011
|
model: relation,
|
|
@@ -7495,7 +7376,7 @@ var downLoadBinaryController = (props) => {
|
|
|
7495
7376
|
};
|
|
7496
7377
|
|
|
7497
7378
|
// src/widget/basic/date-field/controller.ts
|
|
7498
|
-
var
|
|
7379
|
+
var import_moment = __toESM(require_moment());
|
|
7499
7380
|
var DURATIONS = {
|
|
7500
7381
|
PAST: "past",
|
|
7501
7382
|
NOW: "now",
|
|
@@ -7522,8 +7403,8 @@ var dateFieldController = (props) => {
|
|
|
7522
7403
|
const formatDate = showTime ? "DD/MM/YYYY HH:mm:ss" : "DD/MM/YYYY";
|
|
7523
7404
|
const formatDateParse = showTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD";
|
|
7524
7405
|
const fieldForCustom = widget === "datetime_custom" || widget === "date_custom";
|
|
7525
|
-
const minNowValue = fieldForCustom && (min === DURATIONS.NOW ? true : typeof min === "string" && Object.keys(formValues)?.includes(min) && formValues?.[min] ? (0,
|
|
7526
|
-
const maxNowValue = fieldForCustom && (max === DURATIONS.NOW ? true : typeof max === "string" && Object.keys(formValues)?.includes(max) && formValues?.[max] ? (0,
|
|
7406
|
+
const minNowValue = fieldForCustom && (min === DURATIONS.NOW ? true : typeof min === "string" && Object.keys(formValues)?.includes(min) && formValues?.[min] ? (0, import_moment.default)(formValues?.[min], formatDateParse).add(7, "hours") : null);
|
|
7407
|
+
const maxNowValue = fieldForCustom && (max === DURATIONS.NOW ? true : typeof max === "string" && Object.keys(formValues)?.includes(max) && formValues?.[max] ? (0, import_moment.default)(formValues?.[max], formatDateParse).add(7, "hours") : null);
|
|
7527
7408
|
const years = range(
|
|
7528
7409
|
minNowValue ? (/* @__PURE__ */ new Date()).getFullYear() : 1990,
|
|
7529
7410
|
(/* @__PURE__ */ new Date()).getFullYear() + 4,
|
|
@@ -7558,8 +7439,8 @@ var dateFieldController = (props) => {
|
|
|
7558
7439
|
"December"
|
|
7559
7440
|
];
|
|
7560
7441
|
const customValidateMinMax = (date) => {
|
|
7561
|
-
const selected = (0,
|
|
7562
|
-
const now = (0,
|
|
7442
|
+
const selected = (0, import_moment.default)(date, formatDateParse);
|
|
7443
|
+
const now = (0, import_moment.default)();
|
|
7563
7444
|
const compareSelected = showTime ? selected : selected.clone().startOf("day");
|
|
7564
7445
|
const compareNow = showTime ? now : now.clone().startOf("day");
|
|
7565
7446
|
if (minNowValue) {
|
|
@@ -7567,7 +7448,7 @@ var dateFieldController = (props) => {
|
|
|
7567
7448
|
return `${i18n_default.t("please_enter")} ${string} ${i18n_default.t(
|
|
7568
7449
|
"greater_or_equal_now"
|
|
7569
7450
|
)}`;
|
|
7570
|
-
} else if (
|
|
7451
|
+
} else if (import_moment.default.isMoment(minNowValue)) {
|
|
7571
7452
|
const compareMin = showTime ? minNowValue : minNowValue.clone().startOf("day");
|
|
7572
7453
|
if (compareSelected.isBefore(compareMin)) {
|
|
7573
7454
|
const fieldRelationDate = viewData?.models?.[model]?.[min ?? ""];
|
|
@@ -7581,7 +7462,7 @@ var dateFieldController = (props) => {
|
|
|
7581
7462
|
return `${i18n_default.t("please_enter")} ${string} ${i18n_default.t(
|
|
7582
7463
|
"less_or_equal_now"
|
|
7583
7464
|
)}`;
|
|
7584
|
-
} else if (
|
|
7465
|
+
} else if (import_moment.default.isMoment(maxNowValue)) {
|
|
7585
7466
|
const compareMax = showTime ? maxNowValue : maxNowValue.clone().startOf("day");
|
|
7586
7467
|
if (compareSelected.isAfter(compareMax)) {
|
|
7587
7468
|
const fieldRelationDate = viewData?.models?.[model]?.[max ?? ""];
|
|
@@ -7787,6 +7668,12 @@ __reExport(constants_exports, require("@fctc/interface-logic/constants"));
|
|
|
7787
7668
|
// src/index.ts
|
|
7788
7669
|
__reExport(index_exports, constants_exports, module.exports);
|
|
7789
7670
|
__reExport(index_exports, environment_exports, module.exports);
|
|
7671
|
+
|
|
7672
|
+
// src/provider.ts
|
|
7673
|
+
var provider_exports = {};
|
|
7674
|
+
__reExport(provider_exports, require("@fctc/interface-logic/provider"));
|
|
7675
|
+
|
|
7676
|
+
// src/index.ts
|
|
7790
7677
|
__reExport(index_exports, provider_exports, module.exports);
|
|
7791
7678
|
|
|
7792
7679
|
// src/services.ts
|