@jskit-ai/users-web 0.1.36 → 0.1.38
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/package.descriptor.mjs +8 -22
- package/package.json +16 -11
- package/src/client/components/MembersAdminClientElement.vue +5 -5
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +4 -4
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
- package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
- package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
- package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
- package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
- package/src/client/composables/internal/useOperationScope.js +1 -1
- package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +18 -8
- package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
- package/src/client/composables/records/useCrudList.js +83 -0
- package/src/client/composables/records/useCrudView.js +35 -0
- package/src/client/composables/records/useList.js +482 -0
- package/src/client/composables/{useView.js → records/useView.js} +7 -7
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
- package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
- package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
- package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
- package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
- package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +13 -4
- package/src/client/composables/support/listQueryParamSupport.js +459 -0
- package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
- package/src/client/composables/useAccess.js +2 -2
- package/src/client/composables/useAccountSettingsRuntime.js +6 -6
- package/src/client/composables/useBootstrapQuery.js +1 -1
- package/src/client/composables/useCommand.js +5 -5
- package/src/client/composables/useCrudListParentTitle.js +131 -0
- package/src/client/composables/usePagedCollection.js +58 -7
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/lib/bootstrap.js +1 -1
- package/src/client/lib/menuIcons.js +27 -6
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
- package/test/addEditUiRuntime.test.js +19 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +52 -2
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +190 -0
- package/test/listUiRuntime.test.js +22 -1
- package/test/menuIcons.test.js +2 -0
- package/test/menuLinkTarget.test.js +116 -0
- package/test/permissions.test.js +2 -2
- package/test/refValueHelpers.test.js +1 -1
- package/test/resourceLoadStateHelpers.test.js +1 -1
- package/test/routeTemplateHelpers.test.js +57 -1
- package/test/scopeHelpers.test.js +1 -1
- package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
- package/test/useCrudListParentTitle.test.js +143 -0
- package/test/useListSearchSupport.test.js +1 -1
- package/test/usePagedCollection.test.js +53 -0
- package/test/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +36 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
- package/src/client/composables/useList.js +0 -268
- /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
- /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
- /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
- /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
- /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
- /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
- /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
- /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
- /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
- /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
- /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
- /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
applyCrudRouteBoundFieldValues,
|
|
12
12
|
resolveCrudFieldErrors,
|
|
13
13
|
parseCrudResourceOperationInput
|
|
14
|
-
} from "../src/client/composables/crudSchemaFormHelpers.js";
|
|
14
|
+
} from "../src/client/composables/crud/crudSchemaFormHelpers.js";
|
|
15
15
|
|
|
16
16
|
test("normalizeCrudFormFields trims keys, removes invalid entries, and deduplicates", () => {
|
|
17
17
|
const fields = normalizeCrudFormFields([
|
|
@@ -62,6 +62,86 @@ test("buildCrudFormPayload normalizes booleans and numbers while skipping empty
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test("buildCrudFormPayload and applyCrudPayloadToForm round-trip date-time fields", () => {
|
|
66
|
+
const fields = [
|
|
67
|
+
{ key: "scheduledAt", type: "string", format: "date-time" }
|
|
68
|
+
];
|
|
69
|
+
const payload = buildCrudFormPayload(fields, {
|
|
70
|
+
scheduledAt: "2024-01-02T03:04"
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.match(payload.scheduledAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/);
|
|
74
|
+
|
|
75
|
+
const form = reactive({
|
|
76
|
+
scheduledAt: ""
|
|
77
|
+
});
|
|
78
|
+
applyCrudPayloadToForm(fields, form, payload);
|
|
79
|
+
|
|
80
|
+
assert.equal(form.scheduledAt, "2024-01-02T03:04");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("buildCrudFormPayload normalizes time fields to canonical HH:MM", () => {
|
|
84
|
+
const fields = [
|
|
85
|
+
{ key: "fromTime", type: "string", format: "time" },
|
|
86
|
+
{ key: "toTime", type: "string", format: "time" }
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const payload = buildCrudFormPayload(fields, {
|
|
90
|
+
fromTime: "06:13 PM",
|
|
91
|
+
toTime: "18:45:00"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(payload, {
|
|
95
|
+
fromTime: "18:13",
|
|
96
|
+
toTime: "18:45"
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("buildCrudFormPayload serializes cleared nullable typed fields as null", () => {
|
|
101
|
+
const payload = buildCrudFormPayload(
|
|
102
|
+
[
|
|
103
|
+
{ key: "serviceId", type: "integer", nullable: true },
|
|
104
|
+
{ key: "fromDate", type: "string", format: "date", nullable: true },
|
|
105
|
+
{ key: "scheduledAt", type: "string", format: "date-time", nullable: true },
|
|
106
|
+
{ key: "fromTime", type: "string", format: "time", nullable: true }
|
|
107
|
+
],
|
|
108
|
+
{
|
|
109
|
+
serviceId: null,
|
|
110
|
+
fromDate: "",
|
|
111
|
+
scheduledAt: "",
|
|
112
|
+
fromTime: ""
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(payload, {
|
|
117
|
+
serviceId: null,
|
|
118
|
+
fromDate: null,
|
|
119
|
+
scheduledAt: null,
|
|
120
|
+
fromTime: null
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("applyCrudPayloadToForm normalizes time fields for form inputs", () => {
|
|
125
|
+
const fields = [
|
|
126
|
+
{ key: "fromTime", type: "string", format: "time" },
|
|
127
|
+
{ key: "toTime", type: "string", format: "time" }
|
|
128
|
+
];
|
|
129
|
+
const form = reactive({
|
|
130
|
+
fromTime: "",
|
|
131
|
+
toTime: ""
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
applyCrudPayloadToForm(fields, form, {
|
|
135
|
+
fromTime: "18:13:00",
|
|
136
|
+
toTime: "06:45 PM"
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.deepEqual(form, {
|
|
140
|
+
fromTime: "18:13",
|
|
141
|
+
toTime: "18:45"
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
65
145
|
test("applyCrudPayloadToForm maps payload values into reactive form model", () => {
|
|
66
146
|
const form = reactive({
|
|
67
147
|
name: "",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { resolveCrudListParentDescriptor, resolveCrudListParentRecordTitle, resolveCrudListParentTitleFromItems } from "../src/client/composables/internal/crudListParentTitleSupport.js";
|
|
4
|
+
|
|
5
|
+
const contactChildResource = Object.freeze({
|
|
6
|
+
contract: {
|
|
7
|
+
lookup: {
|
|
8
|
+
containerKey: "lookups"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
fieldMeta: Object.freeze([
|
|
12
|
+
Object.freeze({
|
|
13
|
+
key: "contactId",
|
|
14
|
+
relation: Object.freeze({
|
|
15
|
+
kind: "lookup",
|
|
16
|
+
namespace: "contacts",
|
|
17
|
+
valueKey: "id"
|
|
18
|
+
})
|
|
19
|
+
}),
|
|
20
|
+
Object.freeze({
|
|
21
|
+
key: "serviceId",
|
|
22
|
+
relation: Object.freeze({
|
|
23
|
+
kind: "lookup",
|
|
24
|
+
namespace: "services",
|
|
25
|
+
valueKey: "id"
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
])
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("resolveCrudListParentDescriptor selects the nearest lookup route parent", () => {
|
|
32
|
+
const descriptor = resolveCrudListParentDescriptor({
|
|
33
|
+
resource: contactChildResource,
|
|
34
|
+
route: {
|
|
35
|
+
matched: [
|
|
36
|
+
{ path: "/w/:workspaceSlug/admin" },
|
|
37
|
+
{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }
|
|
38
|
+
],
|
|
39
|
+
params: {
|
|
40
|
+
workspaceSlug: "dogandgroom",
|
|
41
|
+
contactId: "538779"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
recordIdParam: "availabilityRuleId"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
descriptor,
|
|
49
|
+
{
|
|
50
|
+
fieldKey: "contactId",
|
|
51
|
+
routeParamKey: "contactId",
|
|
52
|
+
relationNamespace: "contacts",
|
|
53
|
+
entityLabel: "Contact",
|
|
54
|
+
labelKey: "",
|
|
55
|
+
fieldDescriptor: {
|
|
56
|
+
key: "contactId",
|
|
57
|
+
relation: {
|
|
58
|
+
kind: "lookup",
|
|
59
|
+
valueKey: "id",
|
|
60
|
+
labelKey: "",
|
|
61
|
+
containerKey: "lookups"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
apiUrlTemplate: "/contacts/:contactId"
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("resolveCrudListParentTitleFromItems uses the hydrated lookup label", () => {
|
|
70
|
+
const descriptor = resolveCrudListParentDescriptor({
|
|
71
|
+
resource: contactChildResource,
|
|
72
|
+
route: {
|
|
73
|
+
matched: [{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }],
|
|
74
|
+
params: {
|
|
75
|
+
workspaceSlug: "dogandgroom",
|
|
76
|
+
contactId: "538779"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
recordIdParam: "availabilityRuleId"
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const title = resolveCrudListParentTitleFromItems(
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
id: 1,
|
|
86
|
+
contactId: 538779,
|
|
87
|
+
lookups: {
|
|
88
|
+
contactId: {
|
|
89
|
+
id: 538779,
|
|
90
|
+
firstName: "Jessica",
|
|
91
|
+
lastName: "Dickinson"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
descriptor
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
assert.equal(title, "Jessica Dickinson");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("resolveCrudListParentRecordTitle falls back to entity label plus id", () => {
|
|
103
|
+
const title = resolveCrudListParentRecordTitle(
|
|
104
|
+
{
|
|
105
|
+
id: 538779
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
entityLabel: "Contact",
|
|
109
|
+
labelKey: ""
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
assert.equal(title, "Contact #538779");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("resolveCrudListParentDescriptor supports parentRouteParamKey aliases", () => {
|
|
117
|
+
const descriptor = resolveCrudListParentDescriptor({
|
|
118
|
+
resource: {
|
|
119
|
+
fieldMeta: [
|
|
120
|
+
{
|
|
121
|
+
key: "staffContactId",
|
|
122
|
+
parentRouteParamKey: "contactId",
|
|
123
|
+
relation: {
|
|
124
|
+
kind: "lookup",
|
|
125
|
+
namespace: "contacts",
|
|
126
|
+
valueKey: "id"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
route: {
|
|
132
|
+
matched: [{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }],
|
|
133
|
+
params: {
|
|
134
|
+
workspaceSlug: "dogandgroom",
|
|
135
|
+
contactId: "538779"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
recordIdParam: "availabilityRuleId"
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
assert.equal(descriptor?.fieldKey, "staffContactId");
|
|
142
|
+
assert.equal(descriptor?.routeParamKey, "contactId");
|
|
143
|
+
});
|
|
@@ -3,7 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeListSearchConfig,
|
|
5
5
|
matchesLocalSearch
|
|
6
|
-
} from "../src/client/composables/listSearchSupport.js";
|
|
6
|
+
} from "../src/client/composables/support/listSearchSupport.js";
|
|
7
7
|
|
|
8
8
|
test("normalizeListSearchConfig defaults to disabled query search", () => {
|
|
9
9
|
const config = normalizeListSearchConfig();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { trimInfinitePagesToFirst } from "../src/client/composables/usePagedCollection.js";
|
|
4
|
+
|
|
5
|
+
test("trimInfinitePagesToFirst leaves non-object payloads untouched", () => {
|
|
6
|
+
assert.equal(trimInfinitePagesToFirst(null), null);
|
|
7
|
+
assert.equal(trimInfinitePagesToFirst(""), "");
|
|
8
|
+
assert.equal(trimInfinitePagesToFirst(12), 12);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("trimInfinitePagesToFirst leaves single-page payload unchanged", () => {
|
|
12
|
+
const payload = Object.freeze({
|
|
13
|
+
pages: Object.freeze([{ items: [1, 2] }]),
|
|
14
|
+
pageParams: Object.freeze([null])
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.equal(trimInfinitePagesToFirst(payload), payload);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("trimInfinitePagesToFirst truncates to the first page and first pageParam", () => {
|
|
21
|
+
const payload = {
|
|
22
|
+
pages: [
|
|
23
|
+
{ items: [1, 2] },
|
|
24
|
+
{ items: [3, 4] },
|
|
25
|
+
{ items: [5, 6] }
|
|
26
|
+
],
|
|
27
|
+
pageParams: [null, "cursor-1", "cursor-2"],
|
|
28
|
+
extra: true
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
trimInfinitePagesToFirst(payload),
|
|
33
|
+
{
|
|
34
|
+
pages: [{ items: [1, 2] }],
|
|
35
|
+
pageParams: [null],
|
|
36
|
+
extra: true
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("trimInfinitePagesToFirst injects null pageParam when missing", () => {
|
|
42
|
+
const payload = {
|
|
43
|
+
pages: [{ items: [1] }, { items: [2] }]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
trimInfinitePagesToFirst(payload),
|
|
48
|
+
{
|
|
49
|
+
pages: [{ items: [1] }],
|
|
50
|
+
pageParams: [null]
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { useViewCore } from "../src/client/composables/useViewCore.js";
|
|
4
|
+
import { useViewCore } from "../src/client/composables/runtime/useViewCore.js";
|
|
5
5
|
|
|
6
6
|
test("useViewCore prefers resource isInitialLoading/isFetching signals when provided", () => {
|
|
7
7
|
const resource = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { createViewUiRuntime } from "../src/client/composables/viewUiRuntime.js";
|
|
4
|
+
import { createViewUiRuntime } from "../src/client/composables/runtime/viewUiRuntime.js";
|
|
5
5
|
|
|
6
6
|
test("createViewUiRuntime resolves api/list/edit paths with nested params", () => {
|
|
7
7
|
const runtime = createViewUiRuntime({
|
|
@@ -23,6 +23,41 @@ test("createViewUiRuntime resolves api/list/edit paths with nested params", () =
|
|
|
23
23
|
assert.equal(runtime.resolveParams("./edit"), "/users/user-7/addresses/addr-42/edit");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test("createViewUiRuntime resolves view links from the record pathname when route is nested", () => {
|
|
27
|
+
const runtime = createViewUiRuntime({
|
|
28
|
+
recordIdParam: "contactId",
|
|
29
|
+
routeParams: ref({
|
|
30
|
+
workspaceSlug: "dogandgroom",
|
|
31
|
+
contactId: "541841",
|
|
32
|
+
petId: "715528"
|
|
33
|
+
}),
|
|
34
|
+
routeParamNames: ref(["workspaceSlug", "contactId", "petId"]),
|
|
35
|
+
routePath: ref("/w/dogandgroom/admin/contacts/541841/pets/715528"),
|
|
36
|
+
listUrlTemplate: "..",
|
|
37
|
+
editUrlTemplate: "./edit"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(runtime.listUrl.value, "/w/dogandgroom/admin/contacts");
|
|
41
|
+
assert.equal(runtime.editUrl.value, "/w/dogandgroom/admin/contacts/541841/edit");
|
|
42
|
+
assert.equal(runtime.resolveParams("./edit"), "/w/dogandgroom/admin/contacts/541841/edit");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("createViewUiRuntime uses route param order when repeated values are present", () => {
|
|
46
|
+
const runtime = createViewUiRuntime({
|
|
47
|
+
recordIdParam: "contactId",
|
|
48
|
+
routeParams: ref({
|
|
49
|
+
workspaceSlug: "123",
|
|
50
|
+
contactId: "123",
|
|
51
|
+
petId: "123"
|
|
52
|
+
}),
|
|
53
|
+
routeParamNames: ref(["workspaceSlug", "contactId", "petId"]),
|
|
54
|
+
routePath: ref("/w/123/admin/contacts/123/pets/123"),
|
|
55
|
+
editUrlTemplate: "./edit"
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assert.equal(runtime.editUrl.value, "/w/123/admin/contacts/123/edit");
|
|
59
|
+
});
|
|
60
|
+
|
|
26
61
|
test("createViewUiRuntime uses explicit routeRecordId when route params do not include id", () => {
|
|
27
62
|
const runtime = createViewUiRuntime({
|
|
28
63
|
recordIdParam: "addressId",
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import Uppy from "@uppy/core";
|
|
2
|
-
import Dashboard from "@uppy/dashboard";
|
|
3
|
-
import ImageEditor from "@uppy/image-editor";
|
|
4
|
-
import Compressor from "@uppy/compressor";
|
|
5
|
-
import XHRUpload from "@uppy/xhr-upload";
|
|
6
|
-
import "@uppy/core/css/style.min.css";
|
|
7
|
-
import "@uppy/dashboard/css/style.min.css";
|
|
8
|
-
import "@uppy/image-editor/css/style.min.css";
|
|
9
|
-
import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
|
|
10
|
-
import { usersWebHttpClient } from "../lib/httpClient.js";
|
|
11
|
-
import {
|
|
12
|
-
AVATAR_ALLOWED_MIME_TYPES,
|
|
13
|
-
AVATAR_MAX_UPLOAD_BYTES
|
|
14
|
-
} from "./accountSettingsRuntimeConstants.js";
|
|
15
|
-
|
|
16
|
-
function parseUploadResponse(xhr) {
|
|
17
|
-
if (!xhr.responseText) {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(xhr.responseText);
|
|
23
|
-
} catch {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function stopImageEditor(uppy) {
|
|
29
|
-
const imageEditor = uppy.getPlugin("ImageEditor");
|
|
30
|
-
if (imageEditor && typeof imageEditor.stop === "function") {
|
|
31
|
-
imageEditor.stop();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createAccountSettingsAvatarUploadRuntime({
|
|
36
|
-
queryClient,
|
|
37
|
-
sessionQueryKey,
|
|
38
|
-
accountSettingsQueryKey,
|
|
39
|
-
selectedAvatarFileName,
|
|
40
|
-
applySettingsData,
|
|
41
|
-
reportAccountFeedback
|
|
42
|
-
} = {}) {
|
|
43
|
-
let avatarUppy = null;
|
|
44
|
-
|
|
45
|
-
async function resolveCsrfToken() {
|
|
46
|
-
const sessionPayload = await queryClient.fetchQuery({
|
|
47
|
-
queryKey: sessionQueryKey,
|
|
48
|
-
queryFn: () =>
|
|
49
|
-
usersWebHttpClient.request("/api/session", {
|
|
50
|
-
method: "GET"
|
|
51
|
-
}),
|
|
52
|
-
staleTime: 60_000
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const csrfToken = String(sessionPayload?.csrfToken || "");
|
|
56
|
-
if (!csrfToken) {
|
|
57
|
-
throw new Error("Unable to prepare secure avatar upload request.");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return csrfToken;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function setup() {
|
|
64
|
-
if (typeof window === "undefined") {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (avatarUppy) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const uppy = new Uppy({
|
|
73
|
-
autoProceed: false,
|
|
74
|
-
restrictions: {
|
|
75
|
-
maxNumberOfFiles: 1,
|
|
76
|
-
allowedFileTypes: [...AVATAR_ALLOWED_MIME_TYPES],
|
|
77
|
-
maxFileSize: AVATAR_MAX_UPLOAD_BYTES
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
uppy.use(Dashboard, {
|
|
82
|
-
inline: false,
|
|
83
|
-
closeAfterFinish: false,
|
|
84
|
-
showProgressDetails: true,
|
|
85
|
-
proudlyDisplayPoweredByUppy: false,
|
|
86
|
-
hideUploadButton: false,
|
|
87
|
-
doneButtonHandler: () => {
|
|
88
|
-
const dashboard = uppy.getPlugin("Dashboard");
|
|
89
|
-
if (dashboard && typeof dashboard.closeModal === "function") {
|
|
90
|
-
dashboard.closeModal();
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
note: `Accepted: ${AVATAR_ALLOWED_MIME_TYPES.join(", ")}, max ${Math.floor(AVATAR_MAX_UPLOAD_BYTES / (1024 * 1024))}MB`
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
uppy.use(ImageEditor, {
|
|
97
|
-
quality: 0.9
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
uppy.use(Compressor, {
|
|
101
|
-
quality: 0.84,
|
|
102
|
-
limit: 1
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
uppy.use(XHRUpload, {
|
|
106
|
-
endpoint: "/api/settings/profile/avatar",
|
|
107
|
-
method: "POST",
|
|
108
|
-
formData: true,
|
|
109
|
-
fieldName: "avatar",
|
|
110
|
-
withCredentials: true,
|
|
111
|
-
onBeforeRequest: async (xhr) => {
|
|
112
|
-
const csrfToken = await resolveCsrfToken();
|
|
113
|
-
xhr.setRequestHeader("csrf-token", csrfToken);
|
|
114
|
-
},
|
|
115
|
-
getResponseData: parseUploadResponse
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
uppy.on("file-added", (file) => {
|
|
119
|
-
selectedAvatarFileName.value = String(file?.name || "");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
uppy.on("file-removed", () => {
|
|
123
|
-
selectedAvatarFileName.value = "";
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
uppy.on("file-editor:complete", (file) => {
|
|
127
|
-
selectedAvatarFileName.value = String(file?.name || selectedAvatarFileName.value || "");
|
|
128
|
-
stopImageEditor(uppy);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
uppy.on("file-editor:cancel", () => {
|
|
132
|
-
stopImageEditor(uppy);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
uppy.on("dashboard:modal-closed", () => {
|
|
136
|
-
stopImageEditor(uppy);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
uppy.on("upload-success", (_file, response) => {
|
|
140
|
-
const data = response?.body;
|
|
141
|
-
if (!data || typeof data !== "object") {
|
|
142
|
-
reportAccountFeedback({
|
|
143
|
-
message: "Avatar uploaded, but the response payload was invalid.",
|
|
144
|
-
severity: "error",
|
|
145
|
-
channel: "banner",
|
|
146
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
applySettingsData(data);
|
|
152
|
-
queryClient.setQueryData(accountSettingsQueryKey, data);
|
|
153
|
-
|
|
154
|
-
const dashboard = uppy.getPlugin("Dashboard");
|
|
155
|
-
if (dashboard && typeof dashboard.closeModal === "function") {
|
|
156
|
-
dashboard.closeModal();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
reportAccountFeedback({
|
|
160
|
-
message: "Avatar uploaded.",
|
|
161
|
-
severity: "success",
|
|
162
|
-
channel: "snackbar",
|
|
163
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-uploaded"
|
|
164
|
-
});
|
|
165
|
-
selectedAvatarFileName.value = "";
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
uppy.on("upload-error", (_file, error, response) => {
|
|
169
|
-
const body = response?.body && typeof response.body === "object" ? response.body : {};
|
|
170
|
-
const fieldErrors = resolveFieldErrors(body);
|
|
171
|
-
|
|
172
|
-
reportAccountFeedback({
|
|
173
|
-
message: String(fieldErrors.avatar || body?.error || error?.message || "Unable to upload avatar."),
|
|
174
|
-
severity: "error",
|
|
175
|
-
channel: "banner",
|
|
176
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-error"
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
uppy.on("restriction-failed", (_file, error) => {
|
|
181
|
-
reportAccountFeedback({
|
|
182
|
-
message: String(error?.message || "Selected avatar file does not meet upload restrictions."),
|
|
183
|
-
severity: "error",
|
|
184
|
-
channel: "banner",
|
|
185
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-restriction"
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
uppy.on("complete", (result) => {
|
|
190
|
-
const successfulCount = Array.isArray(result?.successful) ? result.successful.length : 0;
|
|
191
|
-
if (successfulCount <= 0) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
uppy.clear();
|
|
197
|
-
} catch {
|
|
198
|
-
// Upload succeeded; ignore clear timing issues.
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
avatarUppy = uppy;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function openEditor() {
|
|
206
|
-
setup();
|
|
207
|
-
|
|
208
|
-
const uppy = avatarUppy;
|
|
209
|
-
if (!uppy) {
|
|
210
|
-
reportAccountFeedback({
|
|
211
|
-
message: "Avatar editor is unavailable in this environment.",
|
|
212
|
-
severity: "error",
|
|
213
|
-
channel: "banner",
|
|
214
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-editor-unavailable"
|
|
215
|
-
});
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const dashboard = uppy.getPlugin("Dashboard");
|
|
220
|
-
if (dashboard && typeof dashboard.openModal === "function") {
|
|
221
|
-
dashboard.openModal();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function destroy() {
|
|
226
|
-
if (!avatarUppy) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
avatarUppy.destroy();
|
|
231
|
-
avatarUppy = null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return Object.freeze({
|
|
235
|
-
destroy,
|
|
236
|
-
openEditor,
|
|
237
|
-
setup
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export { createAccountSettingsAvatarUploadRuntime };
|