mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-16 23:00:51 +00:00
Merge pull request #2113 from OneUptime/file-attachments
File attachments
This commit is contained in:
commit
74e18a2861
52 changed files with 3112 additions and 240 deletions
|
|
@ -29,6 +29,10 @@ import UserCallAPI from "Common/Server/API/UserCallAPI";
|
|||
import UserTotpAuthAPI from "Common/Server/API/UserTotpAuthAPI";
|
||||
import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
|
||||
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
|
||||
import IncidentInternalNoteAPI from "Common/Server/API/IncidentInternalNoteAPI";
|
||||
import IncidentPublicNoteAPI from "Common/Server/API/IncidentPublicNoteAPI";
|
||||
import ScheduledMaintenanceInternalNoteAPI from "Common/Server/API/ScheduledMaintenanceInternalNoteAPI";
|
||||
import ScheduledMaintenancePublicNoteAPI from "Common/Server/API/ScheduledMaintenancePublicNoteAPI";
|
||||
// User Notification methods.
|
||||
import UserEmailAPI from "Common/Server/API/UserEmailAPI";
|
||||
import UserNotificationLogTimelineAPI from "Common/Server/API/UserOnCallLogTimelineAPI";
|
||||
|
|
@ -61,9 +65,7 @@ import EmailVerificationTokenService, {
|
|||
import AlertCustomFieldService, {
|
||||
Service as AlertCustomFieldServiceType,
|
||||
} from "Common/Server/Services/AlertCustomFieldService";
|
||||
import AlertInternalNoteService, {
|
||||
Service as AlertInternalNoteServiceType,
|
||||
} from "Common/Server/Services/AlertInternalNoteService";
|
||||
import AlertInternalNoteAPI from "Common/Server/API/AlertInternalNoteAPI";
|
||||
import AlertNoteTemplateService, {
|
||||
Service as AlertNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/AlertNoteTemplateService";
|
||||
|
|
@ -94,9 +96,6 @@ import AlertStateTimelineService, {
|
|||
import IncidentCustomFieldService, {
|
||||
Service as IncidentCustomFieldServiceType,
|
||||
} from "Common/Server/Services/IncidentCustomFieldService";
|
||||
import IncidentInternalNoteService, {
|
||||
Service as IncidentInternalNoteServiceType,
|
||||
} from "Common/Server/Services/IncidentInternalNoteService";
|
||||
import IncidentNoteTemplateService, {
|
||||
Service as IncidentNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/IncidentNoteTemplateService";
|
||||
|
|
@ -112,9 +111,6 @@ import IncidentOwnerTeamService, {
|
|||
import IncidentOwnerUserService, {
|
||||
Service as IncidentOwnerUserServiceType,
|
||||
} from "Common/Server/Services/IncidentOwnerUserService";
|
||||
import IncidentPublicNoteService, {
|
||||
Service as IncidentPublicNoteServiceType,
|
||||
} from "Common/Server/Services/IncidentPublicNoteService";
|
||||
import IncidentService, {
|
||||
Service as IncidentServiceType,
|
||||
} from "Common/Server/Services/IncidentService";
|
||||
|
|
@ -233,9 +229,6 @@ import ResellerService, {
|
|||
import ScheduledMaintenanceCustomFieldService, {
|
||||
Service as ScheduledMaintenanceCustomFieldServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceCustomFieldService";
|
||||
import ScheduledMaintenanceInternalNoteService, {
|
||||
Service as ScheduledMaintenanceInternalNoteServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceInternalNoteService";
|
||||
import ScheduledMaintenanceNoteTemplateService, {
|
||||
Service as ScheduledMaintenanceNoteTemplateServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceNoteTemplateService";
|
||||
|
|
@ -245,9 +238,6 @@ import ScheduledMaintenanceOwnerTeamService, {
|
|||
import ScheduledMaintenanceOwnerUserService, {
|
||||
Service as ScheduledMaintenanceOwnerUserServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceOwnerUserService";
|
||||
import ScheduledMaintenancePublicNoteService, {
|
||||
Service as ScheduledMaintenancePublicNoteServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenancePublicNoteService";
|
||||
import ScheduledMaintenanceService, {
|
||||
Service as ScheduledMaintenanceServiceType,
|
||||
} from "Common/Server/Services/ScheduledMaintenanceService";
|
||||
|
|
@ -294,9 +284,7 @@ import PushNotificationLogService, {
|
|||
import SpanService, {
|
||||
SpanService as SpanServiceType,
|
||||
} from "Common/Server/Services/SpanService";
|
||||
import StatusPageAnnouncementService, {
|
||||
Service as StatusPageAnnouncementServiceType,
|
||||
} from "Common/Server/Services/StatusPageAnnouncementService";
|
||||
import StatusPageAnnouncementAPI from "Common/Server/API/StatusPageAnnouncementAPI";
|
||||
import StatusPageCustomFieldService, {
|
||||
Service as StatusPageCustomFieldServiceType,
|
||||
} from "Common/Server/Services/StatusPageCustomFieldService";
|
||||
|
|
@ -398,7 +386,6 @@ import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
|||
|
||||
import Alert from "Common/Models/DatabaseModels/Alert";
|
||||
import AlertCustomField from "Common/Models/DatabaseModels/AlertCustomField";
|
||||
import AlertInternalNote from "Common/Models/DatabaseModels/AlertInternalNote";
|
||||
import AlertNoteTemplate from "Common/Models/DatabaseModels/AlertNoteTemplate";
|
||||
import AlertOwnerTeam from "Common/Models/DatabaseModels/AlertOwnerTeam";
|
||||
import AlertOwnerUser from "Common/Models/DatabaseModels/AlertOwnerUser";
|
||||
|
|
@ -408,12 +395,10 @@ import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline"
|
|||
|
||||
import Incident from "Common/Models/DatabaseModels/Incident";
|
||||
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
|
||||
import IncidentInternalNote from "Common/Models/DatabaseModels/IncidentInternalNote";
|
||||
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
|
||||
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
|
||||
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
|
||||
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
|
||||
import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote";
|
||||
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
|
||||
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
||||
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
|
||||
|
|
@ -448,11 +433,9 @@ import PromoCode from "Common/Models/DatabaseModels/PromoCode";
|
|||
import Reseller from "Common/Models/DatabaseModels/Reseller";
|
||||
import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance";
|
||||
import ScheduledMaintenanceCustomField from "Common/Models/DatabaseModels/ScheduledMaintenanceCustomField";
|
||||
import ScheduledMaintenanceInternalNote from "Common/Models/DatabaseModels/ScheduledMaintenanceInternalNote";
|
||||
import ScheduledMaintenanceNoteTemplate from "Common/Models/DatabaseModels/ScheduledMaintenanceNoteTemplate";
|
||||
import ScheduledMaintenanceOwnerTeam from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerTeam";
|
||||
import ScheduledMaintenanceOwnerUser from "Common/Models/DatabaseModels/ScheduledMaintenanceOwnerUser";
|
||||
import ScheduledMaintenancePublicNote from "Common/Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
||||
import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMaintenanceState";
|
||||
import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
||||
import ServiceCatalog from "Common/Models/DatabaseModels/ServiceCatalog";
|
||||
|
|
@ -461,7 +444,6 @@ import ServiceCatalogOwnerUser from "Common/Models/DatabaseModels/ServiceCatalog
|
|||
import ServiceCopilotCodeRepository from "Common/Models/DatabaseModels/ServiceCopilotCodeRepository";
|
||||
import ShortLink from "Common/Models/DatabaseModels/ShortLink";
|
||||
import SmsLog from "Common/Models/DatabaseModels/SmsLog";
|
||||
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
|
||||
// Custom Fields API
|
||||
import StatusPageCustomField from "Common/Models/DatabaseModels/StatusPageCustomField";
|
||||
import StatusPageFooterLink from "Common/Models/DatabaseModels/StatusPageFooterLink";
|
||||
|
|
@ -845,10 +827,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
|||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<AlertInternalNote, AlertInternalNoteServiceType>(
|
||||
AlertInternalNote,
|
||||
AlertInternalNoteService,
|
||||
).getRouter(),
|
||||
new AlertInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
|
|
@ -1045,10 +1024,7 @@ const BaseAPIFeatureSet: FeatureSet = {
|
|||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<StatusPageAnnouncement, StatusPageAnnouncementServiceType>(
|
||||
StatusPageAnnouncement,
|
||||
StatusPageAnnouncementService,
|
||||
).getRouter(),
|
||||
new StatusPageAnnouncementAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
|
|
@ -1721,40 +1697,22 @@ const BaseAPIFeatureSet: FeatureSet = {
|
|||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteServiceType
|
||||
>(
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteService,
|
||||
).getRouter(),
|
||||
new ScheduledMaintenancePublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteServiceType
|
||||
>(
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteService,
|
||||
).getRouter(),
|
||||
new ScheduledMaintenanceInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentPublicNote, IncidentPublicNoteServiceType>(
|
||||
IncidentPublicNote,
|
||||
IncidentPublicNoteService,
|
||||
).getRouter(),
|
||||
new IncidentPublicNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<IncidentInternalNote, IncidentInternalNoteServiceType>(
|
||||
IncidentInternalNote,
|
||||
IncidentInternalNoteService,
|
||||
).getRouter(),
|
||||
new IncidentInternalNoteAPI().getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Alert from "./Alert";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
|
|
@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
|||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("alert")
|
||||
|
|
@ -340,6 +349,54 @@ export default class AlertInternalNote extends BaseModel {
|
|||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateAlertInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadAlertInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditAlertInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "AlertInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "alertInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
|||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Entity } from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
|
|
@ -20,7 +21,7 @@ import { Entity } from "typeorm";
|
|||
})
|
||||
@CrudApiEndpoint(new Route("/file"))
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
create: [Permission.CurrentUser, Permission.AuthenticatedRequest],
|
||||
read: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Incident from "./Incident";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
|
|
@ -17,7 +18,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
|||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("incident")
|
||||
|
|
@ -340,6 +349,54 @@ export default class IncidentInternalNote extends BaseModel {
|
|||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateIncidentInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditIncidentInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "IncidentInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "incidentInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Incident from "./Incident";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
|
|
@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
|
|||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("incident")
|
||||
|
|
@ -341,6 +350,54 @@ export default class IncidentPublicNote extends BaseModel {
|
|||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateIncidentPublicNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentPublicNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditIncidentPublicNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "IncidentPublicNoteFile",
|
||||
joinColumn: {
|
||||
name: "incidentPublicNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Project from "./Project";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
|
|
@ -16,7 +17,15 @@ import TenantColumn from "../../Types/Database/TenantColumn";
|
|||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@CanAccessIfCanReadOn("scheduledMaintenance")
|
||||
@TenantColumn("projectId")
|
||||
|
|
@ -340,6 +349,54 @@ export default class ScheduledMaintenanceInternalNote extends BaseModel {
|
|||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateScheduledMaintenanceInternalNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadScheduledMaintenanceInternalNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditScheduledMaintenanceInternalNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ScheduledMaintenanceInternalNoteFile",
|
||||
joinColumn: {
|
||||
name: "scheduledMaintenanceInternalNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Project from "./Project";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
|
|
@ -18,7 +19,15 @@ import IconProp from "../../Types/Icon/IconProp";
|
|||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn("scheduledMaintenance")
|
||||
|
|
@ -342,6 +351,54 @@ export default class ScheduledMaintenancePublicNote extends BaseModel {
|
|||
})
|
||||
public note?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateScheduledMaintenancePublicNote,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadScheduledMaintenancePublicNote,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditScheduledMaintenancePublicNote,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ScheduledMaintenancePublicNoteFile",
|
||||
joinColumn: {
|
||||
name: "scheduledMaintenancePublicNoteId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Monitor from "./Monitor";
|
|||
import Project from "./Project";
|
||||
import StatusPage from "./StatusPage";
|
||||
import User from "./User";
|
||||
import File from "./File";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
|
||||
|
|
@ -375,6 +376,54 @@ export default class StatusPageAnnouncement extends BaseModel {
|
|||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateStatusPageAnnouncement,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPageAnnouncement,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditStatusPageAnnouncement,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this announcement",
|
||||
required: false,
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return File;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
},
|
||||
)
|
||||
@JoinTable({
|
||||
name: "StatusPageAnnouncementFile",
|
||||
joinColumn: {
|
||||
name: "statusPageAnnouncementId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "fileId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public attachments?: Array<File> = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
|
|
|||
96
Common/Server/API/AlertInternalNoteAPI.ts
Normal file
96
Common/Server/API/AlertInternalNoteAPI.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import AlertInternalNote from "../../Models/DatabaseModels/AlertInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import AlertInternalNoteService, {
|
||||
Service as AlertInternalNoteServiceType,
|
||||
} from "../Services/AlertInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class AlertInternalNoteAPI extends BaseAPI<
|
||||
AlertInternalNote,
|
||||
AlertInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(AlertInternalNote, AlertInternalNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: AlertInternalNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
96
Common/Server/API/IncidentInternalNoteAPI.ts
Normal file
96
Common/Server/API/IncidentInternalNoteAPI.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import IncidentInternalNote from "../../Models/DatabaseModels/IncidentInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentInternalNoteService, {
|
||||
Service as IncidentInternalNoteServiceType,
|
||||
} from "../Services/IncidentInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class IncidentInternalNoteAPI extends BaseAPI<
|
||||
IncidentInternalNote,
|
||||
IncidentInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(IncidentInternalNote, IncidentInternalNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: IncidentInternalNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
94
Common/Server/API/IncidentPublicNoteAPI.ts
Normal file
94
Common/Server/API/IncidentPublicNoteAPI.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import IncidentPublicNoteService, {
|
||||
Service as IncidentPublicNoteServiceType,
|
||||
} from "../Services/IncidentPublicNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class IncidentPublicNoteAPI extends BaseAPI<
|
||||
IncidentPublicNote,
|
||||
IncidentPublicNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(IncidentPublicNote, IncidentPublicNoteService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:noteId/:fileId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: IncidentPublicNote | null = await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
100
Common/Server/API/ScheduledMaintenanceInternalNoteAPI.ts
Normal file
100
Common/Server/API/ScheduledMaintenanceInternalNoteAPI.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import ScheduledMaintenanceInternalNote from "../../Models/DatabaseModels/ScheduledMaintenanceInternalNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import ScheduledMaintenanceInternalNoteService, {
|
||||
Service as ScheduledMaintenanceInternalNoteServiceType,
|
||||
} from "../Services/ScheduledMaintenanceInternalNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class ScheduledMaintenanceInternalNoteAPI extends BaseAPI<
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(
|
||||
ScheduledMaintenanceInternalNote,
|
||||
ScheduledMaintenanceInternalNoteService,
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: ScheduledMaintenanceInternalNote | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
98
Common/Server/API/ScheduledMaintenancePublicNoteAPI.ts
Normal file
98
Common/Server/API/ScheduledMaintenancePublicNoteAPI.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import ScheduledMaintenancePublicNote from "../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import ScheduledMaintenancePublicNoteService, {
|
||||
Service as ScheduledMaintenancePublicNoteServiceType,
|
||||
} from "../Services/ScheduledMaintenancePublicNoteService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
|
||||
export default class ScheduledMaintenancePublicNoteAPI extends BaseAPI<
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(
|
||||
ScheduledMaintenancePublicNote,
|
||||
ScheduledMaintenancePublicNoteService,
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:noteId/:fileId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!noteIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const note: ScheduledMaintenancePublicNote | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: noteId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = note?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ import ScheduledMaintenanceState from "../../Models/DatabaseModels/ScheduledMain
|
|||
import ScheduledMaintenanceStateTimeline from "../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
||||
import StatusPage from "../../Models/DatabaseModels/StatusPage";
|
||||
import StatusPageAnnouncement from "../../Models/DatabaseModels/StatusPageAnnouncement";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import StatusPageDomain from "../../Models/DatabaseModels/StatusPageDomain";
|
||||
import StatusPageFooterLink from "../../Models/DatabaseModels/StatusPageFooterLink";
|
||||
import StatusPageGroup from "../../Models/DatabaseModels/StatusPageGroup";
|
||||
|
|
@ -389,6 +390,48 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
},
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/incident-public-note/attachment/:statusPageId/:incidentId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getIncidentPublicNoteAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/scheduled-maintenance-public-note/attachment/:statusPageId/:scheduledMaintenanceId/:noteId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getScheduledMaintenancePublicNoteAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/status-page-announcement/attachment/:statusPageId/:announcementId/:fileId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getStatusPageAnnouncementAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// embedded overall status badge api
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
|
|
@ -1382,6 +1425,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
note: true,
|
||||
incidentId: true,
|
||||
postedAt: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Descending, // new note first
|
||||
|
|
@ -1567,6 +1614,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
postedAt: true,
|
||||
note: true,
|
||||
scheduledMaintenanceId: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Ascending,
|
||||
|
|
@ -2106,6 +2157,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
postedAt: true,
|
||||
note: true,
|
||||
scheduledMaintenanceId: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Ascending,
|
||||
|
|
@ -2332,6 +2387,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
|
|
@ -3271,6 +3330,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
postedAt: true,
|
||||
note: true,
|
||||
incidentId: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Descending, // new note first
|
||||
|
|
@ -3605,6 +3668,353 @@ export default class StatusPageAPI extends BaseAPI<
|
|||
};
|
||||
}
|
||||
|
||||
private async getStatusPageAnnouncementAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
||||
const announcementIdParam: string | undefined =
|
||||
req.params["announcementId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!statusPageIdParam || !announcementIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let statusPageId: ObjectID;
|
||||
let announcementId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
statusPageId = new ObjectID(statusPageIdParam);
|
||||
announcementId = new ObjectID(announcementIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
await this.checkHasReadAccess({
|
||||
statusPageId: statusPageId,
|
||||
req: req,
|
||||
});
|
||||
|
||||
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
||||
query: {
|
||||
_id: statusPageId.toString(),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
showAnnouncementsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!statusPage ||
|
||||
!statusPage.projectId ||
|
||||
!statusPage.showAnnouncementsOnStatusPage
|
||||
) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const announcement: StatusPageAnnouncement | null =
|
||||
await StatusPageAnnouncementService.findOneBy({
|
||||
query: {
|
||||
_id: announcementId.toString(),
|
||||
projectId: statusPage.projectId!,
|
||||
statusPages: [statusPageId] as any,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!announcement) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const attachment: File | undefined = announcement.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
|
||||
private async getScheduledMaintenancePublicNoteAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
||||
const scheduledMaintenanceIdParam: string | undefined =
|
||||
req.params["scheduledMaintenanceId"];
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (
|
||||
!statusPageIdParam ||
|
||||
!scheduledMaintenanceIdParam ||
|
||||
!noteIdParam ||
|
||||
!fileIdParam
|
||||
) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let statusPageId: ObjectID;
|
||||
let scheduledMaintenanceId: ObjectID;
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
statusPageId = new ObjectID(statusPageIdParam);
|
||||
scheduledMaintenanceId = new ObjectID(scheduledMaintenanceIdParam);
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
await this.checkHasReadAccess({
|
||||
statusPageId: statusPageId,
|
||||
req: req,
|
||||
});
|
||||
|
||||
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
||||
query: {
|
||||
_id: statusPageId.toString(),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
showScheduledMaintenanceEventsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!statusPage ||
|
||||
!statusPage.projectId ||
|
||||
!statusPage.showScheduledMaintenanceEventsOnStatusPage
|
||||
) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const scheduledMaintenance: ScheduledMaintenance | null =
|
||||
await ScheduledMaintenanceService.findOneBy({
|
||||
query: {
|
||||
_id: scheduledMaintenanceId.toString(),
|
||||
projectId: statusPage.projectId!,
|
||||
isVisibleOnStatusPage: true,
|
||||
statusPages: statusPageId as any,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!scheduledMaintenance) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const scheduledMaintenancePublicNote: ScheduledMaintenancePublicNote | null =
|
||||
await ScheduledMaintenancePublicNoteService.findOneBy({
|
||||
query: {
|
||||
_id: noteId.toString(),
|
||||
scheduledMaintenanceId: scheduledMaintenanceId.toString(),
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!scheduledMaintenancePublicNote) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const attachment: File | undefined =
|
||||
scheduledMaintenancePublicNote.attachments?.find((file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
});
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
|
||||
private async getIncidentPublicNoteAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const statusPageIdParam: string | undefined = req.params["statusPageId"];
|
||||
const incidentIdParam: string | undefined = req.params["incidentId"];
|
||||
const noteIdParam: string | undefined = req.params["noteId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (
|
||||
!statusPageIdParam ||
|
||||
!incidentIdParam ||
|
||||
!noteIdParam ||
|
||||
!fileIdParam
|
||||
) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let statusPageId: ObjectID;
|
||||
let incidentId: ObjectID;
|
||||
let noteId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
statusPageId = new ObjectID(statusPageIdParam);
|
||||
incidentId = new ObjectID(incidentIdParam);
|
||||
noteId = new ObjectID(noteIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
await this.checkHasReadAccess({
|
||||
statusPageId: statusPageId,
|
||||
req: req,
|
||||
});
|
||||
|
||||
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
||||
query: {
|
||||
_id: statusPageId.toString(),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
showIncidentsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPage || !statusPage.projectId) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
if (!statusPage.showIncidentsOnStatusPage) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const { monitorsOnStatusPage } =
|
||||
await StatusPageService.getMonitorIdsOnStatusPage({
|
||||
statusPageId: statusPageId,
|
||||
});
|
||||
|
||||
if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const incident: Incident | null = await IncidentService.findOneBy({
|
||||
query: {
|
||||
_id: incidentId.toString(),
|
||||
projectId: statusPage.projectId!,
|
||||
isVisibleOnStatusPage: true,
|
||||
monitors: monitorsOnStatusPage as any,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incident) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const incidentPublicNote: IncidentPublicNote | null =
|
||||
await IncidentPublicNoteService.findOneBy({
|
||||
query: {
|
||||
_id: noteId.toString(),
|
||||
incidentId: incidentId.toString(),
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incidentPublicNote) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const attachment: File | undefined = incidentPublicNote.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
|
||||
public async checkHasReadAccess(data: {
|
||||
statusPageId: ObjectID;
|
||||
req: ExpressRequest;
|
||||
|
|
|
|||
96
Common/Server/API/StatusPageAnnouncementAPI.ts
Normal file
96
Common/Server/API/StatusPageAnnouncementAPI.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import StatusPageAnnouncement from "../../Models/DatabaseModels/StatusPageAnnouncement";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import NotFoundException from "../../Types/Exception/NotFoundException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import StatusPageAnnouncementService, {
|
||||
Service as StatusPageAnnouncementServiceType,
|
||||
} from "../Services/StatusPageAnnouncementService";
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from "../Utils/Express";
|
||||
|
||||
export default class StatusPageAnnouncementAPI extends BaseAPI<
|
||||
StatusPageAnnouncement,
|
||||
StatusPageAnnouncementServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(StatusPageAnnouncement, StatusPageAnnouncementService);
|
||||
|
||||
this.router.get(
|
||||
`${new this.entityType().getCrudApiPath()?.toString()}/attachment/:announcementId/:fileId`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
await this.getAttachment(req, res);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async getAttachment(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
): Promise<void> {
|
||||
const announcementIdParam: string | undefined =
|
||||
req.params["announcementId"];
|
||||
const fileIdParam: string | undefined = req.params["fileId"];
|
||||
|
||||
if (!announcementIdParam || !fileIdParam) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
let announcementId: ObjectID;
|
||||
let fileId: ObjectID;
|
||||
|
||||
try {
|
||||
announcementId = new ObjectID(announcementIdParam);
|
||||
fileId = new ObjectID(fileIdParam);
|
||||
} catch {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
const props: DatabaseCommonInteractionProps =
|
||||
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
||||
|
||||
const announcement: StatusPageAnnouncement | null =
|
||||
await this.service.findOneBy({
|
||||
query: {
|
||||
_id: announcementId,
|
||||
},
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
file: true,
|
||||
fileType: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
props,
|
||||
});
|
||||
|
||||
const attachment: File | undefined = announcement?.attachments?.find(
|
||||
(file: File) => {
|
||||
const attachmentId: string | null = file._id
|
||||
? file._id.toString()
|
||||
: file.id
|
||||
? file.id.toString()
|
||||
: null;
|
||||
return attachmentId === fileId.toString();
|
||||
},
|
||||
);
|
||||
|
||||
if (!attachment || !attachment.file) {
|
||||
throw new NotFoundException("Attachment not found");
|
||||
}
|
||||
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(req, res, attachment);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ export default class UserAPI extends BaseAPI<User, UserServiceType> {
|
|||
});
|
||||
|
||||
if (userById && userById.profilePictureFile) {
|
||||
this.setNoCacheHeaders(res);
|
||||
Response.setNoCacheHeaders(res);
|
||||
return Response.sendFileResponse(
|
||||
req,
|
||||
res,
|
||||
|
|
@ -78,14 +78,8 @@ export default class UserAPI extends BaseAPI<User, UserServiceType> {
|
|||
);
|
||||
}
|
||||
|
||||
private setNoCacheHeaders(res: ExpressResponse): void {
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
}
|
||||
|
||||
private sendBlankProfile(req: ExpressRequest, res: ExpressResponse): void {
|
||||
this.setNoCacheHeaders(res);
|
||||
Response.setNoCacheHeaders(res);
|
||||
|
||||
try {
|
||||
Response.sendFileByPath(req, res, BLANK_PROFILE_PICTURE_PATH);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763471659817 implements MigrationInterface {
|
||||
public name = "MigrationName1763471659817";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "IncidentInternalNoteFile" ("incidentInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1e97a749db84f9dc65ee162dd6b" PRIMARY KEY ("incidentInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0edb0291ff3e97197269d77dc4" ON "IncidentInternalNoteFile" ("incidentInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b30b49d21a553c06bd0ff3acf5" ON "IncidentInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "IncidentPublicNoteFile" ("incidentPublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_42d2fe75b663f8fa20421f31e78" PRIMARY KEY ("incidentPublicNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e5c4a5671b2bb51a9918f1f203" ON "IncidentPublicNoteFile" ("incidentPublicNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_81a5bc92f59cb5577746ee51ba" ON "IncidentPublicNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_0edb0291ff3e97197269d77dc48" FOREIGN KEY ("incidentInternalNoteId") REFERENCES "IncidentInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" ADD CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d" FOREIGN KEY ("incidentPublicNoteId") REFERENCES "IncidentPublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" ADD CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_81a5bc92f59cb5577746ee51baf"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentPublicNoteFile" DROP CONSTRAINT "FK_e5c4a5671b2bb51a9918f1f203d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_b30b49d21a553c06bd0ff3acf5f"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "IncidentInternalNoteFile" DROP CONSTRAINT "FK_0edb0291ff3e97197269d77dc48"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_81a5bc92f59cb5577746ee51ba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_e5c4a5671b2bb51a9918f1f203"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "IncidentPublicNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b30b49d21a553c06bd0ff3acf5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0edb0291ff3e97197269d77dc4"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "IncidentInternalNoteFile"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763477560906 implements MigrationInterface {
|
||||
public name = "MigrationName1763477560906";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_fddb744dc7cf400724befe5ba91" PRIMARY KEY ("scheduledMaintenanceInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ac92a60535a6d598c9619fd199" ON "ScheduledMaintenanceInternalNoteFile" ("scheduledMaintenanceInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_daee340befeece208b507a4242" ON "ScheduledMaintenanceInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_373f78b83aa76e5250df8ebaed7" PRIMARY KEY ("scheduledMaintenancePublicNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_af6905f89ca8108ed0f478fd37" ON "ScheduledMaintenancePublicNoteFile" ("scheduledMaintenancePublicNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f09af6332e0b89f134472f0442" ON "ScheduledMaintenancePublicNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_ac92a60535a6d598c9619fd1999" FOREIGN KEY ("scheduledMaintenanceInternalNoteId") REFERENCES "ScheduledMaintenanceInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" ADD CONSTRAINT "FK_daee340befeece208b507a42423" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_af6905f89ca8108ed0f478fd376" FOREIGN KEY ("scheduledMaintenancePublicNoteId") REFERENCES "ScheduledMaintenancePublicNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" ADD CONSTRAINT "FK_f09af6332e0b89f134472f0442a" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_f09af6332e0b89f134472f0442a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenancePublicNoteFile" DROP CONSTRAINT "FK_af6905f89ca8108ed0f478fd376"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_daee340befeece208b507a42423"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ScheduledMaintenanceInternalNoteFile" DROP CONSTRAINT "FK_ac92a60535a6d598c9619fd1999"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f09af6332e0b89f134472f0442"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_af6905f89ca8108ed0f478fd37"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "ScheduledMaintenancePublicNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_daee340befeece208b507a4242"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ac92a60535a6d598c9619fd199"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TABLE "ScheduledMaintenanceInternalNoteFile"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1763480947474 implements MigrationInterface {
|
||||
public name = "MigrationName1763480947474";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "StatusPageAnnouncementFile" ("statusPageAnnouncementId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_1323a0215e608ece58a96816134" PRIMARY KEY ("statusPageAnnouncementId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b152a77a26a67d2e76160ba15e" ON "StatusPageAnnouncementFile" ("statusPageAnnouncementId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2f78e2d073bf58013c962ce482" ON "StatusPageAnnouncementFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "AlertInternalNoteFile" ("alertInternalNoteId" uuid NOT NULL, "fileId" uuid NOT NULL, CONSTRAINT "PK_a5370c68590b3db5c3635d364aa" PRIMARY KEY ("alertInternalNoteId", "fileId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_09507cdab877a482edcc4c0593" ON "AlertInternalNoteFile" ("alertInternalNoteId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_77dc8a31bd4ebb0882450abdde" ON "AlertInternalNoteFile" ("fileId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3" FOREIGN KEY ("statusPageAnnouncementId") REFERENCES "StatusPageAnnouncement"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" ADD CONSTRAINT "FK_2f78e2d073bf58013c962ce4827" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_09507cdab877a482edcc4c05933" FOREIGN KEY ("alertInternalNoteId") REFERENCES "AlertInternalNote"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" ADD CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9" FOREIGN KEY ("fileId") REFERENCES "File"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_77dc8a31bd4ebb0882450abdde9"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "AlertInternalNoteFile" DROP CONSTRAINT "FK_09507cdab877a482edcc4c05933"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_2f78e2d073bf58013c962ce4827"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncementFile" DROP CONSTRAINT "FK_b152a77a26a67d2e76160ba15e3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_77dc8a31bd4ebb0882450abdde"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_09507cdab877a482edcc4c0593"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "AlertInternalNoteFile"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2f78e2d073bf58013c962ce482"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b152a77a26a67d2e76160ba15e"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "StatusPageAnnouncementFile"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +182,9 @@ import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
|
|||
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
|
||||
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
|
||||
import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
|
||||
import { MigrationName1763471659817 } from "./1763471659817-MigrationName";
|
||||
import { MigrationName1763477560906 } from "./1763477560906-MigrationName";
|
||||
import { MigrationName1763480947474 } from "./1763480947474-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
|
|
@ -368,4 +371,7 @@ export default [
|
|||
MigrationName1762181014879,
|
||||
MigrationName1762554602716,
|
||||
MigrationName1762890441920,
|
||||
MigrationName1763471659817,
|
||||
MigrationName1763477560906,
|
||||
MigrationName1763480947474,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|||
import Alert from "../../Models/DatabaseModels/Alert";
|
||||
import AlertService from "./AlertService";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
|
|
@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
|||
alertId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
|
|
@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
|||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
|
@ -50,6 +63,11 @@ export class Service extends DatabaseService<Model> {
|
|||
alertId: alertId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/alert-internal-note/attachment",
|
||||
);
|
||||
|
||||
await AlertFeedService.createAlertFeedItem({
|
||||
alertId: createdItem.alertId!,
|
||||
projectId: createdItem.projectId!,
|
||||
|
|
@ -59,7 +77,7 @@ export class Service extends DatabaseService<Model> {
|
|||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Alert ${alertNumber}](${(await AlertService.getAlertLinkInDashboard(createdItem.projectId!, alertId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -104,6 +122,10 @@ ${createdItem.note}
|
|||
|
||||
for (const updatedItem of updatedItems) {
|
||||
const alert: Alert = updatedItem.alert!;
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/alert-internal-note/attachment",
|
||||
);
|
||||
await AlertFeedService.createAlertFeedItem({
|
||||
alertId: updatedItem.alertId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
|
|
@ -113,7 +135,7 @@ ${createdItem.note}
|
|||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Alert ${alert.alertNumber}](${(await AlertService.getAlertLinkInDashboard(alert.projectId!, alert.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -124,6 +146,57 @@ ${updatedItem.note}
|
|||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|||
import IncidentService from "./IncidentService";
|
||||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
|
|
@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
|||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
|
|
@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
|||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
|
@ -51,6 +64,11 @@ export class Service extends DatabaseService<Model> {
|
|||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/incident-internal-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: createdItem.incidentId!,
|
||||
projectId: createdItem.projectId!,
|
||||
|
|
@ -60,7 +78,7 @@ export class Service extends DatabaseService<Model> {
|
|||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(createdItem.projectId!, incidentId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -105,6 +123,11 @@ ${createdItem.note}
|
|||
for (const updatedItem of updatedItems) {
|
||||
const incident: Incident = updatedItem.incident!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/incident-internal-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: updatedItem.incidentId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
|
|
@ -114,7 +137,7 @@ ${createdItem.note}
|
|||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -125,6 +148,57 @@ ${updatedItem.note}
|
|||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import IncidentService from "./IncidentService";
|
|||
import Incident from "../../Models/DatabaseModels/Incident";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
|
|
@ -24,6 +26,7 @@ export class Service extends DatabaseService<Model> {
|
|||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
|
|
@ -32,6 +35,16 @@ export class Service extends DatabaseService<Model> {
|
|||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
publicNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
|
|
@ -84,6 +97,11 @@ export class Service extends DatabaseService<Model> {
|
|||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/incident-public-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: createdItem.incidentId!,
|
||||
projectId: createdItem.projectId!,
|
||||
|
|
@ -92,7 +110,7 @@ export class Service extends DatabaseService<Model> {
|
|||
userId: userId || undefined,
|
||||
feedInfoInMarkdown: `📄 posted **public note** for this [Incident ${incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(projectId!, incidentId!)).toString()}) on status page:
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -138,6 +156,11 @@ ${createdItem.note}
|
|||
for (const updatedItem of updatedItems) {
|
||||
const incident: Incident = updatedItem.incident!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/incident-public-note/attachment",
|
||||
);
|
||||
|
||||
await IncidentFeedService.createIncidentFeedItem({
|
||||
incidentId: updatedItem.incidentId!,
|
||||
projectId: updatedItem.projectId!,
|
||||
|
|
@ -147,7 +170,7 @@ ${createdItem.note}
|
|||
|
||||
feedInfoInMarkdown: `📄 updated **Public Note** for this [Incident ${incident.incidentNumber}](${(await IncidentService.getIncidentLinkInDashboard(incident.projectId!, incident.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -158,6 +181,57 @@ ${updatedItem.note}
|
|||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|||
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
||||
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
|
|
@ -21,6 +23,7 @@ export class Service extends DatabaseService<Model> {
|
|||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
|
|
@ -28,6 +31,16 @@ export class Service extends DatabaseService<Model> {
|
|||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
internalNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
|
@ -52,6 +65,11 @@ export class Service extends DatabaseService<Model> {
|
|||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/scheduled-maintenance-internal-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
|
||||
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
|
||||
projectId: createdItem.projectId!,
|
||||
|
|
@ -62,7 +80,7 @@ export class Service extends DatabaseService<Model> {
|
|||
|
||||
feedInfoInMarkdown: `📄 posted **private note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(createdItem.projectId!, scheduledMaintenanceId)).toString()}):
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -109,6 +127,11 @@ ${createdItem.note}
|
|||
const scheduledMaintenance: ScheduledMaintenance =
|
||||
updatedItem.scheduledMaintenance!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/scheduled-maintenance-internal-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
|
||||
{
|
||||
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
|
||||
|
|
@ -120,7 +143,7 @@ ${createdItem.note}
|
|||
|
||||
feedInfoInMarkdown: `📄 updated **Private Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -132,6 +155,57 @@ ${updatedItem.note}
|
|||
}
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
|||
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import File from "../../Models/DatabaseModels/File";
|
||||
import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
|
|
@ -63,6 +65,11 @@ export class Service extends DatabaseService<Model> {
|
|||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
createdItem.id!,
|
||||
"/scheduled-maintenance-public-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
|
||||
scheduledMaintenanceId: createdItem.scheduledMaintenanceId!,
|
||||
projectId: createdItem.projectId!,
|
||||
|
|
@ -72,7 +79,7 @@ export class Service extends DatabaseService<Model> {
|
|||
userId: userId || undefined,
|
||||
feedInfoInMarkdown: `📄 posted **public note** for this [Scheduled Maintenance ${scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(projectId!, scheduledMaintenanceId!)).toString()}) on status page:
|
||||
|
||||
${createdItem.note}
|
||||
${(createdItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -119,6 +126,11 @@ ${createdItem.note}
|
|||
const scheduledMaintenance: ScheduledMaintenance =
|
||||
updatedItem.scheduledMaintenance!;
|
||||
|
||||
const attachmentsMarkdown: string = await this.getAttachmentsMarkdown(
|
||||
updatedItem.id!,
|
||||
"/scheduled-maintenance-public-note/attachment",
|
||||
);
|
||||
|
||||
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem(
|
||||
{
|
||||
scheduledMaintenanceId: updatedItem.scheduledMaintenanceId!,
|
||||
|
|
@ -130,7 +142,7 @@ ${createdItem.note}
|
|||
|
||||
feedInfoInMarkdown: `📄 updated **Public Note** for this [Scheduled Maintenance ${scheduledMaintenance.scheduledMaintenanceNumber}](${(await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(scheduledMaintenance.projectId!, scheduledMaintenance.id!)).toString()})
|
||||
|
||||
${updatedItem.note}
|
||||
${(updatedItem.note || "") + attachmentsMarkdown}
|
||||
`,
|
||||
workspaceNotification: {
|
||||
sendWorkspaceNotification: true,
|
||||
|
|
@ -149,6 +161,7 @@ ${updatedItem.note}
|
|||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachmentFileIds?: Array<ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
|
|
@ -157,6 +170,16 @@ ${updatedItem.note}
|
|||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
|
||||
publicNote.attachments = data.attachmentFileIds.map(
|
||||
(fileId: ObjectID) => {
|
||||
const file: File = new File();
|
||||
file.id = fileId;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
|
|
@ -164,6 +187,57 @@ ${updatedItem.note}
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getAttachmentsMarkdown(
|
||||
modelId: ObjectID,
|
||||
attachmentApiPath: string,
|
||||
): Promise<string> {
|
||||
if (!modelId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const noteWithAttachments: Model | null = await this.findOneById({
|
||||
id: modelId,
|
||||
select: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!noteWithAttachments || !noteWithAttachments.attachments) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentIds: Array<ObjectID> = noteWithAttachments.attachments
|
||||
.map((file: File) => {
|
||||
if (file.id) {
|
||||
return file.id;
|
||||
}
|
||||
|
||||
if (file._id) {
|
||||
return new ObjectID(file._id);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((id: ObjectID | null): id is ObjectID => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
if (!attachmentIds.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({
|
||||
modelId,
|
||||
attachmentIds,
|
||||
attachmentApiPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
|
|
|||
|
|
@ -810,6 +810,7 @@ export class Service extends DatabaseService<StatusPage> {
|
|||
if (data.email) {
|
||||
// force send to this email instead of sending to all subscribers.
|
||||
await sendEmail(data.email, null);
|
||||
return; // don't notify subscribers when explicitly sending a test email.
|
||||
}
|
||||
|
||||
const subscribers: Array<StatusPageSubscriber> =
|
||||
|
|
|
|||
98
Common/Server/Utils/FileAttachmentMarkdownUtil.ts
Normal file
98
Common/Server/Utils/FileAttachmentMarkdownUtil.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import File from "../../Models/DatabaseModels/File";
|
||||
import { AppApiRoute } from "../../ServiceRoute";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import FileService from "../Services/FileService";
|
||||
import QueryHelper from "../Types/Database/QueryHelper";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
|
||||
export interface FileAttachmentMarkdownInput {
|
||||
modelId: ObjectID;
|
||||
attachmentIds: Array<ObjectID>;
|
||||
attachmentApiPath: string;
|
||||
}
|
||||
|
||||
export default class FileAttachmentMarkdownUtil {
|
||||
public static async buildAttachmentMarkdown(
|
||||
input: FileAttachmentMarkdownInput,
|
||||
): Promise<string> {
|
||||
if (
|
||||
!input.modelId ||
|
||||
!input.attachmentIds ||
|
||||
input.attachmentIds.length === 0
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const uniqueIds: Array<string> = Array.from(
|
||||
new Set(
|
||||
input.attachmentIds
|
||||
.map((id: ObjectID) => {
|
||||
return id.toString();
|
||||
})
|
||||
.filter((value: string) => {
|
||||
return Boolean(value);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (uniqueIds.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const files: Array<File> = await FileService.findBy({
|
||||
query: {
|
||||
_id: QueryHelper.any(uniqueIds),
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const fileById: Map<string, File> = new Map(
|
||||
files
|
||||
.filter((file: File) => {
|
||||
return Boolean(file._id);
|
||||
})
|
||||
.map((file: File) => {
|
||||
return [file._id!.toString(), file];
|
||||
}),
|
||||
);
|
||||
|
||||
const attachmentLines: Array<string> = [];
|
||||
|
||||
for (const id of input.attachmentIds) {
|
||||
const key: string = id.toString();
|
||||
const file: File | undefined = fileById.get(key);
|
||||
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName: string = file.name || "Attachment";
|
||||
|
||||
const route: Route = Route.fromString(AppApiRoute.toString())
|
||||
.addRoute(input.attachmentApiPath)
|
||||
.addRoute(`/${input.modelId.toString()}`)
|
||||
.addRoute(`/${key}`);
|
||||
|
||||
attachmentLines.push(`- [${fileName}](${route.toString()})`);
|
||||
}
|
||||
|
||||
if (!attachmentLines.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `\n\n**Attachments:**\n${attachmentLines.join("\n")}\n`;
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ export default class Response {
|
|||
res: ExpressResponse,
|
||||
path: string,
|
||||
): void {
|
||||
Response.setNoCacheHeaders(res);
|
||||
res.sendFile(path);
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ export default class Response {
|
|||
const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
|
||||
|
||||
if (headers) {
|
||||
Response.setNoCacheHeaders(oneUptimeResponse);
|
||||
for (const key in headers) {
|
||||
oneUptimeResponse.set(key, headers[key]?.toString() || "");
|
||||
}
|
||||
|
|
@ -322,4 +324,15 @@ export default class Response {
|
|||
oneUptimeResponse.writeHead(200, { "Content-Type": "text/javascript" });
|
||||
oneUptimeResponse.end(javascript);
|
||||
}
|
||||
|
||||
public static setNoCacheHeaders(res: ExpressResponse): void {
|
||||
const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
|
||||
|
||||
oneUptimeResponse.setHeader(
|
||||
"Cache-Control",
|
||||
"no-store, no-cache, must-revalidate",
|
||||
);
|
||||
oneUptimeResponse.setHeader("Pragma", "no-cache");
|
||||
oneUptimeResponse.setHeader("Expires", "0");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,24 @@ enum MimeType {
|
|||
jpg = "image/jpeg",
|
||||
jpeg = "image/jpeg",
|
||||
svg = "image/svg+xml",
|
||||
gif = "image/gif",
|
||||
webp = "image/webp",
|
||||
pdf = "application/pdf",
|
||||
doc = "application/msword",
|
||||
docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
txt = "text/plain",
|
||||
md = "text/markdown",
|
||||
csv = "text/csv",
|
||||
rtf = "application/rtf",
|
||||
odt = "application/vnd.oasis.opendocument.text",
|
||||
json = "application/json",
|
||||
zip = "application/zip",
|
||||
xls = "application/vnd.ms-excel",
|
||||
xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
ods = "application/vnd.oasis.opendocument.spreadsheet",
|
||||
ppt = "application/vnd.ms-powerpoint",
|
||||
pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
odp = "application/vnd.oasis.opendocument.presentation",
|
||||
|
||||
// TODO add more mime types.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ export enum TimelineItemType {
|
|||
Note = "Note",
|
||||
}
|
||||
|
||||
export interface TimelineAttachment {
|
||||
name: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
date: Date;
|
||||
note?: string;
|
||||
|
|
@ -23,6 +28,7 @@ export interface TimelineItem {
|
|||
state?: BaseModel;
|
||||
icon: IconProp;
|
||||
iconColor: Color;
|
||||
attachments?: Array<TimelineAttachment>;
|
||||
}
|
||||
|
||||
export interface EventItemLabel {
|
||||
|
|
@ -46,6 +52,7 @@ export interface ComponentProps {
|
|||
anotherStatusColor?: Color | undefined;
|
||||
eventSecondDescription: string;
|
||||
labels?: Array<EventItemLabel> | undefined;
|
||||
eventAttachments?: Array<TimelineAttachment> | undefined;
|
||||
}
|
||||
|
||||
const EventItem: FunctionComponent<ComponentProps> = (
|
||||
|
|
@ -102,6 +109,31 @@ const EventItem: FunctionComponent<ComponentProps> = (
|
|||
</div>
|
||||
)}
|
||||
|
||||
{props.eventAttachments && props.eventAttachments.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
Attachments
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{props.eventAttachments.map(
|
||||
(attachment: TimelineAttachment, index: number) => {
|
||||
return (
|
||||
<a
|
||||
href={attachment.downloadUrl}
|
||||
key={index}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 hover:text-indigo-500 text-sm break-words"
|
||||
>
|
||||
{attachment.name}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.eventSecondDescription && (
|
||||
<div className="mt-3 text-gray-500 text-sm active-event-box-body-second-description">
|
||||
{props.eventSecondDescription}
|
||||
|
|
@ -278,6 +310,29 @@ const EventItem: FunctionComponent<ComponentProps> = (
|
|||
<p>
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
</p>
|
||||
{item.attachments &&
|
||||
item.attachments.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{item.attachments.map(
|
||||
(
|
||||
attachment: TimelineAttachment,
|
||||
attachmentIndex: number,
|
||||
) => {
|
||||
return (
|
||||
<a
|
||||
key={attachmentIndex}
|
||||
href={attachment.downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 hover:text-indigo-500 text-sm break-words"
|
||||
>
|
||||
{attachment.name}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { FILE_URL } from "../../Config";
|
||||
import API from "../../Utils/API/API";
|
||||
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
|
||||
import ComponentLoader from "../ComponentLoader/ComponentLoader";
|
||||
import Icon, { SizeProp } from "../Icon/Icon";
|
||||
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
||||
import CommonURL from "../../../Types/API/URL";
|
||||
|
|
@ -34,6 +33,14 @@ export interface ComponentProps {
|
|||
error?: string | undefined;
|
||||
}
|
||||
|
||||
type UploadStatus = {
|
||||
id: string;
|
||||
name: string;
|
||||
progress: number;
|
||||
status: "uploading" | "error";
|
||||
errorMessage?: string | undefined;
|
||||
};
|
||||
|
||||
const FilePicker: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
|
|
@ -42,6 +49,57 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
|||
const [filesModel, setFilesModel] = useState<Array<FileModel>>([]);
|
||||
|
||||
const [acceptTypes, setAcceptTypes] = useState<Dictionary<Array<string>>>({});
|
||||
const [uploadStatuses, setUploadStatuses] = useState<Array<UploadStatus>>([]);
|
||||
|
||||
const addUploadStatus = (status: UploadStatus): void => {
|
||||
setUploadStatuses((current: Array<UploadStatus>) => [
|
||||
...current,
|
||||
status,
|
||||
]);
|
||||
};
|
||||
|
||||
const updateUploadStatus = (
|
||||
id: string,
|
||||
updates: Partial<UploadStatus>,
|
||||
): void => {
|
||||
setUploadStatuses((current: Array<UploadStatus>) =>
|
||||
current.map((upload: UploadStatus) =>
|
||||
upload.id === id
|
||||
? {
|
||||
...upload,
|
||||
...updates,
|
||||
}
|
||||
: upload,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const updateUploadProgress = (id: string, total?: number, loaded?: number): void => {
|
||||
setUploadStatuses((current: Array<UploadStatus>) =>
|
||||
current.map((upload: UploadStatus) => {
|
||||
if (upload.id !== id || upload.status === "error") {
|
||||
return upload;
|
||||
}
|
||||
|
||||
const hasTotal: boolean = Boolean(total && total > 0);
|
||||
const progressFromEvent: number | null = hasTotal
|
||||
? Math.min(100, Math.round(((loaded || 0) / (total as number)) * 100))
|
||||
: null;
|
||||
const fallbackProgress: number = Math.min(upload.progress + 5, 95);
|
||||
|
||||
return {
|
||||
...upload,
|
||||
progress: progressFromEvent !== null ? progressFromEvent : fallbackProgress,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const removeUploadStatus = (id: string): void => {
|
||||
setUploadStatuses((current: Array<UploadStatus>) =>
|
||||
current.filter((upload: UploadStatus) => upload.id !== id),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const _acceptTypes: Dictionary<Array<string>> = {};
|
||||
|
|
@ -77,49 +135,128 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
|||
}
|
||||
}, [props.value]);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: acceptTypes,
|
||||
multiple: props.isMultiFilePicker,
|
||||
noClick: true,
|
||||
disabled: props.readOnly || isLoading,
|
||||
onDrop: async (acceptedFiles: Array<File>) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (props.readOnly) {
|
||||
return;
|
||||
}
|
||||
if (props.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// Upload these files.
|
||||
const filesResult: Array<FileModel> = [];
|
||||
const resolveMimeType = (file: File): MimeType | undefined => {
|
||||
const direct: string | undefined = file.type || undefined;
|
||||
if (direct && Object.values(MimeType).includes(direct as MimeType)) {
|
||||
return direct as MimeType;
|
||||
}
|
||||
|
||||
// fallback based on extension
|
||||
const ext: string | undefined = file.name
|
||||
.split(".")
|
||||
.pop()
|
||||
?.toLowerCase();
|
||||
if (!ext) {
|
||||
return undefined;
|
||||
}
|
||||
const map: { [key: string]: MimeType } = {
|
||||
png: MimeType.png,
|
||||
jpg: MimeType.jpg,
|
||||
jpeg: MimeType.jpeg,
|
||||
svg: MimeType.svg,
|
||||
gif: MimeType.gif,
|
||||
webp: MimeType.webp,
|
||||
pdf: MimeType.pdf,
|
||||
doc: MimeType.doc,
|
||||
docx: MimeType.docx,
|
||||
txt: MimeType.txt,
|
||||
log: MimeType.txt,
|
||||
md: MimeType.md,
|
||||
markdown: MimeType.md,
|
||||
csv: MimeType.csv,
|
||||
json: MimeType.json,
|
||||
zip: MimeType.zip,
|
||||
rtf: MimeType.rtf,
|
||||
odt: MimeType.odt,
|
||||
xls: MimeType.xls,
|
||||
xlsx: MimeType.xlsx,
|
||||
ods: MimeType.ods,
|
||||
ppt: MimeType.ppt,
|
||||
pptx: MimeType.pptx,
|
||||
odp: MimeType.odp,
|
||||
};
|
||||
return map[ext];
|
||||
};
|
||||
|
||||
for (const acceptedFile of acceptedFiles) {
|
||||
const fileModel: FileModel = new FileModel();
|
||||
fileModel.name = acceptedFile.name;
|
||||
const uploadId: string = `${acceptedFile.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
addUploadStatus({
|
||||
id: uploadId,
|
||||
name: acceptedFile.name,
|
||||
progress: 0,
|
||||
status: "uploading",
|
||||
});
|
||||
|
||||
const arrayBuffer: ArrayBuffer = await acceptedFile.arrayBuffer();
|
||||
try {
|
||||
const fileModel: FileModel = new FileModel();
|
||||
fileModel.name = acceptedFile.name;
|
||||
const arrayBuffer: ArrayBuffer = await acceptedFile.arrayBuffer();
|
||||
const fileBuffer: Uint8Array = new Uint8Array(arrayBuffer);
|
||||
fileModel.file = Buffer.from(fileBuffer);
|
||||
fileModel.isPublic = false;
|
||||
fileModel.fileType = resolveMimeType(acceptedFile) || MimeType.txt; // default to text/plain to satisfy required field
|
||||
|
||||
const fileBuffer: Uint8Array = new Uint8Array(arrayBuffer);
|
||||
fileModel.file = Buffer.from(fileBuffer);
|
||||
fileModel.isPublic = false;
|
||||
fileModel.fileType = acceptedFile.type as MimeType;
|
||||
|
||||
const result: HTTPResponse<FileModel> =
|
||||
(await ModelAPI.create<FileModel>({
|
||||
model: fileModel,
|
||||
modelType: FileModel,
|
||||
requestOptions: {
|
||||
overrideRequestUrl: CommonURL.fromURL(FILE_URL),
|
||||
},
|
||||
})) as HTTPResponse<FileModel>;
|
||||
filesResult.push(result.data as FileModel);
|
||||
const result: HTTPResponse<FileModel> =
|
||||
(await ModelAPI.create<FileModel>({
|
||||
model: fileModel,
|
||||
modelType: FileModel,
|
||||
requestOptions: {
|
||||
overrideRequestUrl: CommonURL.fromURL(FILE_URL),
|
||||
apiRequestOptions: {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
updateUploadProgress(
|
||||
uploadId,
|
||||
progressEvent.total,
|
||||
progressEvent.loaded,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as HTTPResponse<FileModel>;
|
||||
filesResult.push(result.data as FileModel);
|
||||
removeUploadStatus(uploadId);
|
||||
} catch (uploadErr) {
|
||||
const friendlyMessage: string = API.getFriendlyMessage(uploadErr);
|
||||
updateUploadStatus(uploadId, {
|
||||
status: "error",
|
||||
errorMessage: friendlyMessage,
|
||||
progress: 100,
|
||||
});
|
||||
setError(friendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
setFilesModel(filesResult);
|
||||
if (filesResult.length > 0) {
|
||||
const updatedFiles: Array<FileModel> = props.isMultiFilePicker
|
||||
? [...filesModel, ...filesResult]
|
||||
: filesResult;
|
||||
|
||||
props.onBlur?.();
|
||||
props.onChange?.(filesResult);
|
||||
setFilesModel(updatedFiles);
|
||||
|
||||
props.onBlur?.();
|
||||
props.onChange?.(updatedFiles);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -127,37 +264,72 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
|||
|
||||
const getThumbs: GetThumbsFunction = (): Array<ReactElement> => {
|
||||
return filesModel.map((file: FileModel, i: number) => {
|
||||
if (!file.file) {
|
||||
return <></>;
|
||||
}
|
||||
const hasPreview: boolean = Boolean(file.file);
|
||||
const key: string = file._id?.toString() || `${file.name || "file"}-${i}`;
|
||||
const removeFile = (): void => {
|
||||
const tempFileModel: Array<FileModel> = [...filesModel];
|
||||
tempFileModel.splice(i, 1);
|
||||
setFilesModel(tempFileModel);
|
||||
props.onChange?.(tempFileModel);
|
||||
};
|
||||
|
||||
const blob: Blob = new Blob([file.file!.buffer as ArrayBuffer], {
|
||||
type: file.fileType as string,
|
||||
});
|
||||
const url: string = URL.createObjectURL(blob);
|
||||
|
||||
return (
|
||||
<div key={file.name}>
|
||||
<div className="text-right flex justify-end">
|
||||
if (hasPreview && file.file) {
|
||||
const blob: Blob = new Blob([file.file!.buffer as ArrayBuffer], {
|
||||
type: file.fileType as string,
|
||||
});
|
||||
const url: string = URL.createObjectURL(blob);
|
||||
return (
|
||||
<div key={key} className="relative flex-none">
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeFile}
|
||||
className="bg-gray-600 text-white text-xs px-2 py-1 rounded absolute left-1 top-1 hover:bg-gray-700"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<Icon
|
||||
icon={IconProp.Close}
|
||||
className="bg-gray-400 rounded text-white h-7 w-7 align-right items-right p-1 absolute hover:bg-gray-500 cursor-pointer -ml-7"
|
||||
className="bg-gray-400 rounded text-white h-6 w-6 flex items-center justify-center absolute -right-2 -top-2 hover:bg-gray-500 cursor-pointer"
|
||||
size={SizeProp.Regular}
|
||||
onClick={() => {
|
||||
const tempFileModel: Array<FileModel> = [...filesModel];
|
||||
tempFileModel.splice(i, 1);
|
||||
setFilesModel(tempFileModel);
|
||||
props.onChange?.(tempFileModel);
|
||||
}}
|
||||
onClick={removeFile}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={url}
|
||||
className="rounded"
|
||||
style={{
|
||||
height: "100px",
|
||||
}}
|
||||
className="rounded border border-gray-200 h-24 w-24 object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex w-full items-center justify-between rounded border border-gray-200 bg-gray-50 px-3 py-2"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-left">
|
||||
<Icon icon={IconProp.File} className="text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{file.name || `File ${i + 1}`}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{file.fileType || "Unknown type"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-gray-600 hover:text-gray-800"
|
||||
onClick={removeFile}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<Icon
|
||||
icon={IconProp.Close}
|
||||
className="text-gray-400 hover:text-gray-600 cursor-pointer"
|
||||
onClick={removeFile}
|
||||
size={SizeProp.Regular}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -165,106 +337,162 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
|||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center w-full">
|
||||
<ComponentLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const hasActiveUploads: boolean = uploadStatuses.some(
|
||||
(upload: UploadStatus) => upload.status === "uploading",
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4 w-full">
|
||||
<div
|
||||
onClick={() => {
|
||||
props.onClick?.();
|
||||
props.onFocus?.();
|
||||
}}
|
||||
data-testid={props.dataTestId}
|
||||
className="flex max-w-lg justify-center rounded-md border-2 border-dashed border-gray-300 px-6 pt-5 pb-6"
|
||||
className={`flex w-full justify-center rounded-md border-2 border-dashed px-6 py-8 transition ${props.readOnly ? "cursor-not-allowed bg-gray-50 border-gray-200" : "bg-white border-gray-300"} ${hasActiveUploads ? "ring-1 ring-indigo-200" : ""} ${isDragActive ? "border-indigo-400" : ""}`}
|
||||
>
|
||||
{props.isMultiFilePicker ||
|
||||
(filesModel.length === 0 && (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: "space-y-1 text-center",
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 48 48"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<label className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500">
|
||||
{!props.placeholder && !error && (
|
||||
<span>{"Upload a file"}</span>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className:
|
||||
"w-full flex flex-col items-center justify-center space-y-3 text-center",
|
||||
"aria-busy": hasActiveUploads || isLoading,
|
||||
})}
|
||||
>
|
||||
{(filesModel.length === 0 || props.isMultiFilePicker) && (
|
||||
<>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 48 48"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<div className="flex flex-col items-center text-sm text-gray-600 space-y-1">
|
||||
<label className="relative cursor-pointer rounded-md bg-white px-4 py-2 font-medium text-indigo-600 hover:text-indigo-500">
|
||||
<span>
|
||||
<span>{error}</span>
|
||||
{props.placeholder
|
||||
? props.placeholder
|
||||
: filesModel.length > 0
|
||||
? "Add more files"
|
||||
: "Upload files"}
|
||||
</span>
|
||||
<input
|
||||
tabIndex={props.tabIndex}
|
||||
{...(getInputProps() as any)}
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
className="sr-only"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
{isDragActive
|
||||
? "Release to start uploading"
|
||||
: filesModel.length === 0
|
||||
? "Click to choose files"
|
||||
: "Click to add more"}{" "}
|
||||
or drag & drop.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{props.mimeTypes && props.mimeTypes?.length > 0 && (
|
||||
<span>Types: </span>
|
||||
)}
|
||||
{props.mimeTypes &&
|
||||
props.mimeTypes
|
||||
.map((type: MimeType) => {
|
||||
const enumKey: string | undefined =
|
||||
Object.keys(MimeType)[
|
||||
Object.values(MimeType).indexOf(type)
|
||||
];
|
||||
return enumKey?.toUpperCase() || "";
|
||||
})
|
||||
.filter((item: string | undefined, pos: number, array: Array<string | undefined>) => {
|
||||
return array.indexOf(item) === pos;
|
||||
})
|
||||
.join(", ")}
|
||||
{props.mimeTypes && props.mimeTypes?.length > 0 && <span>.</span>} Max 10MB each.
|
||||
</p>
|
||||
{error && (
|
||||
<p className="text-xs text-red-500 font-medium">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{props.placeholder && !error && (
|
||||
<span>{props.placeholder}</span>
|
||||
)}
|
||||
|
||||
<input
|
||||
tabIndex={props.tabIndex}
|
||||
{...(getInputProps() as any)}
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
className="sr-only"
|
||||
/>
|
||||
</label>
|
||||
<p className="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{props.mimeTypes && props.mimeTypes?.length > 0 && (
|
||||
<span>File types: </span>
|
||||
)}
|
||||
{props.mimeTypes &&
|
||||
props.mimeTypes
|
||||
.map((type: MimeType) => {
|
||||
const enumKey: string | undefined =
|
||||
Object.keys(MimeType)[
|
||||
Object.values(MimeType).indexOf(type)
|
||||
];
|
||||
return enumKey?.toUpperCase() || "";
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
item: string | undefined,
|
||||
pos: number,
|
||||
array: Array<string | undefined>,
|
||||
) => {
|
||||
return array.indexOf(item) === pos;
|
||||
},
|
||||
)
|
||||
.join(", ")}
|
||||
{props.mimeTypes && props.mimeTypes?.length > 0 && (
|
||||
<span>.</span>
|
||||
)}
|
||||
10 MB or less.
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<aside>{getThumbs()}</aside>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{uploadStatuses.length > 0 && (
|
||||
<div className="space-y-2 w-full">
|
||||
<p className="text-sm font-medium text-gray-700 text-left">
|
||||
{hasActiveUploads ? "Uploading files" : "Upload status"}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{uploadStatuses.map((upload: UploadStatus) => (
|
||||
<div
|
||||
key={upload.id}
|
||||
className={`rounded border px-3 py-2 ${upload.status === "error" ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<p className="font-medium text-gray-800 truncate">
|
||||
{upload.name}
|
||||
</p>
|
||||
<span
|
||||
className={`text-xs ${upload.status === "error" ? "text-red-600" : "text-gray-500"}`}
|
||||
>
|
||||
{upload.status === "error"
|
||||
? "Failed"
|
||||
: `${upload.progress}%`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 rounded bg-gray-200 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${upload.status === "error" ? "bg-red-400" : "bg-indigo-500"}`}
|
||||
style={{ width: `${Math.min(upload.progress, 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
{upload.status === "error" && upload.errorMessage && (
|
||||
<p className="mt-2 text-xs text-red-600 text-left">
|
||||
{upload.errorMessage}
|
||||
</p>
|
||||
)}
|
||||
{upload.status === "error" && (
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-gray-600 hover:text-gray-800"
|
||||
onClick={() => {
|
||||
removeUploadStatus(upload.id);
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{filesModel.length > 0 && (
|
||||
<div className="space-y-2 w-full">
|
||||
<p className="text-sm font-medium text-gray-700 text-left">
|
||||
Uploaded files
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">{getThumbs()}</div>
|
||||
</div>
|
||||
)}
|
||||
{props.error && (
|
||||
<p data-testid="error-message" className="mt-1 text-sm text-red-400">
|
||||
<p data-testid="error-message" className="text-sm text-red-400">
|
||||
{props.error}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,14 @@ const FormField: <T extends GenericObject>(
|
|||
? getFieldType(props.field.fieldType)
|
||||
: "text";
|
||||
|
||||
const isFileField: boolean =
|
||||
props.field.fieldType === FormFieldSchemaType.File ||
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile ||
|
||||
props.field.fieldType === FormFieldSchemaType.MultipleFiles;
|
||||
|
||||
const isMultiFileField: boolean =
|
||||
props.field.fieldType === FormFieldSchemaType.MultipleFiles;
|
||||
|
||||
if (Object.keys(props.field.field || {}).length === 0) {
|
||||
throw new BadDataException("Object cannot be without Field");
|
||||
}
|
||||
|
|
@ -569,49 +577,58 @@ const FormField: <T extends GenericObject>(
|
|||
/>
|
||||
)}
|
||||
|
||||
{(props.field.fieldType === FormFieldSchemaType.File ||
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile) && (
|
||||
{isFileField && (
|
||||
<FilePicker
|
||||
error={props.touched && props.error ? props.error : undefined}
|
||||
tabIndex={index}
|
||||
onChange={async (files: Array<FileModel>) => {
|
||||
let fileResult: FileModel | Array<FileModel> | null = files.map(
|
||||
const strippedFiles: Array<FileModel> = files.map(
|
||||
(i: FileModel) => {
|
||||
const strippedModel: FileModel = new FileModel();
|
||||
strippedModel._id = i._id!;
|
||||
if (i.name) {
|
||||
strippedModel.name = i.name;
|
||||
}
|
||||
if (i.fileType) {
|
||||
strippedModel.fileType = i.fileType;
|
||||
}
|
||||
return strippedModel;
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
(props.field.fieldType === FormFieldSchemaType.File ||
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile) &&
|
||||
Array.isArray(fileResult)
|
||||
) {
|
||||
if (fileResult.length > 0) {
|
||||
fileResult = fileResult[0] as FileModel;
|
||||
let fileResult: FileModel | Array<FileModel> | null =
|
||||
strippedFiles;
|
||||
|
||||
if (!isMultiFileField) {
|
||||
if (strippedFiles.length > 0) {
|
||||
fileResult = strippedFiles[0] as FileModel;
|
||||
} else {
|
||||
fileResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
onChange(fileResult);
|
||||
onChange(fileResult as any);
|
||||
props.setFieldValue(props.fieldName, fileResult);
|
||||
}}
|
||||
onBlur={async () => {
|
||||
props.setFieldTouched(props.fieldName, true);
|
||||
}}
|
||||
mimeTypes={
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile
|
||||
? [MimeType.png, MimeType.jpeg, MimeType.jpg, MimeType.svg]
|
||||
: []
|
||||
props.field.fileTypes
|
||||
? props.field.fileTypes
|
||||
: props.field.fieldType === FormFieldSchemaType.ImageFile
|
||||
? [MimeType.png, MimeType.jpeg, MimeType.jpg, MimeType.svg]
|
||||
: []
|
||||
}
|
||||
isMultiFilePicker={isMultiFileField}
|
||||
dataTestId={props.field.dataTestId}
|
||||
initialValue={
|
||||
props.currentValues &&
|
||||
(props.currentValues as any)[props.fieldName]
|
||||
? (props.currentValues as any)[props.fieldName]
|
||||
: []
|
||||
: isMultiFileField
|
||||
? []
|
||||
: undefined
|
||||
}
|
||||
placeholder={props.field.placeholder || ""}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ enum FormFieldSchemaType {
|
|||
Color = "Color",
|
||||
Dropdown = "Dropdown",
|
||||
File = "File",
|
||||
MultipleFiles = "MultipleFiles",
|
||||
MultiSelectDropdown = "MultiSelectDropdown",
|
||||
OptionChooserButton = "OptionChooserButton",
|
||||
Toggle = "Boolean",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import URL from "../../../Types/API/URL";
|
||||
import type { RequestOptions as CoreRequestOptions } from "../../../Utils/API";
|
||||
import Dictionary from "../../../Types/Dictionary";
|
||||
|
||||
export default interface RequestOptions {
|
||||
requestHeaders?: Dictionary<string> | undefined;
|
||||
overrideRequestUrl?: URL | undefined;
|
||||
apiRequestOptions?: CoreRequestOptions | undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,9 @@ export default class ModelAPI {
|
|||
data: data.data,
|
||||
},
|
||||
headers: this.getCommonHeaders(data.requestOptions),
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
|
@ -156,6 +159,9 @@ export default class ModelAPI {
|
|||
...this.getCommonHeaders(data.requestOptions),
|
||||
...(data.requestOptions?.requestHeaders || {}),
|
||||
},
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (apiResult.isSuccess() && apiResult instanceof HTTPResponse) {
|
||||
|
|
@ -231,6 +237,9 @@ export default class ModelAPI {
|
|||
limit: data.limit.toString(),
|
||||
skip: data.skip.toString(),
|
||||
},
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
|
@ -294,6 +303,9 @@ export default class ModelAPI {
|
|||
query: JSONFunctions.serialize(data.query as JSONObject),
|
||||
},
|
||||
headers,
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
|
@ -383,6 +395,9 @@ export default class ModelAPI {
|
|||
select: JSONFunctions.serialize(data.select as JSONObject) || {},
|
||||
},
|
||||
headers: this.getCommonHeaders(data.requestOptions),
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
|
@ -426,6 +441,9 @@ export default class ModelAPI {
|
|||
method: HTTPMethod.DELETE,
|
||||
url: apiUrl,
|
||||
headers: this.getCommonHeaders(data.requestOptions),
|
||||
...(data.requestOptions?.apiRequestOptions
|
||||
? { options: data.requestOptions.apiRequestOptions }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (result.isSuccess()) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ import URL from "../Types/API/URL";
|
|||
import Dictionary from "../Types/Dictionary";
|
||||
import APIException from "../Types/Exception/ApiException";
|
||||
import { JSONArray, JSONObject } from "../Types/JSON";
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosProgressEvent,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
} from "axios";
|
||||
import Sleep from "../Types/Sleep";
|
||||
import type { Agent as HttpAgent } from "http";
|
||||
import type { Agent as HttpsAgent } from "https";
|
||||
|
|
@ -26,6 +31,7 @@ export interface RequestOptions {
|
|||
httpsAgent?: HttpsAgent | undefined;
|
||||
skipAuthRefresh?: boolean | undefined;
|
||||
hasAttemptedAuthRefresh?: boolean | undefined;
|
||||
onUploadProgress?: ((event: AxiosProgressEvent) => void) | undefined;
|
||||
}
|
||||
|
||||
export interface APIRequestOptions {
|
||||
|
|
@ -408,6 +414,10 @@ export default class API {
|
|||
options.httpsAgent;
|
||||
}
|
||||
|
||||
if (options?.onUploadProgress) {
|
||||
axiosOptions.onUploadProgress = options.onUploadProgress;
|
||||
}
|
||||
|
||||
result = await axios(axiosOptions);
|
||||
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -324,6 +324,16 @@ const AlertFeedElement: FunctionComponent<ComponentProps> = (
|
|||
title: "Private Note",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
description:
|
||||
"Attach files that should be visible to the alert response team.",
|
||||
title: "Attachments",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
formType: FormType.Create,
|
||||
}}
|
||||
|
|
|
|||
90
Dashboard/src/Components/Attachment/AttachmentList.tsx
Normal file
90
Dashboard/src/Components/Attachment/AttachmentList.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import FileModel from "Common/Models/DatabaseModels/File";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import { APP_API_URL } from "Common/UI/Config";
|
||||
|
||||
export interface AttachmentListProps {
|
||||
modelId?: string | null;
|
||||
attachments?: Array<FileModel> | null | undefined;
|
||||
attachmentApiPath: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
buildAttachmentUrl?: (fileId: string) => string;
|
||||
}
|
||||
|
||||
const AttachmentList: FunctionComponent<AttachmentListProps> = (
|
||||
props: AttachmentListProps,
|
||||
): ReactElement | null => {
|
||||
const {
|
||||
modelId,
|
||||
attachments,
|
||||
attachmentApiPath,
|
||||
title = "Attachments",
|
||||
className = "space-y-1",
|
||||
buildAttachmentUrl,
|
||||
} = props;
|
||||
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachmentLinks: Array<ReactElement> = [];
|
||||
|
||||
for (const file of attachments) {
|
||||
const fileIdentifier: string | ObjectID | null | undefined =
|
||||
file._id || file.id;
|
||||
|
||||
if (!fileIdentifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileIdAsString: string = fileIdentifier.toString();
|
||||
|
||||
let downloadUrl: string | null = null;
|
||||
|
||||
if (buildAttachmentUrl) {
|
||||
downloadUrl = buildAttachmentUrl(fileIdAsString);
|
||||
} else if (modelId) {
|
||||
downloadUrl = URL.fromURL(APP_API_URL)
|
||||
.addRoute(attachmentApiPath)
|
||||
.addRoute(`/${modelId}`)
|
||||
.addRoute(`/${fileIdAsString}`)
|
||||
.toString();
|
||||
}
|
||||
|
||||
if (!downloadUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attachmentLinks.push(
|
||||
<li key={fileIdAsString}>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
{file.name || "Download attachment"}
|
||||
</a>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!attachmentLinks.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
{title}
|
||||
</div>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
|
||||
{attachmentLinks}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentList;
|
||||
|
|
@ -380,6 +380,16 @@ const IncidentFeedElement: FunctionComponent<ComponentProps> = (
|
|||
title: "Public Note",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
description:
|
||||
"Attach files that should be shared with subscribers on the status page.",
|
||||
title: "Attachments",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
postedAt: true,
|
||||
|
|
@ -452,6 +462,16 @@ const IncidentFeedElement: FunctionComponent<ComponentProps> = (
|
|||
title: "Private Note",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
description:
|
||||
"Attach files that should be visible to the incident response team.",
|
||||
title: "Attachments",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
formType: FormType.Create,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,15 @@ import Navigation from "Common/UI/Utils/Navigation";
|
|||
import AlertInternalNote from "Common/Models/DatabaseModels/AlertInternalNote";
|
||||
import AlertNoteTemplate from "Common/Models/DatabaseModels/AlertNoteTemplate";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
|
||||
const AlertDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
|
|
@ -165,10 +168,26 @@ const AlertDelete: FunctionComponent<PageComponentProps> = (
|
|||
"Add a private note to this alert here. This is private to your team and is not visible on Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be visible to the alert response team.",
|
||||
},
|
||||
]}
|
||||
showAs={ShowAs.List}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
|
|
@ -243,9 +262,21 @@ const AlertDelete: FunctionComponent<PageComponentProps> = (
|
|||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
colSpan: 2,
|
||||
getElement: (item: AlertInternalNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<AttachmentList
|
||||
modelId={getModelIdString(item)}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/alert-internal-note/attachment"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -25,12 +25,15 @@ import Navigation from "Common/UI/Utils/Navigation";
|
|||
import IncidentInternalNote from "Common/Models/DatabaseModels/IncidentInternalNote";
|
||||
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
|
||||
const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
|
|
@ -165,10 +168,26 @@ const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
|||
"Add a private note to this incident here. This is private to your team and is not visible on Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be visible to the incident response team.",
|
||||
},
|
||||
]}
|
||||
showAs={ShowAs.List}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
|
|
@ -243,9 +262,21 @@ const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
|||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
colSpan: 2,
|
||||
getElement: (item: IncidentInternalNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<AttachmentList
|
||||
modelId={getModelIdString(item)}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/incident-internal-note/attachment"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import OneUptimeDate from "Common/Types/Date";
|
|||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
|
@ -35,6 +36,8 @@ import React, {
|
|||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
|
||||
const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
|
|
@ -191,6 +194,16 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
"This note is visible on your Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be shared with subscribers on the status page.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: true,
|
||||
|
|
@ -224,6 +237,10 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
subscriberNotificationStatusMessage: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
|
|
@ -300,9 +317,21 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-1 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3 space-y-3 text-sm text-gray-800",
|
||||
colSpan: 2,
|
||||
getElement: (item: IncidentPublicNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<AttachmentList
|
||||
modelId={getModelIdString(item)}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/incident-public-note/attachment"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ import React, {
|
|||
useState,
|
||||
} from "react";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
|
||||
const ScheduledMaintenanceDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
|
|
@ -177,10 +180,26 @@ const ScheduledMaintenanceDelete: FunctionComponent<PageComponentProps> = (
|
|||
"Add a private note to this scheduled maintenance here",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be visible to the scheduled maintenance response team.",
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showAs={ShowAs.List}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
|
|
@ -257,9 +276,23 @@ const ScheduledMaintenanceDelete: FunctionComponent<PageComponentProps> = (
|
|||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
colSpan: 2,
|
||||
getElement: (
|
||||
item: ScheduledMaintenanceInternalNote,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<AttachmentList
|
||||
modelId={getModelIdString(item)}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/scheduled-maintenance-internal-note/attachment"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ import User from "Common/Models/DatabaseModels/User";
|
|||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import StatusPageSubscriberNotificationStatus from "Common/Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
||||
import SubscriberNotificationStatus from "../../../Components/StatusPageSubscribers/SubscriberNotificationStatus";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/MarkdownViewer";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import AttachmentList from "../../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../../Utils/ModelId";
|
||||
|
||||
const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
|
|
@ -204,6 +207,16 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
"This note is visible on your Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be shared with subscribers on the status page.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: true,
|
||||
|
|
@ -237,6 +250,10 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
subscriberNotificationStatusMessage: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
|
|
@ -314,9 +331,23 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
|||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3 space-y-3 text-sm text-gray-800",
|
||||
colSpan: 2,
|
||||
getElement: (
|
||||
item: ScheduledMaintenancePublicNote,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<AttachmentList
|
||||
modelId={getModelIdString(item)}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/scheduled-maintenance-public-note/attachment"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,17 @@ const AnnouncementCreate: FunctionComponent<
|
|||
"Add an announcement note",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
stepId: "basic",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be available with this announcement on the status page.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
statusPages: true,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
|||
import Route from "Common/Types/API/Route";
|
||||
import Page from "Common/UI/Components/Page/Page";
|
||||
import { ModalWidth } from "Common/UI/Components/Modal/Modal";
|
||||
import AttachmentList from "../../Components/Attachment/AttachmentList";
|
||||
import { getModelIdString } from "../../Utils/ModelId";
|
||||
|
||||
const AnnouncementView: FunctionComponent<
|
||||
PageComponentProps
|
||||
|
|
@ -131,6 +133,17 @@ const AnnouncementView: FunctionComponent<
|
|||
"Add an announcement note",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: true,
|
||||
},
|
||||
title: "Attachments",
|
||||
stepId: "basic",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
description:
|
||||
"Attach files that should be available with this announcement on the status page.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
statusPages: true,
|
||||
|
|
@ -201,6 +214,10 @@ const AnnouncementView: FunctionComponent<
|
|||
id: "model-detail-status-page-announcement",
|
||||
selectMoreFields: {
|
||||
subscriberNotificationStatusMessage: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -224,6 +241,31 @@ const AnnouncementView: FunctionComponent<
|
|||
title: "Description",
|
||||
fieldType: FieldType.Markdown,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: StatusPageAnnouncement): ReactElement => {
|
||||
const modelIdString: string | null = getModelIdString(item);
|
||||
|
||||
if (!modelIdString || !item.attachments?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AttachmentList
|
||||
modelId={modelIdString}
|
||||
attachments={item.attachments}
|
||||
attachmentApiPath="/status-page-announcement/attachment"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
statusPages: {
|
||||
|
|
|
|||
16
Dashboard/src/Utils/ModelId.ts
Normal file
16
Dashboard/src/Utils/ModelId.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
export interface ModelIdentifier {
|
||||
id?: ObjectID | string | null | undefined;
|
||||
_id?: ObjectID | string | null | undefined;
|
||||
}
|
||||
|
||||
export function getModelIdString(item: ModelIdentifier): string | null {
|
||||
const identifier: ObjectID | string | null | undefined = item.id || item._id;
|
||||
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return identifier.toString();
|
||||
}
|
||||
|
|
@ -23,11 +23,14 @@ import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
|||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EventItem, {
|
||||
ComponentProps as EventItemComponentProps,
|
||||
TimelineAttachment,
|
||||
} from "Common/UI/Components/EventItem/EventItem";
|
||||
import { StatusPageApiRoute } from "Common/ServiceRoute";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import LocalStorage from "Common/UI/Utils/LocalStorage";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import StatusPageAnnouncement from "Common/Models/DatabaseModels/StatusPageAnnouncement";
|
||||
import FileModel from "Common/Models/DatabaseModels/File";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
|
|
@ -42,6 +45,7 @@ type GetAnnouncementEventItemFunctionProps = {
|
|||
monitorsInGroup: Dictionary<Array<ObjectID>>;
|
||||
isPreviewPage: boolean;
|
||||
isSummary: boolean;
|
||||
statusPageId?: ObjectID | null;
|
||||
};
|
||||
|
||||
type GetAnnouncementEventItemFunction = (
|
||||
|
|
@ -57,6 +61,7 @@ export const getAnnouncementEventItem: GetAnnouncementEventItemFunction = (
|
|||
monitorsInGroup,
|
||||
isPreviewPage,
|
||||
isSummary,
|
||||
statusPageId,
|
||||
} = data;
|
||||
|
||||
// Get affected resources based on monitors in the announcement
|
||||
|
|
@ -99,6 +104,47 @@ export const getAnnouncementEventItem: GetAnnouncementEventItemFunction = (
|
|||
}),
|
||||
);
|
||||
|
||||
const statusPageIdString: string | null = statusPageId
|
||||
? statusPageId.toString()
|
||||
: null;
|
||||
const announcementIdString: string | null = announcement.id
|
||||
? announcement.id.toString()
|
||||
: announcement._id
|
||||
? announcement._id.toString()
|
||||
: null;
|
||||
|
||||
const attachments: Array<TimelineAttachment> =
|
||||
statusPageIdString && announcementIdString
|
||||
? (announcement.attachments || [])
|
||||
.map((attachment: FileModel) => {
|
||||
const attachmentId: string | null = attachment.id
|
||||
? attachment.id.toString()
|
||||
: attachment._id
|
||||
? attachment._id.toString()
|
||||
: null;
|
||||
|
||||
if (!attachmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadRoute: Route = Route.fromString(
|
||||
StatusPageApiRoute.toString(),
|
||||
).addRoute(
|
||||
`/status-page-announcement/attachment/${statusPageIdString}/${announcementIdString}/${attachmentId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
name: attachment.name || "Attachment",
|
||||
downloadUrl: downloadRoute.toString(),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(item: TimelineAttachment | null): item is TimelineAttachment => {
|
||||
return Boolean(item);
|
||||
},
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
eventTitle: announcement.title || "",
|
||||
eventDescription: announcement.description,
|
||||
|
|
@ -123,6 +169,11 @@ export const getAnnouncementEventItem: GetAnnouncementEventItemFunction = (
|
|||
announcement.showAnnouncementAt!,
|
||||
)
|
||||
: "",
|
||||
...(attachments.length > 0
|
||||
? {
|
||||
eventAttachments: attachments,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +193,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
const [parsedData, setParsedData] = useState<EventItemComponentProps | null>(
|
||||
null,
|
||||
);
|
||||
const [statusPageId, setStatusPageId] = useState<ObjectID | null>(null);
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
|
|
@ -158,6 +210,8 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
throw new BadDataException("Status Page ID is required");
|
||||
}
|
||||
|
||||
setStatusPageId(id);
|
||||
|
||||
const announcementId: string | undefined =
|
||||
Navigation.getLastParamAsObjectID().toString();
|
||||
|
||||
|
|
@ -227,9 +281,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
monitorsInGroup,
|
||||
isPreviewPage: Boolean(StatusPageUtil.isPreviewPage()),
|
||||
isSummary: false,
|
||||
statusPageId,
|
||||
}),
|
||||
);
|
||||
}, [isLoading]);
|
||||
}, [isLoading, statusPageId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
const [statusPageId, setStatusPageId] = useState<ObjectID | null>(null);
|
||||
|
||||
const [activeAnnounementsParsedData, setActiveAnnouncementsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
|
@ -69,6 +70,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
if (!id) {
|
||||
throw new BadDataException("Status Page ID is required");
|
||||
}
|
||||
setStatusPageId(id);
|
||||
const response: HTTPResponse<JSONObject> = await API.post<JSONObject>({
|
||||
url: URL.fromString(STATUS_PAGE_API_URL.toString()).addRoute(
|
||||
`/announcements/${id.toString()}`,
|
||||
|
|
@ -146,6 +148,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
monitorsInGroup,
|
||||
isPreviewPage: Boolean(StatusPageUtil.isPreviewPage()),
|
||||
isSummary: true,
|
||||
statusPageId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -193,7 +196,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
getAnouncementsParsedData(activeAnnouncement),
|
||||
);
|
||||
setPastAnnouncementsParsedData(getAnouncementsParsedData(pastAnnouncement));
|
||||
}, [isLoading]);
|
||||
}, [isLoading, statusPageId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
|||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EventItem, {
|
||||
ComponentProps as EventItemComponentProps,
|
||||
TimelineAttachment,
|
||||
TimelineItem,
|
||||
TimelineItemType,
|
||||
} from "Common/UI/Components/EventItem/EventItem";
|
||||
import { StatusPageApiRoute } from "Common/ServiceRoute";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import LocalStorage from "Common/UI/Utils/LocalStorage";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
|
|
@ -35,6 +37,7 @@ import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTim
|
|||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import Monitor from "Common/Models/DatabaseModels/Monitor";
|
||||
import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource";
|
||||
import FileModel from "Common/Models/DatabaseModels/File";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
|
|
@ -75,6 +78,16 @@ export const getIncidentEventItem: GetIncidentEventItemFunction = (
|
|||
let currentStateStatus: string = "";
|
||||
let currentStatusColor: Color = Green;
|
||||
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
const statusPageIdString: string | null = statusPageId
|
||||
? statusPageId.toString()
|
||||
: null;
|
||||
const incidentIdString: string | null = incident.id
|
||||
? incident.id.toString()
|
||||
: incident._id
|
||||
? incident._id.toString()
|
||||
: null;
|
||||
|
||||
if (isSummary) {
|
||||
// If this is summary then reverse the order so we show the latest first
|
||||
incidentPublicNotes.sort((a: IncidentPublicNote, b: IncidentPublicNote) => {
|
||||
|
|
@ -95,12 +108,57 @@ export const getIncidentEventItem: GetIncidentEventItemFunction = (
|
|||
incidentPublicNote.incidentId?.toString() === incident.id?.toString() &&
|
||||
incidentPublicNote?.note
|
||||
) {
|
||||
const noteIdString: string | null = incidentPublicNote.id
|
||||
? incidentPublicNote.id.toString()
|
||||
: incidentPublicNote._id
|
||||
? incidentPublicNote._id.toString()
|
||||
: null;
|
||||
|
||||
const attachments: Array<TimelineAttachment> =
|
||||
statusPageIdString && incidentIdString && noteIdString
|
||||
? (incidentPublicNote.attachments || [])
|
||||
.map((attachment: FileModel) => {
|
||||
const attachmentId: string | null = attachment.id
|
||||
? attachment.id.toString()
|
||||
: attachment._id
|
||||
? attachment._id.toString()
|
||||
: null;
|
||||
|
||||
if (!attachmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadRoute: Route = Route.fromString(
|
||||
StatusPageApiRoute.toString(),
|
||||
).addRoute(
|
||||
`/incident-public-note/attachment/${statusPageIdString}/${incidentIdString}/${noteIdString}/${attachmentId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
name: attachment.name || "Attachment",
|
||||
downloadUrl: downloadRoute.toString(),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
attachment: TimelineAttachment | null,
|
||||
): attachment is TimelineAttachment => {
|
||||
return Boolean(attachment);
|
||||
},
|
||||
)
|
||||
: [];
|
||||
|
||||
timeline.push({
|
||||
note: incidentPublicNote?.note,
|
||||
date: incidentPublicNote?.postedAt as Date,
|
||||
type: TimelineItemType.Note,
|
||||
icon: IconProp.Chat,
|
||||
iconColor: Gray500,
|
||||
...(attachments.length > 0
|
||||
? {
|
||||
attachments,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
// If this incident is a sumamry then don't include all the notes .
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
const [currentStatus, setCurrentStatus] = useState<MonitorStatus | null>(
|
||||
null,
|
||||
);
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
|
|
@ -635,6 +636,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
|||
monitorsInGroup,
|
||||
isPreviewPage: StatusPageUtil.isPreviewPage(),
|
||||
isSummary: true,
|
||||
statusPageId,
|
||||
})}
|
||||
isDetailItem={false}
|
||||
key={i}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import EmptyState from "Common/UI/Components/EmptyState/EmptyState";
|
|||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import EventItem, {
|
||||
ComponentProps as EventItemComponentProps,
|
||||
TimelineAttachment,
|
||||
TimelineItem,
|
||||
TimelineItemType,
|
||||
} from "Common/UI/Components/EventItem/EventItem";
|
||||
|
|
@ -35,6 +36,8 @@ import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintena
|
|||
import ScheduledMaintenancePublicNote from "Common/Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
||||
import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
||||
import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource";
|
||||
import { StatusPageApiRoute } from "Common/ServiceRoute";
|
||||
import FileModel from "Common/Models/DatabaseModels/File";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
|
|
@ -76,6 +79,16 @@ export const getScheduledEventEventItem: GetScheduledEventEventItemFunction = (
|
|||
|
||||
const timeline: Array<TimelineItem> = [];
|
||||
|
||||
const statusPageId: ObjectID | null = StatusPageUtil.getStatusPageId();
|
||||
const statusPageIdString: string | null = statusPageId
|
||||
? statusPageId.toString()
|
||||
: null;
|
||||
const scheduledMaintenanceIdString: string | null = scheduledMaintenance.id
|
||||
? scheduledMaintenance.id.toString()
|
||||
: scheduledMaintenance._id
|
||||
? scheduledMaintenance._id.toString()
|
||||
: null;
|
||||
|
||||
if (isSummary) {
|
||||
// If this is summary then reverse the order so we show the latest first
|
||||
scheduledMaintenanceEventsPublicNotes.sort(
|
||||
|
|
@ -107,12 +120,57 @@ export const getScheduledEventEventItem: GetScheduledEventEventItemFunction = (
|
|||
scheduledMaintenance.id?.toString() &&
|
||||
scheduledMaintenancePublicNote?.note
|
||||
) {
|
||||
const noteIdString: string | null = scheduledMaintenancePublicNote.id
|
||||
? scheduledMaintenancePublicNote.id.toString()
|
||||
: scheduledMaintenancePublicNote._id
|
||||
? scheduledMaintenancePublicNote._id.toString()
|
||||
: null;
|
||||
|
||||
const attachments: Array<TimelineAttachment> =
|
||||
statusPageIdString && scheduledMaintenanceIdString && noteIdString
|
||||
? (scheduledMaintenancePublicNote.attachments || [])
|
||||
.map((attachment: FileModel) => {
|
||||
const attachmentId: string | null = attachment.id
|
||||
? attachment.id.toString()
|
||||
: attachment._id
|
||||
? attachment._id.toString()
|
||||
: null;
|
||||
|
||||
if (!attachmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadRoute: Route = Route.fromString(
|
||||
StatusPageApiRoute.toString(),
|
||||
).addRoute(
|
||||
`/scheduled-maintenance-public-note/attachment/${statusPageIdString}/${scheduledMaintenanceIdString}/${noteIdString}/${attachmentId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
name: attachment.name || "Attachment",
|
||||
downloadUrl: downloadRoute.toString(),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
attachment: TimelineAttachment | null,
|
||||
): attachment is TimelineAttachment => {
|
||||
return Boolean(attachment);
|
||||
},
|
||||
)
|
||||
: [];
|
||||
|
||||
timeline.push({
|
||||
note: scheduledMaintenancePublicNote?.note || "",
|
||||
date: scheduledMaintenancePublicNote?.postedAt as Date,
|
||||
type: TimelineItemType.Note,
|
||||
icon: IconProp.Chat,
|
||||
iconColor: Gray500,
|
||||
...(attachments.length > 0
|
||||
? {
|
||||
attachments,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (isSummary) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue