@oneuptime/common 7.0.5058 → 7.0.5068
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/Models/DatabaseModels/Project.ts +78 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1754671483948-MigrationName.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/BillingService.ts +88 -0
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/StatusPageService.ts +19 -3
- package/Server/Types/Markdown.ts +17 -17
- package/UI/Components/Detail/Detail.tsx +2 -3
- package/UI/Utils/Countries.ts +259 -0
- package/build/dist/Models/DatabaseModels/Project.js +81 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754671483948-MigrationName.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754671483948-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756293325324-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756296282627-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1756300358095-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +76 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +23 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +11 -2
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +16 -16
- package/build/dist/UI/Components/Detail/Detail.js +2 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Utils/Countries.js +251 -0
- package/build/dist/UI/Utils/Countries.js.map +1 -0
- package/package.json +1 -1
|
@@ -248,6 +248,84 @@ export default class Project extends TenantModel {
|
|
|
248
248
|
})
|
|
249
249
|
public paymentProviderCustomerId?: string = undefined;
|
|
250
250
|
|
|
251
|
+
@ColumnAccessControl({
|
|
252
|
+
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
253
|
+
read: [
|
|
254
|
+
Permission.ProjectOwner,
|
|
255
|
+
Permission.ProjectAdmin,
|
|
256
|
+
Permission.ProjectMember,
|
|
257
|
+
Permission.ReadProject,
|
|
258
|
+
Permission.UnAuthorizedSsoUser,
|
|
259
|
+
Permission.ProjectUser,
|
|
260
|
+
],
|
|
261
|
+
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
262
|
+
})
|
|
263
|
+
@TableColumn({
|
|
264
|
+
type: TableColumnType.LongText,
|
|
265
|
+
title: "Business Details / Billing Address",
|
|
266
|
+
description:
|
|
267
|
+
"Business legal name, address and any tax information to appear on invoices.",
|
|
268
|
+
})
|
|
269
|
+
@Column({
|
|
270
|
+
type: ColumnType.LongText,
|
|
271
|
+
length: ColumnLength.LongText,
|
|
272
|
+
nullable: true,
|
|
273
|
+
unique: false,
|
|
274
|
+
})
|
|
275
|
+
public businessDetails?: string = undefined;
|
|
276
|
+
|
|
277
|
+
@ColumnAccessControl({
|
|
278
|
+
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
279
|
+
read: [
|
|
280
|
+
Permission.ProjectOwner,
|
|
281
|
+
Permission.ProjectAdmin,
|
|
282
|
+
Permission.ProjectMember,
|
|
283
|
+
Permission.ReadProject,
|
|
284
|
+
Permission.UnAuthorizedSsoUser,
|
|
285
|
+
Permission.ProjectUser,
|
|
286
|
+
],
|
|
287
|
+
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
288
|
+
})
|
|
289
|
+
@TableColumn({
|
|
290
|
+
type: TableColumnType.ShortText,
|
|
291
|
+
title: "Business Country (ISO Alpha-2)",
|
|
292
|
+
description:
|
|
293
|
+
"Two-letter ISO country code for billing address (e.g., US, GB, DE).",
|
|
294
|
+
})
|
|
295
|
+
@Column({
|
|
296
|
+
type: ColumnType.ShortText,
|
|
297
|
+
length: ColumnLength.ShortText,
|
|
298
|
+
nullable: true,
|
|
299
|
+
unique: false,
|
|
300
|
+
})
|
|
301
|
+
public businessDetailsCountry?: string = undefined;
|
|
302
|
+
|
|
303
|
+
@ColumnAccessControl({
|
|
304
|
+
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
305
|
+
read: [
|
|
306
|
+
Permission.ProjectOwner,
|
|
307
|
+
Permission.ProjectAdmin,
|
|
308
|
+
Permission.ProjectMember,
|
|
309
|
+
Permission.ReadProject,
|
|
310
|
+
Permission.UnAuthorizedSsoUser,
|
|
311
|
+
Permission.ProjectUser,
|
|
312
|
+
],
|
|
313
|
+
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
314
|
+
})
|
|
315
|
+
@TableColumn({
|
|
316
|
+
type: TableColumnType.Email,
|
|
317
|
+
title: "Finance / Accounting Email",
|
|
318
|
+
description:
|
|
319
|
+
"Invoices, receipts and billing related notifications will be sent to this email in addition to project owner.",
|
|
320
|
+
})
|
|
321
|
+
@Column({
|
|
322
|
+
type: ColumnType.Email,
|
|
323
|
+
length: ColumnLength.Email,
|
|
324
|
+
nullable: true,
|
|
325
|
+
unique: false,
|
|
326
|
+
})
|
|
327
|
+
public financeAccountingEmail?: string = undefined;
|
|
328
|
+
|
|
251
329
|
@ColumnAccessControl({
|
|
252
330
|
create: [],
|
|
253
331
|
read: [
|
|
@@ -68,6 +68,28 @@ export class MigrationName1754671483948 implements MigrationInterface {
|
|
|
68
68
|
await queryRunner.query(
|
|
69
69
|
`ALTER TABLE "StatusPageAnnouncement" ADD "subscriberNotificationStatusMessage" text`,
|
|
70
70
|
);
|
|
71
|
+
// Set all existing rows' subscriber notification statuses to 'Success' since they were previously considered notified
|
|
72
|
+
await queryRunner.query(
|
|
73
|
+
`UPDATE "Incident" SET "subscriberNotificationStatusOnIncidentCreated"='Success'`,
|
|
74
|
+
);
|
|
75
|
+
await queryRunner.query(
|
|
76
|
+
`UPDATE "IncidentPublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
|
|
77
|
+
);
|
|
78
|
+
await queryRunner.query(
|
|
79
|
+
`UPDATE "IncidentStateTimeline" SET "subscriberNotificationStatus"='Success'`,
|
|
80
|
+
);
|
|
81
|
+
await queryRunner.query(
|
|
82
|
+
`UPDATE "ScheduledMaintenance" SET "subscriberNotificationStatusOnEventScheduled"='Success'`,
|
|
83
|
+
);
|
|
84
|
+
await queryRunner.query(
|
|
85
|
+
`UPDATE "ScheduledMaintenancePublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
|
|
86
|
+
);
|
|
87
|
+
await queryRunner.query(
|
|
88
|
+
`UPDATE "ScheduledMaintenanceStateTimeline" SET "subscriberNotificationStatus"='Success'`,
|
|
89
|
+
);
|
|
90
|
+
await queryRunner.query(
|
|
91
|
+
`UPDATE "StatusPageAnnouncement" SET "subscriberNotificationStatus"='Success'`,
|
|
92
|
+
);
|
|
71
93
|
await queryRunner.query(
|
|
72
94
|
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
|
73
95
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1756293325324 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1756293325324";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Project" ADD "businessDetails" character varying(500)`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "Project" DROP COLUMN "businessDetails"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1756296282627 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1756296282627";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Project" ADD "businessDetailsCountry" character varying(100)`,
|
|
9
|
+
);
|
|
10
|
+
await queryRunner.query(
|
|
11
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
|
12
|
+
);
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
19
|
+
await queryRunner.query(
|
|
20
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
|
21
|
+
);
|
|
22
|
+
await queryRunner.query(
|
|
23
|
+
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
|
24
|
+
);
|
|
25
|
+
await queryRunner.query(
|
|
26
|
+
`ALTER TABLE "Project" DROP COLUMN "businessDetailsCountry"`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1756300358095 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1756300358095";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Project" ADD "financeAccountingEmail" character varying(100)`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "Project" DROP COLUMN "financeAccountingEmail"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -161,6 +161,9 @@ import { MigrationName1755110936888 } from "./1755110936888-MigrationName";
|
|
|
161
161
|
import { MigrationName1755775040650 } from "./1755775040650-MigrationName";
|
|
162
162
|
import { MigrationName1755778495455 } from "./1755778495455-MigrationName";
|
|
163
163
|
import { MigrationName1755778934927 } from "./1755778934927-MigrationName";
|
|
164
|
+
import { MigrationName1756293325324 } from "./1756293325324-MigrationName";
|
|
165
|
+
import { MigrationName1756296282627 } from "./1756296282627-MigrationName";
|
|
166
|
+
import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
|
|
164
167
|
|
|
165
168
|
export default [
|
|
166
169
|
InitialMigration,
|
|
@@ -326,4 +329,7 @@ export default [
|
|
|
326
329
|
MigrationName1755775040650,
|
|
327
330
|
MigrationName1755778495455,
|
|
328
331
|
MigrationName1755778934927,
|
|
332
|
+
MigrationName1756293325324,
|
|
333
|
+
MigrationName1756296282627,
|
|
334
|
+
MigrationName1756300358095,
|
|
329
335
|
];
|
|
@@ -80,6 +80,94 @@ export class BillingService extends BaseService {
|
|
|
80
80
|
await this.stripe.customers.update(id, { name: newName });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
@CaptureSpan()
|
|
84
|
+
public async updateCustomerBusinessDetails(
|
|
85
|
+
id: string,
|
|
86
|
+
businessDetails: string,
|
|
87
|
+
countryCode?: string | null,
|
|
88
|
+
financeAccountingEmail?: string | null,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
if (!this.isBillingEnabled()) {
|
|
91
|
+
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
|
|
92
|
+
}
|
|
93
|
+
// Goal: Update Stripe Customer "Billing details" (address fields) rather than invoice footer.
|
|
94
|
+
// We only have a single free-form textarea. We'll map:
|
|
95
|
+
// First non-empty line -> address.line1
|
|
96
|
+
// Second non-empty line (if any) and remaining (joined, truncated) -> address.line2
|
|
97
|
+
// We also persist full text in metadata so we can reconstruct or improve parsing later.
|
|
98
|
+
// NOTE: Because Stripe requires structured address, any city/state/postal/country detection
|
|
99
|
+
// would be heuristic; we keep it simple unless we later add structured fields.
|
|
100
|
+
|
|
101
|
+
const lines: Array<string> = businessDetails
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.map((l: string) => {
|
|
104
|
+
return l.trim();
|
|
105
|
+
})
|
|
106
|
+
.filter((l: string) => {
|
|
107
|
+
return l.length > 0;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let line1: string | undefined = undefined;
|
|
111
|
+
let line2: string | undefined = undefined;
|
|
112
|
+
|
|
113
|
+
if (lines && lines.length > 0) {
|
|
114
|
+
const first: string = lines[0]!; // non-null
|
|
115
|
+
line1 = first.substring(0, 200); // Stripe typical limit safeguard.
|
|
116
|
+
}
|
|
117
|
+
if (lines && lines.length > 1) {
|
|
118
|
+
const rest: string = lines.slice(1).join(", ");
|
|
119
|
+
line2 = rest.substring(0, 200);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const metadata: Record<string, string> = {
|
|
123
|
+
business_details_full: businessDetails.substring(0, 5000),
|
|
124
|
+
};
|
|
125
|
+
if (financeAccountingEmail) {
|
|
126
|
+
metadata["finance_accounting_email"] = financeAccountingEmail.substring(
|
|
127
|
+
0,
|
|
128
|
+
200,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
// Remove if cleared
|
|
132
|
+
metadata["finance_accounting_email"] = "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updateParams: Stripe.CustomerUpdateParams = {
|
|
136
|
+
metadata,
|
|
137
|
+
address: {},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// If finance / accounting email provided, set it as the customer email so Stripe sends
|
|
141
|
+
// invoices / receipts there. (Stripe only supports a single email via API currently.)
|
|
142
|
+
if (financeAccountingEmail && financeAccountingEmail.trim().length > 0) {
|
|
143
|
+
updateParams.email = financeAccountingEmail.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (line1) {
|
|
147
|
+
updateParams.address = updateParams.address || {};
|
|
148
|
+
updateParams.address.line1 = line1;
|
|
149
|
+
}
|
|
150
|
+
if (line2) {
|
|
151
|
+
updateParams.address = updateParams.address || {};
|
|
152
|
+
updateParams.address.line2 = line2;
|
|
153
|
+
}
|
|
154
|
+
if (countryCode) {
|
|
155
|
+
updateParams.address = updateParams.address || {};
|
|
156
|
+
// Stripe expects uppercase 2-letter ISO code
|
|
157
|
+
updateParams.address.country = countryCode.toUpperCase();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!line1 && !line2 && !countryCode) {
|
|
161
|
+
// Clear address if empty details submitted.
|
|
162
|
+
updateParams.address = {
|
|
163
|
+
line1: "",
|
|
164
|
+
line2: "",
|
|
165
|
+
} as any;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await this.stripe.customers.update(id, updateParams);
|
|
169
|
+
}
|
|
170
|
+
|
|
83
171
|
@CaptureSpan()
|
|
84
172
|
public async deleteCustomer(id: string): Promise<void> {
|
|
85
173
|
if (!this.isBillingEnabled()) {
|
|
@@ -273,6 +273,38 @@ export class ProjectService extends DatabaseService<Model> {
|
|
|
273
273
|
updateBy: UpdateBy<Model>,
|
|
274
274
|
): Promise<OnUpdate<Model>> {
|
|
275
275
|
if (IsBillingEnabled) {
|
|
276
|
+
if (
|
|
277
|
+
updateBy.data.businessDetails ||
|
|
278
|
+
updateBy.data.businessDetailsCountry ||
|
|
279
|
+
updateBy.data.financeAccountingEmail
|
|
280
|
+
) {
|
|
281
|
+
// Sync to Stripe.
|
|
282
|
+
const project: Model | null = await this.findOneById({
|
|
283
|
+
id: new ObjectID(updateBy.query._id! as string),
|
|
284
|
+
select: {
|
|
285
|
+
paymentProviderCustomerId: true,
|
|
286
|
+
financeAccountingEmail: true,
|
|
287
|
+
},
|
|
288
|
+
props: { isRoot: true },
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (project?.paymentProviderCustomerId) {
|
|
292
|
+
try {
|
|
293
|
+
await BillingService.updateCustomerBusinessDetails(
|
|
294
|
+
project.paymentProviderCustomerId,
|
|
295
|
+
(updateBy.data.businessDetails as string) || "",
|
|
296
|
+
(updateBy.data.businessDetailsCountry as string) || null,
|
|
297
|
+
(updateBy.data.financeAccountingEmail as string) ||
|
|
298
|
+
(project as any).financeAccountingEmail ||
|
|
299
|
+
null,
|
|
300
|
+
);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logger.error(
|
|
303
|
+
"Failed to update Stripe customer business details: " + err,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
276
308
|
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
|
|
277
309
|
await NotificationService.rechargeIfBalanceIsLow(
|
|
278
310
|
new ObjectID(updateBy.query._id! as string),
|
|
@@ -340,10 +340,26 @@ export class Service extends DatabaseService<StatusPage> {
|
|
|
340
340
|
projectId: ObjectID,
|
|
341
341
|
statusPageId: ObjectID,
|
|
342
342
|
): Promise<URL> {
|
|
343
|
-
|
|
343
|
+
if (!projectId) {
|
|
344
|
+
throw new BadDataException(
|
|
345
|
+
"projectId is required to build status page dashboard link",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!statusPageId) {
|
|
350
|
+
throw new BadDataException(
|
|
351
|
+
"statusPageId is required to build status page dashboard link",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
|
356
|
+
|
|
357
|
+
// Defensive: ensure objects have toString
|
|
358
|
+
const projectIdStr: string = projectId.toString();
|
|
359
|
+
const statusPageIdStr: string = statusPageId.toString();
|
|
344
360
|
|
|
345
|
-
return URL.fromString(
|
|
346
|
-
`/${
|
|
361
|
+
return URL.fromString(dashboardUrl.toString()).addRoute(
|
|
362
|
+
`/${projectIdStr}/status-pages/${statusPageIdStr}`,
|
|
347
363
|
);
|
|
348
364
|
}
|
|
349
365
|
|
package/Server/Types/Markdown.ts
CHANGED
|
@@ -203,30 +203,30 @@ export default class Markdown {
|
|
|
203
203
|
return text as string;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
const isHash: boolean = href.startsWith(
|
|
207
|
-
const isMailTo: boolean = href.startsWith(
|
|
208
|
-
const isTel: boolean = href.startsWith(
|
|
206
|
+
const isHash: boolean = href.startsWith("#");
|
|
207
|
+
const isMailTo: boolean = href.startsWith("mailto:");
|
|
208
|
+
const isTel: boolean = href.startsWith("tel:");
|
|
209
209
|
const isInternal: boolean =
|
|
210
|
-
href.startsWith(
|
|
211
|
-
href.includes(
|
|
210
|
+
href.startsWith("/") ||
|
|
211
|
+
href.includes("oneuptime.com") ||
|
|
212
212
|
isHash ||
|
|
213
213
|
isMailTo ||
|
|
214
214
|
isTel;
|
|
215
215
|
|
|
216
216
|
const baseClasses: string = [
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
].join(
|
|
226
|
-
|
|
227
|
-
const titleAttr: string = title ? ` title="${title}"` :
|
|
217
|
+
"font-semibold",
|
|
218
|
+
"text-indigo-600",
|
|
219
|
+
"underline",
|
|
220
|
+
"underline-offset-2",
|
|
221
|
+
"decoration-indigo-300",
|
|
222
|
+
"hover:decoration-indigo-500",
|
|
223
|
+
"hover:text-indigo-500",
|
|
224
|
+
"transition-colors",
|
|
225
|
+
].join(" ");
|
|
226
|
+
|
|
227
|
+
const titleAttr: string = title ? ` title="${title}"` : "";
|
|
228
228
|
const externalAttrs: string = isInternal
|
|
229
|
-
?
|
|
229
|
+
? ""
|
|
230
230
|
: ' target="_blank" rel="noopener noreferrer"';
|
|
231
231
|
|
|
232
232
|
return `<a href="${href}"${titleAttr} class="${baseClasses}"${externalAttrs}>${text}</a>`;
|
|
@@ -420,9 +420,8 @@ const Detail: DetailFunction = <T extends GenericObject>(
|
|
|
420
420
|
)}
|
|
421
421
|
</div>
|
|
422
422
|
)}
|
|
423
|
-
{(data === null || data === undefined
|
|
424
|
-
<PlaceholderText text={field.placeholder} />
|
|
425
|
-
)}
|
|
423
|
+
{(data === null || data === undefined || data === "") &&
|
|
424
|
+
field.placeholder && <PlaceholderText text={field.placeholder} />}
|
|
426
425
|
</div>
|
|
427
426
|
</div>
|
|
428
427
|
);
|