@jskit-ai/users-web 0.1.59 → 0.1.61

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.
@@ -3,7 +3,7 @@ import { HOME_TOOLS_OUTLET } from "./src/shared/toolsOutletContracts.js";
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/users-web",
6
- version: "0.1.59",
6
+ version: "0.1.61",
7
7
  kind: "runtime",
8
8
  description: "Users web module: account/profile UI plus shared users web widgets.",
9
9
  dependsOn: [
@@ -66,6 +66,14 @@ export default Object.freeze({
66
66
  subpath: "./client/composables/useCommand",
67
67
  summary: "Exports command operation composable."
68
68
  },
69
+ {
70
+ subpath: "./client/composables/useEndpointResource",
71
+ summary: "Exports low-level endpoint resource composable for custom client requests."
72
+ },
73
+ {
74
+ subpath: "./client/composables/useCrudListFilterLookups",
75
+ summary: "Exports lookup-backed CRUD list filter helper for remote autocomplete filters."
76
+ },
69
77
  {
70
78
  subpath: "./client/composables/useView",
71
79
  summary: "Exports read/view operation composable."
@@ -78,6 +86,10 @@ export default Object.freeze({
78
86
  subpath: "./client/composables/usePaths",
79
87
  summary: "Exports surface route path resolver composable."
80
88
  },
89
+ {
90
+ subpath: "./client/lib/httpClient",
91
+ summary: "Exports the shared users-web HTTP client with credentials and CSRF behavior."
92
+ },
81
93
  {
82
94
  subpath: "./client/composables/useAccountSettingsRuntime",
83
95
  summary: "Exports account settings runtime composable for app-owned settings UI."
@@ -142,12 +154,12 @@ export default Object.freeze({
142
154
  runtime: {
143
155
  "@tanstack/vue-query": "5.92.12",
144
156
  "@mdi/js": "^7.4.47",
145
- "@jskit-ai/http-runtime": "0.1.43",
146
- "@jskit-ai/realtime": "0.1.43",
147
- "@jskit-ai/kernel": "0.1.44",
148
- "@jskit-ai/shell-web": "0.1.43",
149
- "@jskit-ai/uploads-image-web": "0.1.22",
150
- "@jskit-ai/users-core": "0.1.54",
157
+ "@jskit-ai/http-runtime": "0.1.45",
158
+ "@jskit-ai/realtime": "0.1.45",
159
+ "@jskit-ai/kernel": "0.1.46",
160
+ "@jskit-ai/shell-web": "0.1.45",
161
+ "@jskit-ai/uploads-image-web": "0.1.24",
162
+ "@jskit-ai/users-core": "0.1.56",
151
163
  vuetify: "^4.0.0"
152
164
  },
153
165
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -13,6 +13,9 @@
13
13
  "./client/composables/useCommand": "./src/client/composables/useCommand.js",
14
14
  "./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
15
15
  "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
16
+ "./client/composables/useCrudListFilterLookups": "./src/client/composables/useCrudListFilterLookups.js",
17
+ "./client/composables/useCrudListFilters": "./src/client/composables/useCrudListFilters.js",
18
+ "./client/composables/useEndpointResource": "./src/client/composables/runtime/useEndpointResource.js",
16
19
  "./client/composables/useList": "./src/client/composables/records/useList.js",
17
20
  "./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
18
21
  "./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
@@ -24,6 +27,7 @@
24
27
  "./client/composables/useAccountSettingsRuntime": "./src/client/composables/useAccountSettingsRuntime.js",
25
28
  "./client/composables/usePaths": "./src/client/composables/usePaths.js",
26
29
  "./client/composables/runtime/useUiFeedback": "./src/client/composables/runtime/useUiFeedback.js",
30
+ "./client/lib/httpClient": "./src/client/lib/httpClient.js",
27
31
  "./client/lib/bootstrap": "./src/client/lib/bootstrap.js",
28
32
  "./client/lib/permissions": "./src/client/lib/permissions.js",
29
33
  "./client/support/contractGuards": "./src/client/support/contractGuards.js"
@@ -31,12 +35,12 @@
31
35
  "dependencies": {
32
36
  "@tanstack/vue-query": "5.92.12",
33
37
  "@mdi/js": "^7.4.47",
34
- "@jskit-ai/http-runtime": "0.1.43",
35
- "@jskit-ai/kernel": "0.1.44",
36
- "@jskit-ai/realtime": "0.1.43",
37
- "@jskit-ai/shell-web": "0.1.43",
38
- "@jskit-ai/uploads-image-web": "0.1.22",
39
- "@jskit-ai/users-core": "0.1.54",
38
+ "@jskit-ai/http-runtime": "0.1.45",
39
+ "@jskit-ai/kernel": "0.1.46",
40
+ "@jskit-ai/realtime": "0.1.45",
41
+ "@jskit-ai/shell-web": "0.1.45",
42
+ "@jskit-ai/uploads-image-web": "0.1.24",
43
+ "@jskit-ai/users-core": "0.1.56",
40
44
  "vuetify": "^4.0.0"
41
45
  }
42
46
  }
@@ -48,6 +48,10 @@ function resolveFormFieldFormat(field = {}) {
48
48
  return String(field.format || "").trim().toLowerCase();
49
49
  }
50
50
 
51
+ function isNullableFormField(field = {}) {
52
+ return field?.nullable === true;
53
+ }
54
+
51
55
  function padDateTimePart(value) {
52
56
  return String(value).padStart(2, "0");
53
57
  }
@@ -132,7 +136,7 @@ function resolveFormFieldInitialValue(field = {}) {
132
136
 
133
137
  const fieldType = resolveFormFieldType(field);
134
138
  if (fieldType === "boolean") {
135
- return false;
139
+ return isNullableFormField(field) ? null : false;
136
140
  }
137
141
 
138
142
  return "";
@@ -176,7 +180,9 @@ function buildCrudFormPayload(fields = [], model = {}) {
176
180
  const rawValue = sourceModel[fieldKey];
177
181
 
178
182
  if (fieldType === "boolean") {
179
- payload[fieldKey] = Boolean(rawValue);
183
+ payload[fieldKey] = rawValue == null && isNullableFormField(field)
184
+ ? null
185
+ : Boolean(rawValue);
180
186
  continue;
181
187
  }
182
188
 
@@ -258,7 +264,9 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
258
264
  const rawValue = sourcePayload[fieldKey];
259
265
 
260
266
  if (fieldType === "boolean") {
261
- targetModel[fieldKey] = Boolean(rawValue);
267
+ targetModel[fieldKey] = rawValue == null && isNullableFormField(field)
268
+ ? null
269
+ : Boolean(rawValue);
262
270
  continue;
263
271
  }
264
272
 
@@ -0,0 +1,161 @@
1
+ import { normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { resolveLookupItemLabel } from "../crud/crudLookupFieldLabelSupport.js";
3
+ import { asPlainObject } from "../support/scopeHelpers.js";
4
+
5
+ function normalizeLookupQueryKeyPrefix(value = []) {
6
+ const source = Array.isArray(value) ? value : [];
7
+ return Object.freeze(
8
+ source
9
+ .map((entry) => normalizeText(entry))
10
+ .filter(Boolean)
11
+ );
12
+ }
13
+
14
+ function normalizeLookupLabelResolverMap(value = {}) {
15
+ const source = value && typeof value === "object" && !Array.isArray(value)
16
+ ? value
17
+ : {};
18
+ const normalized = {};
19
+
20
+ for (const [key, entry] of Object.entries(source)) {
21
+ const normalizedKey = normalizeText(key);
22
+ if (!normalizedKey || typeof entry !== "function") {
23
+ continue;
24
+ }
25
+
26
+ normalized[normalizedKey] = entry;
27
+ }
28
+
29
+ return Object.freeze(normalized);
30
+ }
31
+
32
+ function normalizeLookupRequestQueryParamsMap(value = {}) {
33
+ const source = value && typeof value === "object" && !Array.isArray(value)
34
+ ? value
35
+ : {};
36
+ const normalized = {};
37
+
38
+ for (const [key, entry] of Object.entries(source)) {
39
+ const normalizedKey = normalizeText(key);
40
+ if (!normalizedKey) {
41
+ continue;
42
+ }
43
+ if (entry == null) {
44
+ continue;
45
+ }
46
+ if (typeof entry === "function") {
47
+ normalized[normalizedKey] = entry;
48
+ continue;
49
+ }
50
+ if (typeof entry !== "object" || Array.isArray(entry)) {
51
+ continue;
52
+ }
53
+
54
+ normalized[normalizedKey] = asPlainObject(entry);
55
+ }
56
+
57
+ return Object.freeze(normalized);
58
+ }
59
+
60
+ function resolveLookupSelectedValues(filter = {}, rawValue = undefined) {
61
+ if (filter?.type === "recordIdMany") {
62
+ return normalizeUniqueTextList(rawValue, {
63
+ acceptSingle: true
64
+ });
65
+ }
66
+
67
+ const normalizedValue = normalizeText(rawValue);
68
+ return normalizedValue ? Object.freeze([normalizedValue]) : Object.freeze([]);
69
+ }
70
+
71
+ function createLookupOptionFromItem(item = {}, filter = {}, labelResolver = null) {
72
+ const sourceRecord = asPlainObject(item);
73
+ const valueKey = normalizeText(filter?.lookup?.valueKey) || "id";
74
+ const labelKey = normalizeText(filter?.lookup?.labelKey);
75
+ const value = normalizeText(sourceRecord[valueKey]);
76
+ if (!value) {
77
+ return null;
78
+ }
79
+
80
+ const customLabel = typeof labelResolver === "function"
81
+ ? normalizeText(labelResolver(sourceRecord, filter))
82
+ : "";
83
+ const fallbackLabel = resolveLookupItemLabel(sourceRecord, labelKey) || value;
84
+
85
+ return Object.freeze({
86
+ value,
87
+ label: customLabel || String(fallbackLabel || value),
88
+ record: sourceRecord
89
+ });
90
+ }
91
+
92
+ function createLookupOptionsFromItems(items = [], filter = {}, labelResolver = null) {
93
+ const optionMap = new Map();
94
+ for (const item of Array.isArray(items) ? items : []) {
95
+ const option = createLookupOptionFromItem(item, filter, labelResolver);
96
+ if (!option || optionMap.has(option.value)) {
97
+ continue;
98
+ }
99
+
100
+ optionMap.set(option.value, option);
101
+ }
102
+
103
+ return Object.freeze([...optionMap.values()]);
104
+ }
105
+
106
+ function mergeSelectedLookupOptions(options = [], selectedValues = [], cachedOptions = new Map()) {
107
+ const optionMap = new Map(
108
+ (Array.isArray(options) ? options : []).map((option) => [normalizeText(option?.value), option])
109
+ );
110
+ const normalizedCache = cachedOptions instanceof Map ? cachedOptions : new Map();
111
+
112
+ for (const value of Array.isArray(selectedValues) ? selectedValues : []) {
113
+ const normalizedValue = normalizeText(value);
114
+ if (!normalizedValue || optionMap.has(normalizedValue)) {
115
+ continue;
116
+ }
117
+
118
+ const cachedOption = normalizedCache.get(normalizedValue);
119
+ if (cachedOption) {
120
+ optionMap.set(normalizedValue, cachedOption);
121
+ }
122
+ }
123
+
124
+ return Object.freeze(
125
+ [...optionMap.values()].sort((left, right) => {
126
+ return String(left?.label || "").localeCompare(String(right?.label || ""));
127
+ })
128
+ );
129
+ }
130
+
131
+ function resolveLookupOptionLabel(options = [], cachedOptions = new Map(), value = "", fallback = "Option") {
132
+ const normalizedValue = normalizeText(value);
133
+ if (!normalizedValue) {
134
+ return normalizeText(fallback) || "Option";
135
+ }
136
+
137
+ const matchingOption = (Array.isArray(options) ? options : [])
138
+ .find((option) => normalizeText(option?.value) === normalizedValue);
139
+ if (matchingOption?.label) {
140
+ return String(matchingOption.label);
141
+ }
142
+
143
+ const normalizedCache = cachedOptions instanceof Map ? cachedOptions : new Map();
144
+ const cachedOption = normalizedCache.get(normalizedValue);
145
+ if (cachedOption?.label) {
146
+ return String(cachedOption.label);
147
+ }
148
+
149
+ const normalizedFallback = normalizeText(fallback) || "Option";
150
+ return `${normalizedFallback} ${normalizedValue}`;
151
+ }
152
+
153
+ export {
154
+ normalizeLookupQueryKeyPrefix,
155
+ normalizeLookupLabelResolverMap,
156
+ normalizeLookupRequestQueryParamsMap,
157
+ resolveLookupSelectedValues,
158
+ createLookupOptionsFromItems,
159
+ mergeSelectedLookupOptions,
160
+ resolveLookupOptionLabel
161
+ };
@@ -0,0 +1,141 @@
1
+ import { computed, proxyRefs, ref, watch } from "vue";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ defineCrudListFilters,
5
+ CRUD_LIST_FILTER_TYPE_RECORD_ID,
6
+ CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY
7
+ } from "@jskit-ai/kernel/shared/support/crudListFilters";
8
+ import { useList } from "./records/useList.js";
9
+ import {
10
+ normalizeLookupQueryKeyPrefix,
11
+ normalizeLookupLabelResolverMap,
12
+ normalizeLookupRequestQueryParamsMap,
13
+ resolveLookupSelectedValues,
14
+ createLookupOptionsFromItems,
15
+ mergeSelectedLookupOptions,
16
+ resolveLookupOptionLabel
17
+ } from "./internal/crudListFilterLookupSupport.js";
18
+
19
+ function useCrudListFilterLookups(
20
+ definitions = {},
21
+ {
22
+ values = {},
23
+ adapter = null,
24
+ recordIdParam = "recordId",
25
+ queryKeyPrefix = [],
26
+ placementSourcePrefix = "",
27
+ requestQueryParams = {},
28
+ labelResolvers = {}
29
+ } = {}
30
+ ) {
31
+ const filters = defineCrudListFilters(definitions);
32
+ const filterEntries = Object.values(filters);
33
+ const normalizedQueryKeyPrefix = normalizeLookupQueryKeyPrefix(queryKeyPrefix);
34
+ const normalizedPlacementSourcePrefix = normalizeText(placementSourcePrefix);
35
+ const normalizedLabelResolvers = normalizeLookupLabelResolverMap(labelResolvers);
36
+ const normalizedRequestQueryParams = normalizeLookupRequestQueryParamsMap(requestQueryParams);
37
+ const lookups = {};
38
+
39
+ for (const filter of filterEntries) {
40
+ if (
41
+ filter.type !== CRUD_LIST_FILTER_TYPE_RECORD_ID &&
42
+ filter.type !== CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY
43
+ ) {
44
+ continue;
45
+ }
46
+ if (!filter.lookup?.apiSuffix) {
47
+ continue;
48
+ }
49
+
50
+ const runtime = useList({
51
+ adapter: adapter || undefined,
52
+ apiSuffix: filter.lookup.apiSuffix,
53
+ queryKeyFactory: (surfaceId = "", scopeParamValue = "") => [
54
+ ...normalizedQueryKeyPrefix,
55
+ filter.key,
56
+ String(surfaceId || ""),
57
+ String(scopeParamValue || "")
58
+ ],
59
+ search: {
60
+ enabled: true,
61
+ mode: "query"
62
+ },
63
+ ...(Object.hasOwn(normalizedRequestQueryParams, filter.key)
64
+ ? { requestQueryParams: normalizedRequestQueryParams[filter.key] }
65
+ : {}),
66
+ placementSource: normalizedPlacementSourcePrefix
67
+ ? `${normalizedPlacementSourcePrefix}.${filter.key}`
68
+ : `crud.list-filter.lookup.${filter.key}`,
69
+ fallbackLoadError: `Unable to load filter options (${filter.lookup.apiSuffix}).`,
70
+ recordIdParam,
71
+ recordIdSelector: (item = {}) => item[filter.lookup.valueKey || "id"],
72
+ viewUrlTemplate: "",
73
+ editUrlTemplate: ""
74
+ });
75
+
76
+ const cachedOptions = ref(new Map());
77
+ const selectedValues = computed(() => resolveLookupSelectedValues(filter, values?.[filter.key]));
78
+ const currentOptions = computed(() => createLookupOptionsFromItems(
79
+ Array.isArray(runtime.items) ? runtime.items : [],
80
+ filter,
81
+ normalizedLabelResolvers[filter.key]
82
+ ));
83
+ const options = computed(() => {
84
+ return mergeSelectedLookupOptions(
85
+ currentOptions.value,
86
+ selectedValues.value,
87
+ cachedOptions.value
88
+ );
89
+ });
90
+
91
+ watch(currentOptions, (nextOptions) => {
92
+ const nextCache = new Map(cachedOptions.value);
93
+ for (const option of nextOptions) {
94
+ nextCache.set(option.value, option);
95
+ }
96
+ cachedOptions.value = nextCache;
97
+ }, { immediate: true });
98
+
99
+ lookups[filter.key] = proxyRefs({
100
+ filter,
101
+ options,
102
+ searchQuery: computed(() => String(runtime.searchQuery || "")),
103
+ isLoading: computed(() => {
104
+ return Boolean(runtime.isInitialLoading || runtime.isFetching || runtime.isRefetching || runtime.isSearchDebouncing);
105
+ }),
106
+ resolveLabel(value, fallback = filter.label || "Option") {
107
+ return resolveLookupOptionLabel(options.value, cachedOptions.value, value, fallback);
108
+ },
109
+ setSearch(value = "") {
110
+ runtime.searchQuery = normalizeText(value);
111
+ }
112
+ });
113
+ }
114
+
115
+ function resolveLookup(filterKey = "") {
116
+ return lookups[normalizeText(filterKey)] || null;
117
+ }
118
+
119
+ return Object.freeze({
120
+ lookups: Object.freeze(lookups),
121
+ resolveLookup,
122
+ resolveLookupItems(filterKey = "") {
123
+ return resolveLookup(filterKey)?.options || [];
124
+ },
125
+ resolveLookupLoading(filterKey = "") {
126
+ return Boolean(resolveLookup(filterKey)?.isLoading);
127
+ },
128
+ resolveLookupSearch(filterKey = "") {
129
+ return String(resolveLookup(filterKey)?.searchQuery || "");
130
+ },
131
+ setLookupSearch(filterKey = "", value = "") {
132
+ resolveLookup(filterKey)?.setSearch(value);
133
+ },
134
+ resolveLookupLabel(filterKey = "", value = "", fallback = "Option") {
135
+ return resolveLookup(filterKey)?.resolveLabel(value, fallback)
136
+ || resolveLookupOptionLabel([], new Map(), value, fallback);
137
+ }
138
+ });
139
+ }
140
+
141
+ export { useCrudListFilterLookups };