@jskit-ai/workspaces-core 0.2.6 → 0.2.8
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 +74 -0
- package/locales/en.json +57 -2
- package/package.json +9 -9
- package/src/server/types.js +1 -1
- package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +45 -3
- package/src/server/workspaceMembers/workspaceMembersService.js +12 -6
- package/src/shared/operationMessages.js +7 -3
- package/src/shared/resources/workspaceInvitesResource.js +5 -5
- package/src/shared/resources/workspaceMembersResource.js +14 -5
- package/src/shared/resources/workspaceMembershipsResource.js +5 -5
- package/src/shared/resources/workspacePendingInvitationsResource.js +10 -3
- package/src/shared/resources/workspaceResource.js +16 -6
- package/src/shared/resources/workspaceSettingsResource.js +107 -30
- package/test/roleLabels.test.js +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- The workspace, settings, member and invitation field messages are sent in the coded form `{ code, message, params }`, so their field errors translate. The English is unchanged.
|
|
6
|
+
- The resources' save, delete and validation feedback names catalog keys under `jskit.workspaces.feedback.*` instead of plain English; JSKIT's forms render them with `text()`, so the English shown is unchanged.
|
|
7
|
+
- The invitation email writes its expiry date with `Intl` in the recipient's locale and saved time zone (UTC without one), for example "November 2, 2026 at 12:00 AM UTC", instead of the raw ISO timestamp. An app's own `workspaces.invite-email-template` also receives the recipient's `timeZone` next to `locale`.
|
|
8
|
+
|
|
9
|
+
### New codes (21)
|
|
10
|
+
|
|
11
|
+
<details><summary>Code list</summary>
|
|
12
|
+
|
|
13
|
+
- `workspaces.workspace.avatarUrlInvalid`
|
|
14
|
+
- `workspaces.invite.decisionRequired`
|
|
15
|
+
- `workspaces.invite.decisionInvalid`
|
|
16
|
+
- `workspaces.settings.lightPrimaryColor.required`
|
|
17
|
+
- `workspaces.settings.lightPrimaryColor.invalid`
|
|
18
|
+
- `workspaces.settings.lightSecondaryColor.required`
|
|
19
|
+
- `workspaces.settings.lightSecondaryColor.invalid`
|
|
20
|
+
- `workspaces.settings.lightSurfaceColor.required`
|
|
21
|
+
- `workspaces.settings.lightSurfaceColor.invalid`
|
|
22
|
+
- `workspaces.settings.lightSurfaceVariantColor.required`
|
|
23
|
+
- `workspaces.settings.lightSurfaceVariantColor.invalid`
|
|
24
|
+
- `workspaces.settings.darkPrimaryColor.required`
|
|
25
|
+
- `workspaces.settings.darkPrimaryColor.invalid`
|
|
26
|
+
- `workspaces.settings.darkSecondaryColor.required`
|
|
27
|
+
- `workspaces.settings.darkSecondaryColor.invalid`
|
|
28
|
+
- `workspaces.settings.darkSurfaceColor.required`
|
|
29
|
+
- `workspaces.settings.darkSurfaceColor.invalid`
|
|
30
|
+
- `workspaces.settings.darkSurfaceVariantColor.required`
|
|
31
|
+
- `workspaces.settings.darkSurfaceVariantColor.invalid`
|
|
32
|
+
- `workspaces.settings.invitesEnabled.required`
|
|
33
|
+
- `workspaces.settings.invitesEnabled.invalid`
|
|
34
|
+
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
### New message keys (33)
|
|
38
|
+
|
|
39
|
+
<details><summary>Key list</summary>
|
|
40
|
+
|
|
41
|
+
- `jskit.workspaces.workspace.avatarUrlInvalid`
|
|
42
|
+
- `jskit.workspaces.invite.decisionRequired`
|
|
43
|
+
- `jskit.workspaces.invite.decisionInvalid`
|
|
44
|
+
- `jskit.workspaces.feedback.validationFailed`
|
|
45
|
+
- `jskit.workspaces.feedback.fixInvalidValues`
|
|
46
|
+
- `jskit.workspaces.feedback.recordSaved`
|
|
47
|
+
- `jskit.workspaces.feedback.recordSaveFailed`
|
|
48
|
+
- `jskit.workspaces.feedback.recordDeleted`
|
|
49
|
+
- `jskit.workspaces.feedback.recordDeleteFailed`
|
|
50
|
+
- `jskit.workspaces.feedback.workspaceInvalid`
|
|
51
|
+
- `jskit.workspaces.feedback.workspaceUpdated`
|
|
52
|
+
- `jskit.workspaces.feedback.workspaceUpdateFailed`
|
|
53
|
+
- `jskit.workspaces.feedback.settingsInvalid`
|
|
54
|
+
- `jskit.workspaces.feedback.settingsUpdated`
|
|
55
|
+
- `jskit.workspaces.feedback.settingsUpdateFailed`
|
|
56
|
+
- `jskit.workspaces.settings.lightPrimaryColor.required`
|
|
57
|
+
- `jskit.workspaces.settings.lightPrimaryColor.invalid`
|
|
58
|
+
- `jskit.workspaces.settings.lightSecondaryColor.required`
|
|
59
|
+
- `jskit.workspaces.settings.lightSecondaryColor.invalid`
|
|
60
|
+
- `jskit.workspaces.settings.lightSurfaceColor.required`
|
|
61
|
+
- `jskit.workspaces.settings.lightSurfaceColor.invalid`
|
|
62
|
+
- `jskit.workspaces.settings.lightSurfaceVariantColor.required`
|
|
63
|
+
- `jskit.workspaces.settings.lightSurfaceVariantColor.invalid`
|
|
64
|
+
- `jskit.workspaces.settings.darkPrimaryColor.required`
|
|
65
|
+
- `jskit.workspaces.settings.darkPrimaryColor.invalid`
|
|
66
|
+
- `jskit.workspaces.settings.darkSecondaryColor.required`
|
|
67
|
+
- `jskit.workspaces.settings.darkSecondaryColor.invalid`
|
|
68
|
+
- `jskit.workspaces.settings.darkSurfaceColor.required`
|
|
69
|
+
- `jskit.workspaces.settings.darkSurfaceColor.invalid`
|
|
70
|
+
- `jskit.workspaces.settings.darkSurfaceVariantColor.required`
|
|
71
|
+
- `jskit.workspaces.settings.darkSurfaceVariantColor.invalid`
|
|
72
|
+
- `jskit.workspaces.settings.invitesEnabled.required`
|
|
73
|
+
- `jskit.workspaces.settings.invitesEnabled.invalid`
|
|
74
|
+
|
|
75
|
+
</details>
|
|
76
|
+
|
|
3
77
|
## 0.2.0 - 2026-10-29
|
|
4
78
|
|
|
5
79
|
- Translation ready: every user-visible text is a message key with its English in `locales/en.json`, so an app translates it by supplying translation files only. With no translations, texts and behaviour are unchanged. See the guide "Translating a JSKIT app".
|
package/locales/en.json
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"contextDisabled": "Workspace context is disabled.",
|
|
13
13
|
"slugRequired": "workspaceSlug is required.",
|
|
14
14
|
"notFound": "Workspace not found.",
|
|
15
|
-
"accessDenied": "You do not have access to this workspace."
|
|
15
|
+
"accessDenied": "You do not have access to this workspace.",
|
|
16
|
+
"avatarUrlInvalid": "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
16
17
|
},
|
|
17
18
|
"member": {
|
|
18
19
|
"roleNotAssignable": "Role is not assignable.",
|
|
@@ -36,7 +37,9 @@
|
|
|
36
37
|
"emailMismatchAccount": "Invitation email does not match account email.",
|
|
37
38
|
"notAvailable": "Invitation is not available.",
|
|
38
39
|
"mailerUnconfigured": "No workspace invite mailer is configured.",
|
|
39
|
-
"sendFailed": "Unable to send workspace invite email."
|
|
40
|
+
"sendFailed": "Unable to send workspace invite email.",
|
|
41
|
+
"decisionRequired": "Decision is required.",
|
|
42
|
+
"decisionInvalid": "Decision must be accept or refuse."
|
|
40
43
|
},
|
|
41
44
|
"inviteEmail": {
|
|
42
45
|
"subject": "You're invited to {workspace}",
|
|
@@ -46,6 +49,58 @@
|
|
|
46
49
|
"openLink": "Open this link to accept the invitation:",
|
|
47
50
|
"accept": "Accept invitation",
|
|
48
51
|
"workspaceFallback": "the workspace"
|
|
52
|
+
},
|
|
53
|
+
"feedback": {
|
|
54
|
+
"validationFailed": "Validation failed.",
|
|
55
|
+
"fixInvalidValues": "Fix invalid values and try again.",
|
|
56
|
+
"recordSaved": "Record saved.",
|
|
57
|
+
"recordSaveFailed": "Unable to save record.",
|
|
58
|
+
"recordDeleted": "Record deleted.",
|
|
59
|
+
"recordDeleteFailed": "Unable to delete record.",
|
|
60
|
+
"workspaceInvalid": "Fix invalid workspace values and try again.",
|
|
61
|
+
"workspaceUpdated": "Workspace updated.",
|
|
62
|
+
"workspaceUpdateFailed": "Unable to update workspace.",
|
|
63
|
+
"settingsInvalid": "Fix invalid workspace settings values and try again.",
|
|
64
|
+
"settingsUpdated": "Workspace settings updated.",
|
|
65
|
+
"settingsUpdateFailed": "Unable to update workspace settings."
|
|
66
|
+
},
|
|
67
|
+
"settings": {
|
|
68
|
+
"lightPrimaryColor": {
|
|
69
|
+
"required": "Light primary color is required.",
|
|
70
|
+
"invalid": "Light primary color must be a hex color like #1867C0."
|
|
71
|
+
},
|
|
72
|
+
"lightSecondaryColor": {
|
|
73
|
+
"required": "Light secondary color is required.",
|
|
74
|
+
"invalid": "Light secondary color must be a hex color like #48A9A6."
|
|
75
|
+
},
|
|
76
|
+
"lightSurfaceColor": {
|
|
77
|
+
"required": "Light surface color is required.",
|
|
78
|
+
"invalid": "Light surface color must be a hex color like #FFFFFF."
|
|
79
|
+
},
|
|
80
|
+
"lightSurfaceVariantColor": {
|
|
81
|
+
"required": "Light surface variant color is required.",
|
|
82
|
+
"invalid": "Light surface variant color must be a hex color like #424242."
|
|
83
|
+
},
|
|
84
|
+
"darkPrimaryColor": {
|
|
85
|
+
"required": "Dark primary color is required.",
|
|
86
|
+
"invalid": "Dark primary color must be a hex color like #2196F3."
|
|
87
|
+
},
|
|
88
|
+
"darkSecondaryColor": {
|
|
89
|
+
"required": "Dark secondary color is required.",
|
|
90
|
+
"invalid": "Dark secondary color must be a hex color like #54B6B2."
|
|
91
|
+
},
|
|
92
|
+
"darkSurfaceColor": {
|
|
93
|
+
"required": "Dark surface color is required.",
|
|
94
|
+
"invalid": "Dark surface color must be a hex color like #212121."
|
|
95
|
+
},
|
|
96
|
+
"darkSurfaceVariantColor": {
|
|
97
|
+
"required": "Dark surface variant color is required.",
|
|
98
|
+
"invalid": "Dark surface variant color must be a hex color like #C8C8C8."
|
|
99
|
+
},
|
|
100
|
+
"invitesEnabled": {
|
|
101
|
+
"required": "invitesEnabled is required.",
|
|
102
|
+
"invalid": "invitesEnabled must be a boolean."
|
|
103
|
+
}
|
|
49
104
|
}
|
|
50
105
|
}
|
|
51
106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"./locales/en.json": "./locales/en.json"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
26
|
-
"@jskit-ai/json-rest-api-core": "0.2.
|
|
27
|
-
"@jskit-ai/resource-core": "0.1.
|
|
28
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
29
|
-
"@jskit-ai/users-core": "0.2.
|
|
25
|
+
"@jskit-ai/database-runtime": "0.1.256",
|
|
26
|
+
"@jskit-ai/json-rest-api-core": "0.2.7",
|
|
27
|
+
"@jskit-ai/resource-core": "0.1.198",
|
|
28
|
+
"@jskit-ai/resource-crud-core": "0.1.198",
|
|
29
|
+
"@jskit-ai/users-core": "0.2.8",
|
|
30
30
|
"json-rest-schema": "^1.0.17"
|
|
31
31
|
},
|
|
32
32
|
"description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
@@ -198,8 +198,8 @@
|
|
|
198
198
|
}
|
|
199
199
|
},
|
|
200
200
|
"peerDependencies": {
|
|
201
|
-
"@jskit-ai/auth-core": "0.2.
|
|
202
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
203
|
-
"@jskit-ai/kernel": "0.2.
|
|
201
|
+
"@jskit-ai/auth-core": "0.2.7",
|
|
202
|
+
"@jskit-ai/http-runtime": "0.2.7",
|
|
203
|
+
"@jskit-ai/kernel": "0.2.7"
|
|
204
204
|
}
|
|
205
205
|
}
|
package/src/server/types.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
/**
|
|
28
28
|
* The `workspaces.invite-email-template` capability: renders an invitation
|
|
29
29
|
* email's `{ subject, text, html }`.
|
|
30
|
-
* @typedef {(input: { email: string, inviteUrl: string, workspace: object, inviter: object | null, roleSid: string, roleLabel?: string, expiresAt: string, locale: string, t: (key: string, params?: object) => string }) => object | Promise<object>} WorkspaceInviteEmailTemplate
|
|
30
|
+
* @typedef {(input: { email: string, inviteUrl: string, workspace: object, inviter: object | null, roleSid: string, roleLabel?: string, expiresAt: string, locale: string, timeZone: string, t: (key: string, params?: object) => string }) => object | Promise<object>} WorkspaceInviteEmailTemplate
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -7,18 +7,60 @@ function escapeHtml(value) {
|
|
|
7
7
|
.replaceAll("'", "'");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const EXPIRY_FORMAT = Object.freeze({
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "long",
|
|
13
|
+
day: "numeric",
|
|
14
|
+
hour: "numeric",
|
|
15
|
+
minute: "2-digit",
|
|
16
|
+
timeZoneName: "short"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The expiry date in the recipient's language and time zone. A locale or time zone
|
|
21
|
+
* Intl does not know falls back to English or UTC; a value that is not a date is
|
|
22
|
+
* shown as sent.
|
|
23
|
+
* @param {string} expiresAt
|
|
24
|
+
* @param {string} locale
|
|
25
|
+
* @param {string} timeZone
|
|
26
|
+
*/
|
|
27
|
+
function formatInviteExpiry(expiresAt, locale, timeZone) {
|
|
28
|
+
const date = new Date(expiresAt);
|
|
29
|
+
if (!expiresAt || Number.isNaN(date.getTime())) {
|
|
30
|
+
return String(expiresAt || "");
|
|
31
|
+
}
|
|
32
|
+
for (const [tryLocale, tryTimeZone] of [[locale, timeZone], [locale, "UTC"], ["en", "UTC"]]) {
|
|
33
|
+
try {
|
|
34
|
+
return new Intl.DateTimeFormat(tryLocale || "en", { ...EXPIRY_FORMAT, timeZone: tryTimeZone || "UTC" }).format(date);
|
|
35
|
+
} catch {
|
|
36
|
+
// An unknown locale or time zone: try the next fallback.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return String(expiresAt);
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
/**
|
|
11
43
|
* The `workspaces.invite-email-template` default. t renders a message in the
|
|
12
44
|
* recipient's language. The markup stays here; each translated fragment,
|
|
13
45
|
* parameters included, is escaped before it enters the HTML.
|
|
14
46
|
* @type {import("../types.js").WorkspaceInviteEmailTemplate}
|
|
15
47
|
*/
|
|
16
|
-
function renderDefaultWorkspaceInviteEmail({
|
|
48
|
+
function renderDefaultWorkspaceInviteEmail({
|
|
49
|
+
inviteUrl,
|
|
50
|
+
workspace,
|
|
51
|
+
inviter,
|
|
52
|
+
roleSid,
|
|
53
|
+
roleLabel,
|
|
54
|
+
expiresAt,
|
|
55
|
+
locale = "en",
|
|
56
|
+
timeZone = "UTC",
|
|
57
|
+
t
|
|
58
|
+
}) {
|
|
17
59
|
const workspaceName =
|
|
18
60
|
String(workspace.name || workspace.slug || "").trim() || t("jskit.workspaces.inviteEmail.workspaceFallback");
|
|
19
61
|
const inviterName = String(inviter?.displayName || inviter?.email || "").trim();
|
|
20
62
|
const roleName = roleLabel || roleSid;
|
|
21
|
-
const expiryText = t("jskit.workspaces.inviteEmail.expiry", { expiresAt });
|
|
63
|
+
const expiryText = t("jskit.workspaces.inviteEmail.expiry", { expiresAt: formatInviteExpiry(expiresAt, locale, timeZone) });
|
|
22
64
|
const intro = inviterName
|
|
23
65
|
? t("jskit.workspaces.inviteEmail.introFromInviter", { inviter: inviterName, workspace: workspaceName, role: roleName })
|
|
24
66
|
: t("jskit.workspaces.inviteEmail.intro", { workspace: workspaceName, role: roleName });
|
|
@@ -38,4 +80,4 @@ function renderDefaultWorkspaceInviteEmail({ inviteUrl, workspace, inviter, role
|
|
|
38
80
|
};
|
|
39
81
|
}
|
|
40
82
|
|
|
41
|
-
export { renderDefaultWorkspaceInviteEmail };
|
|
83
|
+
export { formatInviteExpiry, renderDefaultWorkspaceInviteEmail };
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./unconfiguredWorkspaceInviteMailer.js";
|
|
12
12
|
|
|
13
13
|
const DEFAULT_LOCALE = "en";
|
|
14
|
+
const DEFAULT_TIME_ZONE = "UTC";
|
|
14
15
|
|
|
15
16
|
function roleNotAssignableError() {
|
|
16
17
|
return createValidationError({
|
|
@@ -128,15 +129,19 @@ function createService({
|
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
// The saved locale of an existing account with the invited email,
|
|
132
|
-
// A failed lookup must not stop the invitation, so it also
|
|
133
|
-
|
|
132
|
+
// The saved locale and time zone of an existing account with the invited email,
|
|
133
|
+
// else English and UTC. A failed lookup must not stop the invitation, so it also
|
|
134
|
+
// falls back to them.
|
|
135
|
+
async function resolveRecipientSettings(email, options = {}) {
|
|
134
136
|
try {
|
|
135
137
|
const profile = await userProfilesRepository.findByEmail(email, options);
|
|
136
138
|
const settings = profile?.id ? await userSettingsRepository.findByUserId(profile.id, options) : null;
|
|
137
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
locale: normalizeText(settings?.locale) || DEFAULT_LOCALE,
|
|
141
|
+
timeZone: normalizeText(settings?.timeZone) || DEFAULT_TIME_ZONE
|
|
142
|
+
};
|
|
138
143
|
} catch {
|
|
139
|
-
return DEFAULT_LOCALE;
|
|
144
|
+
return { locale: DEFAULT_LOCALE, timeZone: DEFAULT_TIME_ZONE };
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -148,7 +153,7 @@ function createService({
|
|
|
148
153
|
}
|
|
149
154
|
try {
|
|
150
155
|
const roleLabel = roleCatalog.roles.find((role) => role.id === invite.roleSid)?.label;
|
|
151
|
-
const locale = await
|
|
156
|
+
const { locale, timeZone } = await resolveRecipientSettings(invite.email, options);
|
|
152
157
|
const message = await workspaceInviteEmailTemplate({
|
|
153
158
|
email: invite.email,
|
|
154
159
|
inviteUrl,
|
|
@@ -158,6 +163,7 @@ function createService({
|
|
|
158
163
|
...(roleLabel ? { roleLabel } : {}),
|
|
159
164
|
expiresAt: invite.expiresAt,
|
|
160
165
|
locale,
|
|
166
|
+
timeZone,
|
|
161
167
|
t: (key, params) => i18n.t(locale, key, params)
|
|
162
168
|
});
|
|
163
169
|
const result = await workspaceInviteMailer.sendWorkspaceInvite({
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The messages a resource's operations show when their input fails validation:
|
|
3
|
+
* validation is a message key the client renders; apiValidation is the API's
|
|
4
|
+
* English with its code.
|
|
5
|
+
*/
|
|
2
6
|
const OPERATION_MESSAGES = Object.freeze({
|
|
3
|
-
validation: "
|
|
4
|
-
apiValidation: "Validation failed."
|
|
7
|
+
validation: "jskit.workspaces.feedback.validationFailed",
|
|
8
|
+
apiValidation: Object.freeze({ code: "validation_failed", message: "Validation failed." })
|
|
5
9
|
});
|
|
6
10
|
|
|
7
11
|
export { OPERATION_MESSAGES };
|
|
@@ -150,11 +150,11 @@ const workspaceInvitesResource = defineCrudResource({
|
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
152
|
messages: {
|
|
153
|
-
validation: "
|
|
154
|
-
saveSuccess: "
|
|
155
|
-
saveError: "
|
|
156
|
-
deleteSuccess: "
|
|
157
|
-
deleteError: "
|
|
153
|
+
validation: "jskit.workspaces.feedback.fixInvalidValues",
|
|
154
|
+
saveSuccess: "jskit.workspaces.feedback.recordSaved",
|
|
155
|
+
saveError: "jskit.workspaces.feedback.recordSaveFailed",
|
|
156
|
+
deleteSuccess: "jskit.workspaces.feedback.recordDeleted",
|
|
157
|
+
deleteError: "jskit.workspaces.feedback.recordDeleteFailed"
|
|
158
158
|
},
|
|
159
159
|
crudOperations: ["list", "view", "create", "patch"]
|
|
160
160
|
});
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
2
2
|
import { createSchema } from "json-rest-schema";
|
|
3
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
3
4
|
import { defineResource } from "@jskit-ai/resource-core/shared/resource";
|
|
4
5
|
import { OPERATION_MESSAGES } from "../operationMessages.js";
|
|
5
6
|
import { workspaceRoleCatalogSchema } from "./workspaceRoleCatalogSchema.js";
|
|
6
7
|
|
|
8
|
+
// Each field message: its English and the code a client renders it from in the user's language.
|
|
9
|
+
const WORKSPACE_MEMBERS_FIELD_MESSAGES = deepFreeze({
|
|
10
|
+
inviteTokenRequired: { code: "workspaces.invite.tokenRequired", message: "Invite token is required." },
|
|
11
|
+
inviteTokenInvalid: { code: "workspaces.invite.tokenInvalid", message: "Invite token is invalid." },
|
|
12
|
+
decisionRequired: { code: "workspaces.invite.decisionRequired", message: "Decision is required." },
|
|
13
|
+
decisionInvalid: { code: "workspaces.invite.decisionInvalid", message: "Decision must be accept or refuse." }
|
|
14
|
+
});
|
|
15
|
+
|
|
7
16
|
const workspaceSummaryOutputSchema = createSchema({
|
|
8
17
|
id: { type: "string", required: true, minLength: 1, pattern: RECORD_ID_PATTERN },
|
|
9
18
|
slug: { type: "string", required: true, minLength: 1, maxLength: 120 },
|
|
@@ -180,9 +189,9 @@ const redeemInviteBodySchema = createSchema({
|
|
|
180
189
|
required: true,
|
|
181
190
|
minLength: 1,
|
|
182
191
|
messages: {
|
|
183
|
-
required:
|
|
184
|
-
minLength:
|
|
185
|
-
default:
|
|
192
|
+
required: WORKSPACE_MEMBERS_FIELD_MESSAGES.inviteTokenRequired,
|
|
193
|
+
minLength: WORKSPACE_MEMBERS_FIELD_MESSAGES.inviteTokenRequired,
|
|
194
|
+
default: WORKSPACE_MEMBERS_FIELD_MESSAGES.inviteTokenInvalid
|
|
186
195
|
}
|
|
187
196
|
},
|
|
188
197
|
decision: {
|
|
@@ -190,8 +199,8 @@ const redeemInviteBodySchema = createSchema({
|
|
|
190
199
|
required: true,
|
|
191
200
|
enum: ["accept", "refuse"],
|
|
192
201
|
messages: {
|
|
193
|
-
required:
|
|
194
|
-
default:
|
|
202
|
+
required: WORKSPACE_MEMBERS_FIELD_MESSAGES.decisionRequired,
|
|
203
|
+
default: WORKSPACE_MEMBERS_FIELD_MESSAGES.decisionInvalid
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
});
|
|
@@ -87,11 +87,11 @@ const workspaceMembershipsResource = defineCrudResource({
|
|
|
87
87
|
}
|
|
88
88
|
},
|
|
89
89
|
messages: {
|
|
90
|
-
validation: "
|
|
91
|
-
saveSuccess: "
|
|
92
|
-
saveError: "
|
|
93
|
-
deleteSuccess: "
|
|
94
|
-
deleteError: "
|
|
90
|
+
validation: "jskit.workspaces.feedback.fixInvalidValues",
|
|
91
|
+
saveSuccess: "jskit.workspaces.feedback.recordSaved",
|
|
92
|
+
saveError: "jskit.workspaces.feedback.recordSaveFailed",
|
|
93
|
+
deleteSuccess: "jskit.workspaces.feedback.recordDeleted",
|
|
94
|
+
deleteError: "jskit.workspaces.feedback.recordDeleteFailed"
|
|
95
95
|
},
|
|
96
96
|
crudOperations: ["list", "view", "create", "patch"]
|
|
97
97
|
});
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
2
2
|
import { createSchema } from "json-rest-schema";
|
|
3
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
3
4
|
import { defineResource } from "@jskit-ai/resource-core/shared/resource";
|
|
4
5
|
import { OPERATION_MESSAGES } from "../operationMessages.js";
|
|
5
6
|
|
|
7
|
+
// Each field message: its English and the code a client renders it from in the user's language.
|
|
8
|
+
const WORKSPACE_PENDING_INVITATIONS_FIELD_MESSAGES = deepFreeze({
|
|
9
|
+
inviteTokenRequired: { code: "workspaces.invite.tokenRequired", message: "Invite token is required." },
|
|
10
|
+
inviteTokenInvalid: { code: "workspaces.invite.tokenInvalid", message: "Invite token is invalid." }
|
|
11
|
+
});
|
|
12
|
+
|
|
6
13
|
const invitationTokenQuerySchema = createSchema({
|
|
7
14
|
token: {
|
|
8
15
|
type: "string",
|
|
9
16
|
required: true,
|
|
10
17
|
minLength: 1,
|
|
11
18
|
messages: {
|
|
12
|
-
required:
|
|
13
|
-
minLength:
|
|
14
|
-
default:
|
|
19
|
+
required: WORKSPACE_PENDING_INVITATIONS_FIELD_MESSAGES.inviteTokenRequired,
|
|
20
|
+
minLength: WORKSPACE_PENDING_INVITATIONS_FIELD_MESSAGES.inviteTokenRequired,
|
|
21
|
+
default: WORKSPACE_PENDING_INVITATIONS_FIELD_MESSAGES.inviteTokenInvalid
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
2
3
|
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
4
|
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
4
5
|
import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
|
|
5
6
|
import { createSchemaDefinition } from "@jskit-ai/resource-core/shared/resource";
|
|
7
|
+
import { OPERATION_MESSAGES } from "../operationMessages.js";
|
|
8
|
+
|
|
9
|
+
// Each field message: its English and the code a client renders it from in the user's language.
|
|
10
|
+
const WORKSPACE_FIELD_MESSAGES = deepFreeze({
|
|
11
|
+
avatarUrlInvalid: {
|
|
12
|
+
code: "workspaces.workspace.avatarUrlInvalid",
|
|
13
|
+
message: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
14
|
+
}
|
|
15
|
+
});
|
|
6
16
|
|
|
7
17
|
function normalizeWorkspaceAvatarUrl(value) {
|
|
8
18
|
const avatarUrl = normalizeText(value);
|
|
@@ -50,8 +60,8 @@ const workspacePatchBodySchema = createSchema({
|
|
|
50
60
|
required: false,
|
|
51
61
|
pattern: "^(https?://.+)?$",
|
|
52
62
|
messages: {
|
|
53
|
-
pattern:
|
|
54
|
-
default:
|
|
63
|
+
pattern: WORKSPACE_FIELD_MESSAGES.avatarUrlInvalid,
|
|
64
|
+
default: WORKSPACE_FIELD_MESSAGES.avatarUrlInvalid
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
});
|
|
@@ -133,10 +143,10 @@ const resource = defineCrudResource({
|
|
|
133
143
|
}
|
|
134
144
|
},
|
|
135
145
|
messages: {
|
|
136
|
-
validation: "
|
|
137
|
-
saveSuccess: "
|
|
138
|
-
saveError: "
|
|
139
|
-
apiValidation:
|
|
146
|
+
validation: "jskit.workspaces.feedback.workspaceInvalid",
|
|
147
|
+
saveSuccess: "jskit.workspaces.feedback.workspaceUpdated",
|
|
148
|
+
saveError: "jskit.workspaces.feedback.workspaceUpdateFailed",
|
|
149
|
+
apiValidation: OPERATION_MESSAGES.apiValidation
|
|
140
150
|
},
|
|
141
151
|
crudOperations: ["view", "list", "create", "replace", "patch"],
|
|
142
152
|
crud: {
|
|
@@ -9,6 +9,83 @@ import {
|
|
|
9
9
|
DEFAULT_WORKSPACE_LIGHT_PALETTE,
|
|
10
10
|
WORKSPACE_SETTINGS_COLOR_FIELDS
|
|
11
11
|
} from "../settings.js";
|
|
12
|
+
import { OPERATION_MESSAGES } from "../operationMessages.js";
|
|
13
|
+
|
|
14
|
+
// Each field message: its English and the code a client renders it from in the user's language.
|
|
15
|
+
const WORKSPACE_SETTINGS_FIELD_MESSAGES = deepFreeze({
|
|
16
|
+
lightPrimaryColorRequired: {
|
|
17
|
+
code: "workspaces.settings.lightPrimaryColor.required",
|
|
18
|
+
message: "Light primary color is required."
|
|
19
|
+
},
|
|
20
|
+
lightPrimaryColorInvalid: {
|
|
21
|
+
code: "workspaces.settings.lightPrimaryColor.invalid",
|
|
22
|
+
message: "Light primary color must be a hex color like #1867C0."
|
|
23
|
+
},
|
|
24
|
+
lightSecondaryColorRequired: {
|
|
25
|
+
code: "workspaces.settings.lightSecondaryColor.required",
|
|
26
|
+
message: "Light secondary color is required."
|
|
27
|
+
},
|
|
28
|
+
lightSecondaryColorInvalid: {
|
|
29
|
+
code: "workspaces.settings.lightSecondaryColor.invalid",
|
|
30
|
+
message: "Light secondary color must be a hex color like #48A9A6."
|
|
31
|
+
},
|
|
32
|
+
lightSurfaceColorRequired: {
|
|
33
|
+
code: "workspaces.settings.lightSurfaceColor.required",
|
|
34
|
+
message: "Light surface color is required."
|
|
35
|
+
},
|
|
36
|
+
lightSurfaceColorInvalid: {
|
|
37
|
+
code: "workspaces.settings.lightSurfaceColor.invalid",
|
|
38
|
+
message: "Light surface color must be a hex color like #FFFFFF."
|
|
39
|
+
},
|
|
40
|
+
lightSurfaceVariantColorRequired: {
|
|
41
|
+
code: "workspaces.settings.lightSurfaceVariantColor.required",
|
|
42
|
+
message: "Light surface variant color is required."
|
|
43
|
+
},
|
|
44
|
+
lightSurfaceVariantColorInvalid: {
|
|
45
|
+
code: "workspaces.settings.lightSurfaceVariantColor.invalid",
|
|
46
|
+
message: "Light surface variant color must be a hex color like #424242."
|
|
47
|
+
},
|
|
48
|
+
darkPrimaryColorRequired: {
|
|
49
|
+
code: "workspaces.settings.darkPrimaryColor.required",
|
|
50
|
+
message: "Dark primary color is required."
|
|
51
|
+
},
|
|
52
|
+
darkPrimaryColorInvalid: {
|
|
53
|
+
code: "workspaces.settings.darkPrimaryColor.invalid",
|
|
54
|
+
message: "Dark primary color must be a hex color like #2196F3."
|
|
55
|
+
},
|
|
56
|
+
darkSecondaryColorRequired: {
|
|
57
|
+
code: "workspaces.settings.darkSecondaryColor.required",
|
|
58
|
+
message: "Dark secondary color is required."
|
|
59
|
+
},
|
|
60
|
+
darkSecondaryColorInvalid: {
|
|
61
|
+
code: "workspaces.settings.darkSecondaryColor.invalid",
|
|
62
|
+
message: "Dark secondary color must be a hex color like #54B6B2."
|
|
63
|
+
},
|
|
64
|
+
darkSurfaceColorRequired: {
|
|
65
|
+
code: "workspaces.settings.darkSurfaceColor.required",
|
|
66
|
+
message: "Dark surface color is required."
|
|
67
|
+
},
|
|
68
|
+
darkSurfaceColorInvalid: {
|
|
69
|
+
code: "workspaces.settings.darkSurfaceColor.invalid",
|
|
70
|
+
message: "Dark surface color must be a hex color like #212121."
|
|
71
|
+
},
|
|
72
|
+
darkSurfaceVariantColorRequired: {
|
|
73
|
+
code: "workspaces.settings.darkSurfaceVariantColor.required",
|
|
74
|
+
message: "Dark surface variant color is required."
|
|
75
|
+
},
|
|
76
|
+
darkSurfaceVariantColorInvalid: {
|
|
77
|
+
code: "workspaces.settings.darkSurfaceVariantColor.invalid",
|
|
78
|
+
message: "Dark surface variant color must be a hex color like #C8C8C8."
|
|
79
|
+
},
|
|
80
|
+
invitesEnabledRequired: {
|
|
81
|
+
code: "workspaces.settings.invitesEnabled.required",
|
|
82
|
+
message: "invitesEnabled is required."
|
|
83
|
+
},
|
|
84
|
+
invitesEnabledInvalid: {
|
|
85
|
+
code: "workspaces.settings.invitesEnabled.invalid",
|
|
86
|
+
message: "invitesEnabled must be a boolean."
|
|
87
|
+
}
|
|
88
|
+
});
|
|
12
89
|
|
|
13
90
|
const WORKSPACE_SETTINGS_FIELD_KEYS = deepFreeze([...WORKSPACE_SETTINGS_COLOR_FIELDS, "invitesEnabled"]);
|
|
14
91
|
|
|
@@ -20,9 +97,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
20
97
|
maxLength: 7,
|
|
21
98
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
22
99
|
messages: {
|
|
23
|
-
required:
|
|
24
|
-
pattern:
|
|
25
|
-
default:
|
|
100
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightPrimaryColorRequired,
|
|
101
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightPrimaryColorInvalid,
|
|
102
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightPrimaryColorInvalid
|
|
26
103
|
}
|
|
27
104
|
},
|
|
28
105
|
lightSecondaryColor: {
|
|
@@ -32,9 +109,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
32
109
|
maxLength: 7,
|
|
33
110
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
34
111
|
messages: {
|
|
35
|
-
required:
|
|
36
|
-
pattern:
|
|
37
|
-
default:
|
|
112
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSecondaryColorRequired,
|
|
113
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSecondaryColorInvalid,
|
|
114
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSecondaryColorInvalid
|
|
38
115
|
}
|
|
39
116
|
},
|
|
40
117
|
lightSurfaceColor: {
|
|
@@ -44,9 +121,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
44
121
|
maxLength: 7,
|
|
45
122
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
46
123
|
messages: {
|
|
47
|
-
required:
|
|
48
|
-
pattern:
|
|
49
|
-
default:
|
|
124
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceColorRequired,
|
|
125
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceColorInvalid,
|
|
126
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceColorInvalid
|
|
50
127
|
}
|
|
51
128
|
},
|
|
52
129
|
lightSurfaceVariantColor: {
|
|
@@ -56,9 +133,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
56
133
|
maxLength: 7,
|
|
57
134
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
58
135
|
messages: {
|
|
59
|
-
required:
|
|
60
|
-
pattern:
|
|
61
|
-
default:
|
|
136
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceVariantColorRequired,
|
|
137
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceVariantColorInvalid,
|
|
138
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.lightSurfaceVariantColorInvalid
|
|
62
139
|
}
|
|
63
140
|
},
|
|
64
141
|
darkPrimaryColor: {
|
|
@@ -68,9 +145,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
68
145
|
maxLength: 7,
|
|
69
146
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
70
147
|
messages: {
|
|
71
|
-
required:
|
|
72
|
-
pattern:
|
|
73
|
-
default:
|
|
148
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkPrimaryColorRequired,
|
|
149
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkPrimaryColorInvalid,
|
|
150
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkPrimaryColorInvalid
|
|
74
151
|
}
|
|
75
152
|
},
|
|
76
153
|
darkSecondaryColor: {
|
|
@@ -80,9 +157,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
80
157
|
maxLength: 7,
|
|
81
158
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
82
159
|
messages: {
|
|
83
|
-
required:
|
|
84
|
-
pattern:
|
|
85
|
-
default:
|
|
160
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSecondaryColorRequired,
|
|
161
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSecondaryColorInvalid,
|
|
162
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSecondaryColorInvalid
|
|
86
163
|
}
|
|
87
164
|
},
|
|
88
165
|
darkSurfaceColor: {
|
|
@@ -92,9 +169,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
92
169
|
maxLength: 7,
|
|
93
170
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
94
171
|
messages: {
|
|
95
|
-
required:
|
|
96
|
-
pattern:
|
|
97
|
-
default:
|
|
172
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceColorRequired,
|
|
173
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceColorInvalid,
|
|
174
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceColorInvalid
|
|
98
175
|
}
|
|
99
176
|
},
|
|
100
177
|
darkSurfaceVariantColor: {
|
|
@@ -104,9 +181,9 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
104
181
|
maxLength: 7,
|
|
105
182
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
106
183
|
messages: {
|
|
107
|
-
required:
|
|
108
|
-
pattern:
|
|
109
|
-
default:
|
|
184
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceVariantColorRequired,
|
|
185
|
+
pattern: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceVariantColorInvalid,
|
|
186
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.darkSurfaceVariantColorInvalid
|
|
110
187
|
}
|
|
111
188
|
},
|
|
112
189
|
invitesEnabled: {
|
|
@@ -114,8 +191,8 @@ const workspaceSettingsBodySchema = createSchema({
|
|
|
114
191
|
required: true,
|
|
115
192
|
strictBoolean: true,
|
|
116
193
|
messages: {
|
|
117
|
-
required:
|
|
118
|
-
default:
|
|
194
|
+
required: WORKSPACE_SETTINGS_FIELD_MESSAGES.invitesEnabledRequired,
|
|
195
|
+
default: WORKSPACE_SETTINGS_FIELD_MESSAGES.invitesEnabledInvalid
|
|
119
196
|
}
|
|
120
197
|
}
|
|
121
198
|
});
|
|
@@ -273,10 +350,10 @@ const workspaceSettingsResource = defineCrudResource({
|
|
|
273
350
|
}
|
|
274
351
|
},
|
|
275
352
|
messages: {
|
|
276
|
-
validation: "
|
|
277
|
-
saveSuccess: "
|
|
278
|
-
saveError: "
|
|
279
|
-
apiValidation:
|
|
353
|
+
validation: "jskit.workspaces.feedback.settingsInvalid",
|
|
354
|
+
saveSuccess: "jskit.workspaces.feedback.settingsUpdated",
|
|
355
|
+
saveError: "jskit.workspaces.feedback.settingsUpdateFailed",
|
|
356
|
+
apiValidation: OPERATION_MESSAGES.apiValidation
|
|
280
357
|
},
|
|
281
358
|
crudOperations: ["view", "list", "create", "replace", "patch"],
|
|
282
359
|
crud: {
|
package/test/roleLabels.test.js
CHANGED
|
@@ -11,7 +11,10 @@ import { createWorkspacesServerI18n } from "../src/server/i18n.js";
|
|
|
11
11
|
import { noInviteAcceptanceParticipant } from "../src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js";
|
|
12
12
|
import { createService as createSettingsService } from "../src/server/workspaceSettings/workspaceSettingsService.js";
|
|
13
13
|
import { createService as createPendingService } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsService.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatInviteExpiry,
|
|
16
|
+
renderDefaultWorkspaceInviteEmail
|
|
17
|
+
} from "../src/server/workspaceMembers/defaultWorkspaceInviteEmail.js";
|
|
15
18
|
import { createWorkspaceInviteUrlBuilder } from "../src/server/workspaceMembers/workspaceInviteUrls.js";
|
|
16
19
|
import { unconfiguredWorkspaceInviteMailer } from "../src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js";
|
|
17
20
|
|
|
@@ -155,3 +158,33 @@ test("default invitation email uses the configured label with HTML escaping and
|
|
|
155
158
|
assert.match(labelled.html, /as Member <team>/u);
|
|
156
159
|
assert.match(renderDefaultWorkspaceInviteEmail(input).text, /as worker/u);
|
|
157
160
|
});
|
|
161
|
+
|
|
162
|
+
test("default invitation email writes the expiry as a date in the recipient's locale and time zone", () => {
|
|
163
|
+
const i18n = createWorkspacesServerI18n();
|
|
164
|
+
const message = renderDefaultWorkspaceInviteEmail({
|
|
165
|
+
email: "reader@example.test",
|
|
166
|
+
inviteUrl: "/invite/abc",
|
|
167
|
+
workspace: { name: "Acme", slug: "acme" },
|
|
168
|
+
inviter: null,
|
|
169
|
+
roleSid: "worker",
|
|
170
|
+
expiresAt: "2026-11-02T00:00:00.000Z",
|
|
171
|
+
locale: "en",
|
|
172
|
+
timeZone: "UTC",
|
|
173
|
+
t: (key, params) => i18n.t("en", key, params)
|
|
174
|
+
});
|
|
175
|
+
assert.match(message.text, /November 2, 2026/u);
|
|
176
|
+
assert.doesNotMatch(message.text, /2026-11-02T00:00:00\.000Z/u);
|
|
177
|
+
|
|
178
|
+
assert.match(formatInviteExpiry("2026-11-02T00:00:00.000Z", "it", "Europe/Rome"), /2 novembre 2026/u);
|
|
179
|
+
assert.match(formatInviteExpiry("2026-11-02T00:00:00.000Z", "en", "America/New_York"), /November 1, 2026/u);
|
|
180
|
+
// An unknown time zone or locale falls back to UTC or English; a value that is not a date is shown as sent.
|
|
181
|
+
assert.equal(
|
|
182
|
+
formatInviteExpiry("2026-11-02T00:00:00.000Z", "en", "Not/AZone"),
|
|
183
|
+
formatInviteExpiry("2026-11-02T00:00:00.000Z", "en", "UTC")
|
|
184
|
+
);
|
|
185
|
+
assert.equal(
|
|
186
|
+
formatInviteExpiry("2026-11-02T00:00:00.000Z", "not a locale!", "UTC"),
|
|
187
|
+
formatInviteExpiry("2026-11-02T00:00:00.000Z", "en", "UTC")
|
|
188
|
+
);
|
|
189
|
+
assert.equal(formatInviteExpiry("soon", "en", "UTC"), "soon");
|
|
190
|
+
});
|