@vtex/faststore-plugin-buyer-portal 2.0.19 → 2.0.21
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/CHANGELOG.md +14 -1
- package/docs/i18n/locales.json +39 -0
- package/docs/i18n/migration-inventory.md +78 -0
- package/package.json +1 -1
- package/specs/analytics-actor-type.md +243 -0
- package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +28 -15
- package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +15 -12
- package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +6 -4
- package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +5 -1
- package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +5 -2
- package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +4 -1
- package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +5 -2
- package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +4 -1
- package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +12 -15
- package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +11 -8
- package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +27 -20
- package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +28 -26
- package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +9 -6
- package/src/features/shared/hooks/analytics/useAnalytics.ts +1 -1
- package/src/features/shared/services/logger/analytics/__tests__/deriveActorType.test.ts +30 -0
- package/src/features/shared/services/logger/analytics/__tests__/mergeActorTypeIntoEvent.test.ts +83 -0
- package/src/features/shared/services/logger/analytics/analytics.ts +2 -24
- package/src/features/shared/services/logger/analytics/deriveActorType.ts +24 -0
- package/src/features/shared/services/logger/analytics/mergeActorTypeIntoEvent.ts +29 -0
- package/src/features/shared/services/logger/analytics/types.ts +5 -0
- package/src/features/shared/services/logger/analytics/useDataIngestionApi.ts +32 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/users/components/RolesMultiSelect/RolesMultiSelect.tsx +9 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
|
-
import { useDataIngestionApi } from "../../services/logger/analytics/
|
|
3
|
+
import { useDataIngestionApi } from "../../services/logger/analytics/useDataIngestionApi";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
AnalyticsTimer,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { deriveActorType } from "../deriveActorType";
|
|
4
|
+
|
|
5
|
+
describe("deriveActorType", () => {
|
|
6
|
+
it("returns internal for exact @vtex.com domain", () => {
|
|
7
|
+
expect(deriveActorType("rodrigo.tavares@vtex.com")).toBe("internal");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns internal for case-insensitive and plus-tagged vtex.com emails", () => {
|
|
11
|
+
expect(deriveActorType("Name+qa@VTEX.COM")).toBe("internal");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns external for non-vtex.com domains", () => {
|
|
15
|
+
expect(deriveActorType("buyer@acme.com")).toBe("external");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns external for vtex.com.br and subdomain lookalikes", () => {
|
|
19
|
+
expect(deriveActorType("user@vtex.com.br")).toBe("external");
|
|
20
|
+
expect(deriveActorType("user@corp.vtex.com")).toBe("external");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns external when email is missing, empty, or malformed", () => {
|
|
24
|
+
expect(deriveActorType(undefined)).toBe("external");
|
|
25
|
+
expect(deriveActorType(null)).toBe("external");
|
|
26
|
+
expect(deriveActorType("")).toBe("external");
|
|
27
|
+
expect(deriveActorType(" ")).toBe("external");
|
|
28
|
+
expect(deriveActorType("not-an-email")).toBe("external");
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/features/shared/services/logger/analytics/__tests__/mergeActorTypeIntoEvent.test.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as deriveActorTypeModule from "../deriveActorType";
|
|
4
|
+
import { mergeActorTypeIntoEvent } from "../mergeActorTypeIntoEvent";
|
|
5
|
+
|
|
6
|
+
import type { DataIngestionApiFields } from "../types";
|
|
7
|
+
|
|
8
|
+
describe("mergeActorTypeIntoEvent", () => {
|
|
9
|
+
it("attaches internal actor_type into metadata from a vtex.com email", () => {
|
|
10
|
+
const result = mergeActorTypeIntoEvent(
|
|
11
|
+
{ event_name: "budget_created" },
|
|
12
|
+
"alice@vtex.com"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(result.metadata?.actor_type).toBe("internal");
|
|
16
|
+
expect(result).not.toHaveProperty("actor_type");
|
|
17
|
+
expect(result).not.toHaveProperty("email");
|
|
18
|
+
expect(result).not.toHaveProperty("login");
|
|
19
|
+
expect(result).not.toHaveProperty("userId");
|
|
20
|
+
expect(result).not.toHaveProperty("sub");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("attaches external actor_type into metadata from a non-vtex email", () => {
|
|
24
|
+
const result = mergeActorTypeIntoEvent(
|
|
25
|
+
{ event_name: "budget_created" },
|
|
26
|
+
"buyer@acme.com"
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(result.metadata?.actor_type).toBe("external");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("defaults to external when email is unavailable", () => {
|
|
33
|
+
expect(
|
|
34
|
+
mergeActorTypeIntoEvent({ event_name: "budget_created" }, null).metadata
|
|
35
|
+
?.actor_type
|
|
36
|
+
).toBe("external");
|
|
37
|
+
expect(
|
|
38
|
+
mergeActorTypeIntoEvent({ event_name: "budget_created" }, undefined)
|
|
39
|
+
.metadata?.actor_type
|
|
40
|
+
).toBe("external");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("preserves other metadata fields while attaching actor_type", () => {
|
|
44
|
+
const fields: DataIngestionApiFields = {
|
|
45
|
+
event_name: "budget_created",
|
|
46
|
+
metadata: { foo: "bar" },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = mergeActorTypeIntoEvent(fields, "buyer@acme.com");
|
|
50
|
+
|
|
51
|
+
expect(result.metadata).toEqual({ foo: "bar", actor_type: "external" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("falls back to external when deriveActorType throws", () => {
|
|
55
|
+
vi.spyOn(deriveActorTypeModule, "deriveActorType").mockImplementation(
|
|
56
|
+
() => {
|
|
57
|
+
throw new Error("unexpected");
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
mergeActorTypeIntoEvent(
|
|
63
|
+
{ event_name: "budget_created" },
|
|
64
|
+
"alice@vtex.com"
|
|
65
|
+
).metadata?.actor_type
|
|
66
|
+
).toBe("external");
|
|
67
|
+
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not put the source email into the event payload", () => {
|
|
72
|
+
const email = "alice@vtex.com";
|
|
73
|
+
const result = mergeActorTypeIntoEvent(
|
|
74
|
+
{ event_name: "address_edited", event_category: "edition" },
|
|
75
|
+
email
|
|
76
|
+
);
|
|
77
|
+
const serialized = JSON.stringify(result);
|
|
78
|
+
|
|
79
|
+
expect(serialized).not.toContain(email);
|
|
80
|
+
expect(serialized).not.toContain("alice");
|
|
81
|
+
expect(result.metadata?.actor_type).toBe("internal");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -2,7 +2,7 @@ import storeConfig from "discovery.config";
|
|
|
2
2
|
|
|
3
3
|
import { isDevelopment } from "../../../utils/environment";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { DataIngestionApiParams } from "./types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* VTEX Data Ingestion API for Analytics
|
|
@@ -50,7 +50,7 @@ export async function dataIngestionApi(
|
|
|
50
50
|
/**
|
|
51
51
|
* Get common analytics context
|
|
52
52
|
*/
|
|
53
|
-
function getCommonAnalyticsContext() {
|
|
53
|
+
export function getCommonAnalyticsContext() {
|
|
54
54
|
const account = storeConfig?.api?.storeId || "unknown";
|
|
55
55
|
const locale =
|
|
56
56
|
typeof window !== "undefined"
|
|
@@ -77,25 +77,3 @@ function getCommonAnalyticsContext() {
|
|
|
77
77
|
device,
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Hook to send analytics events
|
|
83
|
-
* This is a simple version that will be extended by useAnalytics hook
|
|
84
|
-
*/
|
|
85
|
-
export function useDataIngestionApi() {
|
|
86
|
-
const commonFields = getCommonAnalyticsContext();
|
|
87
|
-
|
|
88
|
-
const sendEvent = (fields: DataIngestionApiFields) => {
|
|
89
|
-
// Get workspace from environment
|
|
90
|
-
const workspace = isDevelopment() ? "dev" : "master";
|
|
91
|
-
|
|
92
|
-
dataIngestionApi({
|
|
93
|
-
...commonFields,
|
|
94
|
-
event_category: fields.event_category || "click",
|
|
95
|
-
...fields,
|
|
96
|
-
workspace,
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return { sendEvent };
|
|
101
|
-
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ActorType } from "./types";
|
|
2
|
+
|
|
3
|
+
const INTERNAL_EMAIL_DOMAIN = "vtex.com";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive analytics actor_type from the logged-in user's email domain.
|
|
7
|
+
* Never logs or returns the input email — only the enum.
|
|
8
|
+
*/
|
|
9
|
+
export function deriveActorType(email?: string | null): ActorType {
|
|
10
|
+
if (!email) {
|
|
11
|
+
return "external";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const trimmed = email.trim().toLowerCase();
|
|
15
|
+
const atIndex = trimmed.lastIndexOf("@");
|
|
16
|
+
|
|
17
|
+
if (atIndex < 0 || atIndex === trimmed.length - 1) {
|
|
18
|
+
return "external";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const domain = trimmed.slice(atIndex + 1);
|
|
22
|
+
|
|
23
|
+
return domain === INTERNAL_EMAIL_DOMAIN ? "internal" : "external";
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { deriveActorType } from "./deriveActorType";
|
|
2
|
+
|
|
3
|
+
import type { ActorType, DataIngestionApiFields } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attach derived actor_type to analytics fields' metadata.
|
|
7
|
+
* Caller-supplied actor_type is ignored — derivation always wins.
|
|
8
|
+
* Never copies email or other identity into the payload.
|
|
9
|
+
*/
|
|
10
|
+
export function mergeActorTypeIntoEvent(
|
|
11
|
+
fields: DataIngestionApiFields,
|
|
12
|
+
email?: string | null
|
|
13
|
+
): DataIngestionApiFields {
|
|
14
|
+
let actorType: ActorType;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
actorType = deriveActorType(email);
|
|
18
|
+
} catch {
|
|
19
|
+
actorType = "external";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
...fields,
|
|
24
|
+
metadata: {
|
|
25
|
+
...fields.metadata,
|
|
26
|
+
actor_type: actorType,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import { BuyerPortalContext } from "../../../components/BuyerPortalProvider/BuyerPortalProvider";
|
|
4
|
+
import { isDevelopment } from "../../../utils/environment";
|
|
5
|
+
|
|
6
|
+
import { dataIngestionApi, getCommonAnalyticsContext } from "./analytics";
|
|
7
|
+
import { mergeActorTypeIntoEvent } from "./mergeActorTypeIntoEvent";
|
|
8
|
+
|
|
9
|
+
import type { DataIngestionApiFields } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to send analytics events with actor_type derived from the logged-in user.
|
|
13
|
+
*/
|
|
14
|
+
export function useDataIngestionApi() {
|
|
15
|
+
const buyerPortalContext = useContext(BuyerPortalContext);
|
|
16
|
+
const commonFields = getCommonAnalyticsContext();
|
|
17
|
+
const email = buyerPortalContext?.currentUser?.email;
|
|
18
|
+
|
|
19
|
+
const sendEvent = (fields: DataIngestionApiFields) => {
|
|
20
|
+
const workspace = isDevelopment() ? "dev" : "master";
|
|
21
|
+
const enrichedFields = mergeActorTypeIntoEvent(fields, email);
|
|
22
|
+
|
|
23
|
+
dataIngestionApi({
|
|
24
|
+
...commonFields,
|
|
25
|
+
event_category: enrichedFields.event_category || "click",
|
|
26
|
+
...enrichedFields,
|
|
27
|
+
workspace,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return { sendEvent };
|
|
32
|
+
}
|
|
@@ -13,6 +13,7 @@ import { Skeleton, Tag as RemovableTag } from "@faststore/ui";
|
|
|
13
13
|
import { Tag } from "../../../shared/components";
|
|
14
14
|
import { useAutocompletePosition } from "../../../shared/components/AutocompleteDropdown/useAutocompletePosition";
|
|
15
15
|
import { useDropdownFlipPosition } from "../../../shared/components/AutocompleteDropdown/useDropdownFlipPosition";
|
|
16
|
+
import { useLocalization } from "../../../shared/localization/LocalizationContext";
|
|
16
17
|
|
|
17
18
|
import {
|
|
18
19
|
filterRoleOptions,
|
|
@@ -43,6 +44,7 @@ export const RolesMultiSelect = ({
|
|
|
43
44
|
isLoading = false,
|
|
44
45
|
mode,
|
|
45
46
|
}: RolesMultiSelectProps) => {
|
|
47
|
+
const { t } = useLocalization();
|
|
46
48
|
const listboxId = useId();
|
|
47
49
|
const roleOptions = useMemo(() => options ?? [], [options]);
|
|
48
50
|
|
|
@@ -236,11 +238,9 @@ export const RolesMultiSelect = ({
|
|
|
236
238
|
onKeyDown={handleKeyDown}
|
|
237
239
|
/>
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
the raw key string if missing. */}
|
|
243
|
-
<label data-fs-bp-roles-multi-select-label>Roles</label>
|
|
241
|
+
<label data-fs-bp-roles-multi-select-label>
|
|
242
|
+
{t("users.forms.roles")}
|
|
243
|
+
</label>
|
|
244
244
|
</div>
|
|
245
245
|
|
|
246
246
|
{isOpen && (
|
|
@@ -254,10 +254,9 @@ export const RolesMultiSelect = ({
|
|
|
254
254
|
style={{ width: positionStyle.width }}
|
|
255
255
|
>
|
|
256
256
|
{filteredOptions.length === 0 ? (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<li data-fs-bp-roles-multi-select-empty>No matching roles</li>
|
|
257
|
+
<li data-fs-bp-roles-multi-select-empty>
|
|
258
|
+
{t("users.rolesMultiSelect.noMatchingRoles")}
|
|
259
|
+
</li>
|
|
261
260
|
) : (
|
|
262
261
|
filteredOptions.map((role, index) => {
|
|
263
262
|
const isChecked = selected.includes(role.roleId);
|
|
@@ -297,7 +296,7 @@ export const RolesMultiSelect = ({
|
|
|
297
296
|
</span>
|
|
298
297
|
{role.isCustom && (
|
|
299
298
|
<Tag data-fs-bp-roles-multi-select-option-custom-tag>
|
|
300
|
-
|
|
299
|
+
{t("users.rolesMultiSelect.customTag")}
|
|
301
300
|
</Tag>
|
|
302
301
|
)}
|
|
303
302
|
</div>
|