@jskit-ai/users-web 0.1.37 → 0.1.40
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 +13 -244
- package/package.json +17 -13
- package/src/client/components/ConsoleSettingsClientElement.vue +1 -1
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +3 -3
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +2 -2
- 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/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +1 -1
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +1 -1
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +6 -2
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +155 -2
- 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} +9 -9
- 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/{useList.js → records/useList.js} +31 -57
- package/src/client/composables/{useView.js → records/useView.js} +6 -9
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +2 -2
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +2 -2
- 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} +2 -2
- 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 +3 -3
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/index.js +1 -0
- package/src/client/providers/UsersWebClientProvider.js +0 -12
- package/src/client/providers/UsersWorkspacesClientProvider.js +26 -0
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +16 -9
- package/templates/src/pages/console/settings/index.vue +1 -0
- package/test/addEditUiRuntime.test.js +1 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +1 -1
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +1 -1
- package/test/listUiRuntime.test.js +1 -1
- 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 +1 -1
- package/test/scopeHelpers.test.js +1 -1
- package/test/settingsPlacementContract.test.js +44 -0
- 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/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +1 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -95
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +0 -162
- package/templates/src/components/WorkspaceNotFoundCard.vue +0 -34
- package/templates/src/composables/useWorkspaceNotFoundState.js +0 -41
- package/templates/src/pages/admin/members/index.vue +0 -7
- package/templates/src/pages/admin/workspace/settings/index.vue +0 -16
- package/templates/src/surfaces/admin/index.vue +0 -29
- package/templates/src/surfaces/admin/root.vue +0 -20
- package/templates/src/surfaces/app/index.vue +0 -27
- package/templates/src/surfaces/app/root.vue +0 -20
- /package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -0
- /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/{listQueryParamSupport.js → support/listQueryParamSupport.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/{routeTemplateHelpers.js → support/routeTemplateHelpers.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();
|
|
@@ -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({
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import "@jskit-ai/uploads-image-web/client/styles";
|
|
2
|
-
import { createImageUploadRuntime } from "@jskit-ai/uploads-image-web/client/composables/createImageUploadRuntime";
|
|
3
|
-
import { resolveFieldErrors } from "@jskit-ai/http-runtime/client";
|
|
4
|
-
import { usersWebHttpClient } from "../lib/httpClient.js";
|
|
5
|
-
|
|
6
|
-
function createAccountSettingsAvatarUploadRuntime({
|
|
7
|
-
queryClient,
|
|
8
|
-
sessionQueryKey,
|
|
9
|
-
accountSettingsQueryKey,
|
|
10
|
-
selectedAvatarFileName,
|
|
11
|
-
applySettingsData,
|
|
12
|
-
reportAccountFeedback
|
|
13
|
-
} = {}) {
|
|
14
|
-
async function resolveCsrfToken() {
|
|
15
|
-
const sessionPayload = await queryClient.fetchQuery({
|
|
16
|
-
queryKey: sessionQueryKey,
|
|
17
|
-
queryFn: () =>
|
|
18
|
-
usersWebHttpClient.request("/api/session", {
|
|
19
|
-
method: "GET"
|
|
20
|
-
}),
|
|
21
|
-
staleTime: 60_000
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const csrfToken = String(sessionPayload?.csrfToken || "");
|
|
25
|
-
if (!csrfToken) {
|
|
26
|
-
throw new Error("Unable to prepare secure avatar upload request.");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return csrfToken;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return createImageUploadRuntime({
|
|
33
|
-
endpoint: "/api/settings/profile/avatar",
|
|
34
|
-
fieldName: "avatar",
|
|
35
|
-
resolveRequestHeaders: async () => ({
|
|
36
|
-
"csrf-token": await resolveCsrfToken()
|
|
37
|
-
}),
|
|
38
|
-
onSelectedFileNameChanged: (fileName) => {
|
|
39
|
-
selectedAvatarFileName.value = String(fileName || "");
|
|
40
|
-
},
|
|
41
|
-
onUploadSuccess: ({ data, uppy }) => {
|
|
42
|
-
applySettingsData(data);
|
|
43
|
-
queryClient.setQueryData(accountSettingsQueryKey, data);
|
|
44
|
-
|
|
45
|
-
const dashboard = uppy.getPlugin("Dashboard");
|
|
46
|
-
if (dashboard && typeof dashboard.closeModal === "function") {
|
|
47
|
-
dashboard.closeModal();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
reportAccountFeedback({
|
|
51
|
-
message: "Avatar uploaded.",
|
|
52
|
-
severity: "success",
|
|
53
|
-
channel: "snackbar",
|
|
54
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-uploaded"
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
onInvalidResponse: () => {
|
|
58
|
-
reportAccountFeedback({
|
|
59
|
-
message: "Avatar uploaded, but the response payload was invalid.",
|
|
60
|
-
severity: "error",
|
|
61
|
-
channel: "banner",
|
|
62
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-invalid-response"
|
|
63
|
-
});
|
|
64
|
-
},
|
|
65
|
-
onUploadError: ({ error, response }) => {
|
|
66
|
-
const body = response?.body && typeof response.body === "object" ? response.body : {};
|
|
67
|
-
const fieldErrors = resolveFieldErrors(body);
|
|
68
|
-
|
|
69
|
-
reportAccountFeedback({
|
|
70
|
-
message: String(fieldErrors.avatar || body?.error || error?.message || "Unable to upload avatar."),
|
|
71
|
-
severity: "error",
|
|
72
|
-
channel: "banner",
|
|
73
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-error"
|
|
74
|
-
});
|
|
75
|
-
},
|
|
76
|
-
onRestrictionFailed: ({ error }) => {
|
|
77
|
-
reportAccountFeedback({
|
|
78
|
-
message: String(error?.message || "Selected avatar file does not meet upload restrictions."),
|
|
79
|
-
severity: "error",
|
|
80
|
-
channel: "banner",
|
|
81
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-upload-restriction"
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
onUnavailable: () => {
|
|
85
|
-
reportAccountFeedback({
|
|
86
|
-
message: "Avatar editor is unavailable in this environment.",
|
|
87
|
-
severity: "error",
|
|
88
|
-
channel: "banner",
|
|
89
|
-
dedupeKey: "users-web.account-settings-runtime:avatar-editor-unavailable"
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export { createAccountSettingsAvatarUploadRuntime };
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
3
|
-
import { useRoute } from "vue-router";
|
|
4
|
-
import { useQuery } from "@tanstack/vue-query";
|
|
5
|
-
import { mdiEmailAlertOutline } from "@mdi/js";
|
|
6
|
-
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
7
|
-
import {
|
|
8
|
-
useWebPlacementContext,
|
|
9
|
-
resolveSurfaceDefinitionFromPlacementContext,
|
|
10
|
-
resolveSurfacePathFromPlacementContext,
|
|
11
|
-
resolveSurfaceNavigationTargetFromPlacementContext
|
|
12
|
-
} from "@jskit-ai/shell-web/client/placement";
|
|
13
|
-
|
|
14
|
-
const { context: placementContext } = useWebPlacementContext();
|
|
15
|
-
const route = useRoute();
|
|
16
|
-
|
|
17
|
-
function normalizePendingInvitesCount(value) {
|
|
18
|
-
const numeric = Number(value);
|
|
19
|
-
if (!Number.isInteger(numeric) || numeric < 1) {
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
return numeric;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveReturnTo() {
|
|
26
|
-
const fullPath = String(route?.fullPath || "").trim();
|
|
27
|
-
if (fullPath.startsWith("/") && !fullPath.startsWith("//")) {
|
|
28
|
-
return fullPath;
|
|
29
|
-
}
|
|
30
|
-
const path = String(route?.path || "").trim();
|
|
31
|
-
if (path.startsWith("/") && !path.startsWith("//")) {
|
|
32
|
-
return path;
|
|
33
|
-
}
|
|
34
|
-
return "/";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveReturnToHref() {
|
|
38
|
-
if (typeof window === "object" && window?.location?.href) {
|
|
39
|
-
return String(window.location.href || "").trim() || resolveReturnTo();
|
|
40
|
-
}
|
|
41
|
-
return resolveReturnTo();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function countPendingInvites(entries = []) {
|
|
45
|
-
if (!Array.isArray(entries)) {
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let total = 0;
|
|
50
|
-
for (const entry of entries) {
|
|
51
|
-
if (!entry || typeof entry !== "object") {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
total += 1;
|
|
55
|
-
}
|
|
56
|
-
return total;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const authenticated = computed(() => placementContext.value?.auth?.authenticated === true);
|
|
60
|
-
|
|
61
|
-
const bootstrapSummaryQuery = useQuery({
|
|
62
|
-
queryKey: ["local-main", "account", "invites-cue", "bootstrap"],
|
|
63
|
-
enabled: authenticated,
|
|
64
|
-
staleTime: 5_000,
|
|
65
|
-
refetchInterval: 15_000,
|
|
66
|
-
queryFn: async () => {
|
|
67
|
-
const response = await fetch("/api/bootstrap", {
|
|
68
|
-
method: "GET",
|
|
69
|
-
credentials: "include",
|
|
70
|
-
headers: {
|
|
71
|
-
accept: "application/json"
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
throw new Error(`Bootstrap request failed with status ${response.status}.`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return response.json();
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const placementPendingInvitesCount = computed(() =>
|
|
83
|
-
normalizePendingInvitesCount(placementContext.value?.pendingInvitesCount)
|
|
84
|
-
);
|
|
85
|
-
const bootstrapPendingInvitesCount = computed(() => {
|
|
86
|
-
const payload = bootstrapSummaryQuery.data.value;
|
|
87
|
-
const invitesEnabled = payload?.app?.features?.workspaceInvites === true;
|
|
88
|
-
if (!invitesEnabled) {
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return countPendingInvites(payload?.pendingInvites);
|
|
93
|
-
});
|
|
94
|
-
const pendingInvitesCount = computed(() =>
|
|
95
|
-
Math.max(placementPendingInvitesCount.value, bootstrapPendingInvitesCount.value)
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const placementWorkspaceInvitesEnabled = computed(() => placementContext.value?.workspaceInvitesEnabled === true);
|
|
99
|
-
const bootstrapWorkspaceInvitesEnabled = computed(() => {
|
|
100
|
-
const payload = bootstrapSummaryQuery.data.value;
|
|
101
|
-
return payload?.app?.features?.workspaceInvites === true;
|
|
102
|
-
});
|
|
103
|
-
const workspaceInvitesEnabled = computed(
|
|
104
|
-
() => placementWorkspaceInvitesEnabled.value || bootstrapWorkspaceInvitesEnabled.value
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const isVisible = computed(() => {
|
|
108
|
-
return (
|
|
109
|
-
authenticated.value &&
|
|
110
|
-
workspaceInvitesEnabled.value &&
|
|
111
|
-
pendingInvitesCount.value > 0
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const resolvedTo = computed(() => {
|
|
116
|
-
const hasAccountSurface = Boolean(resolveSurfaceDefinitionFromPlacementContext(placementContext.value, "account"));
|
|
117
|
-
const accountSettingsPath = hasAccountSurface
|
|
118
|
-
? resolveSurfacePathFromPlacementContext(placementContext.value, "account", "/")
|
|
119
|
-
: "/account";
|
|
120
|
-
const accountSettingsNavigation = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
121
|
-
path: accountSettingsPath,
|
|
122
|
-
surfaceId: "account"
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const query = new URLSearchParams({
|
|
126
|
-
section: "invites",
|
|
127
|
-
returnTo: accountSettingsNavigation.sameOrigin ? resolveReturnTo() : resolveReturnToHref()
|
|
128
|
-
});
|
|
129
|
-
return appendQueryString(accountSettingsPath, query.toString());
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const resolvedNavigationTarget = computed(() =>
|
|
133
|
-
resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
134
|
-
path: resolvedTo.value,
|
|
135
|
-
surfaceId: "account"
|
|
136
|
-
})
|
|
137
|
-
);
|
|
138
|
-
</script>
|
|
139
|
-
|
|
140
|
-
<template>
|
|
141
|
-
<v-badge
|
|
142
|
-
v-if="isVisible"
|
|
143
|
-
color="error"
|
|
144
|
-
:content="pendingInvitesCount"
|
|
145
|
-
:model-value="pendingInvitesCount > 0"
|
|
146
|
-
bordered
|
|
147
|
-
offset-x="6"
|
|
148
|
-
offset-y="8"
|
|
149
|
-
>
|
|
150
|
-
<v-btn
|
|
151
|
-
:to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href : undefined"
|
|
152
|
-
:href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href"
|
|
153
|
-
variant="tonal"
|
|
154
|
-
color="warning"
|
|
155
|
-
:prepend-icon="mdiEmailAlertOutline"
|
|
156
|
-
size="small"
|
|
157
|
-
class="text-none"
|
|
158
|
-
>
|
|
159
|
-
Invites
|
|
160
|
-
</v-btn>
|
|
161
|
-
</v-badge>
|
|
162
|
-
</template>
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { mdiAlertCircleOutline } from "@mdi/js";
|
|
3
|
-
import { computed } from "vue";
|
|
4
|
-
|
|
5
|
-
const props = defineProps({
|
|
6
|
-
surfaceLabel: {
|
|
7
|
-
type: String,
|
|
8
|
-
default: "Workspace"
|
|
9
|
-
},
|
|
10
|
-
message: {
|
|
11
|
-
type: String,
|
|
12
|
-
default: "Workspace is currently unavailable."
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const normalizedSurfaceLabel = computed(() => String(props.surfaceLabel || "").trim() || "Workspace");
|
|
17
|
-
const normalizedMessage = computed(() => String(props.message || "").trim() || "Workspace is currently unavailable.");
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<v-card rounded="lg" elevation="1" border>
|
|
22
|
-
<v-card-item>
|
|
23
|
-
<template #prepend>
|
|
24
|
-
<v-icon :icon="mdiAlertCircleOutline" color="error" />
|
|
25
|
-
</template>
|
|
26
|
-
<v-card-title class="text-h5">Unavailable</v-card-title>
|
|
27
|
-
<v-card-subtitle>{{ normalizedSurfaceLabel }} surface.</v-card-subtitle>
|
|
28
|
-
</v-card-item>
|
|
29
|
-
<v-divider />
|
|
30
|
-
<v-card-text class="d-flex flex-column ga-4">
|
|
31
|
-
<p class="text-medium-emphasis mb-0">{{ normalizedMessage }}</p>
|
|
32
|
-
</v-card-text>
|
|
33
|
-
</v-card>
|
|
34
|
-
</template>
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
2
|
-
import { useRoute } from "vue-router";
|
|
3
|
-
import { normalizeLowerText, normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
-
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
5
|
-
|
|
6
|
-
const STATUS_MESSAGES = {
|
|
7
|
-
not_found: "The requested workspace was not found.",
|
|
8
|
-
forbidden: "You do not have access to this workspace.",
|
|
9
|
-
unauthenticated: "You need to sign in to access this workspace.",
|
|
10
|
-
error: "Workspace data could not be loaded right now."
|
|
11
|
-
};
|
|
12
|
-
const DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE = "Workspace is currently unavailable.";
|
|
13
|
-
const RESOLVED_WORKSPACE_STATUS = "resolved";
|
|
14
|
-
|
|
15
|
-
function useWorkspaceNotFoundState() {
|
|
16
|
-
const route = useRoute();
|
|
17
|
-
const { context: placementContext } = useWebPlacementContext();
|
|
18
|
-
|
|
19
|
-
const routeWorkspaceSlug = computed(() => normalizeLowerText(route?.params?.workspaceSlug));
|
|
20
|
-
|
|
21
|
-
const workspaceBootstrapStatus = computed(() => {
|
|
22
|
-
const statuses = normalizeObject(placementContext.value?.workspaceBootstrapStatuses);
|
|
23
|
-
return normalizeLowerText(statuses[routeWorkspaceSlug.value]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const workspaceUnavailable = computed(
|
|
27
|
-
() => Boolean(workspaceBootstrapStatus.value) && workspaceBootstrapStatus.value !== RESOLVED_WORKSPACE_STATUS
|
|
28
|
-
);
|
|
29
|
-
const workspaceUnavailableMessage = computed(
|
|
30
|
-
() => STATUS_MESSAGES[workspaceBootstrapStatus.value] || DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
return Object.freeze({
|
|
34
|
-
routeWorkspaceSlug,
|
|
35
|
-
workspaceBootstrapStatus,
|
|
36
|
-
workspaceUnavailable,
|
|
37
|
-
workspaceUnavailableMessage
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export { useWorkspaceNotFoundState };
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section class="settings-page">
|
|
3
|
-
<ShellOutlet host="workspace-settings" position="forms" />
|
|
4
|
-
</section>
|
|
5
|
-
</template>
|
|
6
|
-
|
|
7
|
-
<script setup>
|
|
8
|
-
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
<style scoped>
|
|
12
|
-
.settings-page {
|
|
13
|
-
display: grid;
|
|
14
|
-
gap: 1rem;
|
|
15
|
-
}
|
|
16
|
-
</style>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import WorkspaceNotFoundCard from "@/components/WorkspaceNotFoundCard.vue";
|
|
3
|
-
import { useWorkspaceNotFoundState } from "@/composables/useWorkspaceNotFoundState";
|
|
4
|
-
|
|
5
|
-
const { workspaceUnavailable, workspaceUnavailableMessage } = useWorkspaceNotFoundState();
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<template>
|
|
9
|
-
<WorkspaceNotFoundCard
|
|
10
|
-
v-if="workspaceUnavailable"
|
|
11
|
-
:message="workspaceUnavailableMessage"
|
|
12
|
-
surface-label="Admin"
|
|
13
|
-
/>
|
|
14
|
-
<v-card v-else rounded="lg" elevation="1" border>
|
|
15
|
-
<v-card-item>
|
|
16
|
-
<template #prepend>
|
|
17
|
-
<v-chip color="primary" size="small" label>Admin</v-chip>
|
|
18
|
-
</template>
|
|
19
|
-
<v-card-title class="text-h5">Workspace Admin</v-card-title>
|
|
20
|
-
<v-card-subtitle>Privileged workspace workflows.</v-card-subtitle>
|
|
21
|
-
</v-card-item>
|
|
22
|
-
<v-divider />
|
|
23
|
-
<v-card-text class="d-flex flex-column ga-4">
|
|
24
|
-
<p class="text-medium-emphasis mb-0">
|
|
25
|
-
Use this area for workspace administration modules.
|
|
26
|
-
</p>
|
|
27
|
-
</v-card-text>
|
|
28
|
-
</v-card>
|
|
29
|
-
</template>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<route lang="json">
|
|
2
|
-
{
|
|
3
|
-
"meta": {
|
|
4
|
-
"jskit": {
|
|
5
|
-
"surface": "admin"
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
</route>
|
|
10
|
-
|
|
11
|
-
<script setup>
|
|
12
|
-
import ShellLayout from "@/components/ShellLayout.vue";
|
|
13
|
-
import { RouterView } from "vue-router";
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<ShellLayout title="" subtitle="">
|
|
18
|
-
<RouterView />
|
|
19
|
-
</ShellLayout>
|
|
20
|
-
</template>
|