mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
4 Commits
10.0.33
...
files-note
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff0861d68 | ||
|
|
96f0b111c1 | ||
|
|
044ec492da | ||
|
|
6d4462c969 |
@@ -17,7 +17,16 @@ 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 File from "./File";
|
||||
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({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note.",
|
||||
})
|
||||
@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: [
|
||||
|
||||
@@ -17,7 +17,16 @@ 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 File from "./File";
|
||||
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({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note.",
|
||||
})
|
||||
@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: [
|
||||
|
||||
@@ -18,7 +18,16 @@ 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 File from "./File";
|
||||
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({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note.",
|
||||
})
|
||||
@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,
|
||||
|
||||
@@ -16,7 +16,16 @@ 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 File from "./File";
|
||||
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({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note.",
|
||||
})
|
||||
@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: [
|
||||
|
||||
@@ -18,7 +18,16 @@ 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 File from "./File";
|
||||
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({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: File,
|
||||
title: "Attachments",
|
||||
description: "Files attached to this note.",
|
||||
})
|
||||
@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,
|
||||
|
||||
@@ -1269,6 +1269,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
|
||||
@@ -1454,6 +1458,10 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
postedAt: true,
|
||||
note: true,
|
||||
scheduledMaintenanceId: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Ascending,
|
||||
@@ -1993,6 +2001,10 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
postedAt: true,
|
||||
note: true,
|
||||
scheduledMaintenanceId: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
postedAt: SortOrder.Ascending,
|
||||
@@ -3155,6 +3167,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
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +22,7 @@ export class Service extends DatabaseService<Model> {
|
||||
alertId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachments?: Array<File | ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +30,20 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
internalNote.attachments = data.attachments.map(
|
||||
(attachment: File | ObjectID) => {
|
||||
if (attachment instanceof File) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const file: File = new File();
|
||||
file.id = attachment;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +22,7 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachments?: Array<File | ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +30,20 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
internalNote.attachments = data.attachments.map(
|
||||
(attachment: File | ObjectID) => {
|
||||
if (attachment instanceof File) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const file: File = new File();
|
||||
file.id = attachment;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -24,6 +25,7 @@ export class Service extends DatabaseService<Model> {
|
||||
incidentId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachments?: Array<File | ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
@@ -32,6 +34,20 @@ export class Service extends DatabaseService<Model> {
|
||||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
publicNote.attachments = data.attachments.map(
|
||||
(attachment: File | ObjectID) => {
|
||||
if (attachment instanceof File) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const file: File = new File();
|
||||
file.id = attachment;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -21,6 +22,7 @@ export class Service extends DatabaseService<Model> {
|
||||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachments?: Array<File | ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const internalNote: Model = new Model();
|
||||
internalNote.createdByUserId = data.userId;
|
||||
@@ -28,6 +30,20 @@ export class Service extends DatabaseService<Model> {
|
||||
internalNote.projectId = data.projectId;
|
||||
internalNote.note = data.note;
|
||||
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
internalNote.attachments = data.attachments.map(
|
||||
(attachment: File | ObjectID) => {
|
||||
if (attachment instanceof File) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const file: File = new File();
|
||||
file.id = attachment;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: internalNote,
|
||||
props: {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -149,6 +150,7 @@ ${updatedItem.note}
|
||||
scheduledMaintenanceId: ObjectID;
|
||||
projectId: ObjectID;
|
||||
note: string;
|
||||
attachments?: Array<File | ObjectID>;
|
||||
}): Promise<Model> {
|
||||
const publicNote: Model = new Model();
|
||||
publicNote.createdByUserId = data.userId;
|
||||
@@ -157,6 +159,20 @@ ${updatedItem.note}
|
||||
publicNote.note = data.note;
|
||||
publicNote.postedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
if (data.attachments && data.attachments.length > 0) {
|
||||
publicNote.attachments = data.attachments.map(
|
||||
(attachment: File | ObjectID) => {
|
||||
if (attachment instanceof File) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const file: File = new File();
|
||||
file.id = attachment;
|
||||
return file;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.create({
|
||||
data: publicNote,
|
||||
props: {
|
||||
|
||||
@@ -10,6 +10,8 @@ import Color from "../../../Types/Color";
|
||||
import OneUptimeDate from "../../../Types/Date";
|
||||
import IconProp from "../../../Types/Icon/IconProp";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import FileModel from "../../../Models/DatabaseModels/File";
|
||||
import FileList from "../FileList/FileList";
|
||||
|
||||
export enum TimelineItemType {
|
||||
StateChange = "StateChange",
|
||||
@@ -23,6 +25,7 @@ export interface TimelineItem {
|
||||
state?: BaseModel;
|
||||
icon: IconProp;
|
||||
iconColor: Color;
|
||||
attachments?: Array<FileModel> | undefined;
|
||||
}
|
||||
|
||||
export interface EventItemLabel {
|
||||
@@ -278,6 +281,11 @@ const EventItem: FunctionComponent<ComponentProps> = (
|
||||
<p>
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
</p>
|
||||
<FileList
|
||||
files={item.attachments}
|
||||
containerClassName="mt-3 space-y-2"
|
||||
linkClassName="text-indigo-600 hover:text-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
64
Common/UI/Components/FileList/FileList.tsx
Normal file
64
Common/UI/Components/FileList/FileList.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import Link from "../Link/Link";
|
||||
import FileModel from "../../../Models/DatabaseModels/File";
|
||||
import URL from "../../../Types/API/URL";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { FILE_URL } from "../../Config";
|
||||
|
||||
export interface FileListProps {
|
||||
files?: Array<FileModel | undefined | null> | null | undefined;
|
||||
containerClassName?: string;
|
||||
linkClassName?: string;
|
||||
openInNewTab?: boolean;
|
||||
getFileName?: (file: FileModel, index: number) => string;
|
||||
}
|
||||
|
||||
const DEFAULT_LINK_CLASSNAME: string = "text-primary-500 hover:underline";
|
||||
const DEFAULT_CONTAINER_CLASSNAME: string = "flex flex-col space-y-2";
|
||||
|
||||
const FileList: FunctionComponent<FileListProps> = (
|
||||
props: FileListProps,
|
||||
): ReactElement | null => {
|
||||
const files: Array<FileModel> = (props.files || []).filter(
|
||||
(file: FileModel | undefined | null): file is FileModel => {
|
||||
return Boolean(file);
|
||||
},
|
||||
);
|
||||
|
||||
if (!files.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={props.containerClassName || DEFAULT_CONTAINER_CLASSNAME}>
|
||||
{files.map((file: FileModel, index: number) => {
|
||||
const fileId: string | null =
|
||||
file.id?.toString?.() || (file as any)._id?.toString?.() || null;
|
||||
|
||||
if (!fileId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileUrl: URL = URL.fromString(FILE_URL.toString()).addRoute(
|
||||
`/image/${fileId}`,
|
||||
);
|
||||
|
||||
const label: string = props.getFileName
|
||||
? props.getFileName(file, index)
|
||||
: file.name || `Attachment ${index + 1}`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={`${fileId}-${index}`}
|
||||
to={fileUrl}
|
||||
openInNewTab={props.openInNewTab !== false}
|
||||
className={props.linkClassName || DEFAULT_LINK_CLASSNAME}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
@@ -58,23 +58,22 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
||||
}, [props.initialValue]);
|
||||
|
||||
const setInitialValue: VoidFunction = () => {
|
||||
if (
|
||||
Array.isArray(props.initialValue) &&
|
||||
props.initialValue &&
|
||||
props.initialValue.length > 0
|
||||
) {
|
||||
if (Array.isArray(props.initialValue)) {
|
||||
setFilesModel(props.initialValue);
|
||||
} else if (props.initialValue instanceof FileModel) {
|
||||
setFilesModel([props.initialValue as FileModel]);
|
||||
} else {
|
||||
setFilesModel([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value && props.value.length > 0) {
|
||||
setFilesModel(props.value && props.value.length > 0 ? props.value : []);
|
||||
} else {
|
||||
setInitialValue();
|
||||
if (props.value) {
|
||||
setFilesModel(props.value);
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialValue();
|
||||
}, [props.value]);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
@@ -112,10 +111,22 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
||||
filesResult.push(result.data as FileModel);
|
||||
}
|
||||
|
||||
setFilesModel(filesResult);
|
||||
const updatedFiles: Array<FileModel> = props.isMultiFilePicker
|
||||
? [...filesModel, ...filesResult]
|
||||
: filesResult;
|
||||
|
||||
const uniqueFiles: Array<FileModel> = Array.from(
|
||||
new Map(
|
||||
updatedFiles.map((file: FileModel) => {
|
||||
return [file._id?.toString(), file];
|
||||
}),
|
||||
).values(),
|
||||
);
|
||||
|
||||
setFilesModel(uniqueFiles);
|
||||
|
||||
props.onBlur?.();
|
||||
props.onChange?.(filesResult);
|
||||
props.onChange?.(uniqueFiles);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
@@ -183,84 +194,79 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
||||
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"
|
||||
>
|
||||
{props.isMultiFilePicker ||
|
||||
(filesModel.length === 0 && (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: "space-y-1 text-center",
|
||||
})}
|
||||
{(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"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<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 && (
|
||||
<span>
|
||||
<span>{error}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<p className="text-xs text-gray-500">
|
||||
{props.mimeTypes && props.mimeTypes?.length > 0 && (
|
||||
<span>File types: </span>
|
||||
{error && (
|
||||
<span>
|
||||
<span>{error}</span>
|
||||
</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>
|
||||
|
||||
{props.placeholder && !error && (
|
||||
<span>{props.placeholder}</span>
|
||||
)}
|
||||
10 MB or less.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
))}
|
||||
<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>
|
||||
{props.error && (
|
||||
|
||||
@@ -570,33 +570,51 @@ const FormField: <T extends GenericObject>(
|
||||
)}
|
||||
|
||||
{(props.field.fieldType === FormFieldSchemaType.File ||
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile) && (
|
||||
props.field.fieldType === FormFieldSchemaType.ImageFile ||
|
||||
props.field.fieldType === FormFieldSchemaType.MultipleFiles) && (
|
||||
<FilePicker
|
||||
error={props.touched && props.error ? props.error : undefined}
|
||||
tabIndex={index}
|
||||
isMultiFilePicker={
|
||||
props.field.fieldType === FormFieldSchemaType.MultipleFiles
|
||||
}
|
||||
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!;
|
||||
const fileId: string | undefined =
|
||||
(i as any)._id?.toString?.() ||
|
||||
(i as any).id?.toString?.();
|
||||
|
||||
if (fileId) {
|
||||
strippedModel._id = fileId;
|
||||
}
|
||||
|
||||
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)
|
||||
props.field.fieldType === FormFieldSchemaType.MultipleFiles
|
||||
) {
|
||||
if (fileResult.length > 0) {
|
||||
fileResult = fileResult[0] as FileModel;
|
||||
} else {
|
||||
fileResult = null;
|
||||
}
|
||||
onChange(strippedFiles);
|
||||
props.setFieldValue(props.fieldName, strippedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(fileResult);
|
||||
props.setFieldValue(props.fieldName, fileResult);
|
||||
const singleFile: FileModel | null = strippedFiles.length
|
||||
? strippedFiles[0]!
|
||||
: null;
|
||||
|
||||
onChange(singleFile);
|
||||
props.setFieldValue(props.fieldName, singleFile);
|
||||
}}
|
||||
onBlur={async () => {
|
||||
props.setFieldTouched(props.fieldName, true);
|
||||
@@ -611,7 +629,9 @@ const FormField: <T extends GenericObject>(
|
||||
props.currentValues &&
|
||||
(props.currentValues as any)[props.fieldName]
|
||||
? (props.currentValues as any)[props.fieldName]
|
||||
: []
|
||||
: props.field.fieldType === FormFieldSchemaType.MultipleFiles
|
||||
? []
|
||||
: undefined
|
||||
}
|
||||
placeholder={props.field.placeholder || ""}
|
||||
/>
|
||||
|
||||
@@ -104,7 +104,6 @@ export default interface Field<TEntity> {
|
||||
) => ReactElement | undefined; // custom element to render instead of the elements in the form.
|
||||
categoryCheckboxProps?: CategoryCheckboxProps | undefined; // props for the category checkbox component. If fieldType is CategoryCheckbox, this prop is required.
|
||||
dataTestId?: string | undefined;
|
||||
|
||||
// set this to true if you want to show this field in the form even when the form is in edit mode.
|
||||
doNotShowWhenEditing?: boolean | undefined;
|
||||
doNotShowWhenCreating?: boolean | undefined;
|
||||
|
||||
@@ -19,6 +19,7 @@ enum FormFieldSchemaType {
|
||||
Color = "Color",
|
||||
Dropdown = "Dropdown",
|
||||
File = "File",
|
||||
MultipleFiles = "MultipleFiles",
|
||||
MultiSelectDropdown = "MultiSelectDropdown",
|
||||
OptionChooserButton = "OptionChooserButton",
|
||||
Toggle = "Boolean",
|
||||
|
||||
@@ -47,7 +47,8 @@ export default class FormFieldSchemaTypeUtil {
|
||||
case FormFieldSchemaType.RadioButton:
|
||||
return FieldType.Text;
|
||||
case FormFieldSchemaType.File:
|
||||
return FieldType.File;
|
||||
case FormFieldSchemaType.MultipleFiles:
|
||||
return FieldType.File; // Treat MultipleFiles as standard file input
|
||||
case FormFieldSchemaType.MultiSelectDropdown:
|
||||
return FieldType.MultiSelectDropdown;
|
||||
case FormFieldSchemaType.Toggle:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import FileList from "Common/UI/Components/FileList/FileList";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
import MarkdownUtil from "Common/UI/Utils/Markdown";
|
||||
import UserElement from "../../../Components/User/User";
|
||||
import ProjectUser from "../../../Utils/ProjectUser";
|
||||
@@ -165,7 +167,25 @@ 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: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
description: "Upload files to attach to this note.",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
showAs={ShowAs.List}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
@@ -243,9 +263,17 @@ const AlertDelete: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3",
|
||||
colSpan: 2,
|
||||
getElement: (item: AlertInternalNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3 text-sm text-gray-800">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<FileList files={item.attachments} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import FileList from "Common/UI/Components/FileList/FileList";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
import MarkdownUtil from "Common/UI/Utils/Markdown";
|
||||
import UserElement from "../../../Components/User/User";
|
||||
import ProjectUser from "../../../Utils/ProjectUser";
|
||||
@@ -165,6 +167,18 @@ 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: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
description: "Upload files to attach to this note.",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
showAs={ShowAs.List}
|
||||
showRefreshButton={true}
|
||||
@@ -202,6 +216,12 @@ const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
||||
title: "Created At",
|
||||
},
|
||||
]}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
@@ -243,9 +263,17 @@ const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3",
|
||||
colSpan: 2,
|
||||
getElement: (item: IncidentInternalNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3 text-sm text-gray-800">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<FileList files={item.attachments} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import FileList from "Common/UI/Components/FileList/FileList";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
import MarkdownUtil from "Common/UI/Utils/Markdown";
|
||||
import UserElement from "../../../Components/User/User";
|
||||
import ProjectUser from "../../../Utils/ProjectUser";
|
||||
@@ -191,6 +193,18 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
"This note is visible on your Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
description: "Upload files to attach to this note.",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: true,
|
||||
@@ -224,6 +238,10 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
subscriberNotificationStatusMessage: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
@@ -300,9 +318,17 @@ 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",
|
||||
colSpan: 2,
|
||||
getElement: (item: IncidentPublicNote): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3 text-sm text-gray-800">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<FileList files={item.attachments} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import FileList from "Common/UI/Components/FileList/FileList";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
import MarkdownUtil from "Common/UI/Utils/Markdown";
|
||||
import UserElement from "../../../Components/User/User";
|
||||
import ProjectUser from "../../../Utils/ProjectUser";
|
||||
@@ -177,9 +179,27 @@ const ScheduledMaintenanceDelete: FunctionComponent<PageComponentProps> = (
|
||||
"Add a private note to this scheduled maintenance here",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
description: "Upload files to attach to this note.",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showAs={ShowAs.List}
|
||||
selectMoreFields={{
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
filters={[
|
||||
{
|
||||
@@ -257,9 +277,19 @@ const ScheduledMaintenanceDelete: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3",
|
||||
colSpan: 2,
|
||||
getElement: (
|
||||
item: ScheduledMaintenanceInternalNote,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3 text-sm text-gray-800">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<FileList files={item.attachments} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import FileList from "Common/UI/Components/FileList/FileList";
|
||||
import MarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
import MarkdownUtil from "Common/UI/Utils/Markdown";
|
||||
import UserElement from "../../../Components/User/User";
|
||||
import ProjectUser from "../../../Utils/ProjectUser";
|
||||
@@ -204,6 +206,18 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
"This note is visible on your Status Page",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Attachments",
|
||||
description: "Upload files to attach to this note.",
|
||||
fieldType: FormFieldSchemaType.MultipleFiles,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: true,
|
||||
@@ -237,6 +251,10 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
subscriberNotificationStatusMessage: true,
|
||||
attachments: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
@@ -312,11 +330,20 @@ const PublicNote: FunctionComponent<PageComponentProps> = (
|
||||
field: {
|
||||
note: true,
|
||||
},
|
||||
|
||||
title: "",
|
||||
type: FieldType.Markdown,
|
||||
contentClassName: "-mt-3 space-y-6 text-sm text-gray-800",
|
||||
type: FieldType.Element,
|
||||
contentClassName: "-mt-3",
|
||||
colSpan: 2,
|
||||
getElement: (
|
||||
item: ScheduledMaintenancePublicNote,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-3 text-sm text-gray-800">
|
||||
<MarkdownViewer text={item.note || ""} />
|
||||
<FileList files={item.attachments} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
||||
@@ -101,6 +101,7 @@ export const getIncidentEventItem: GetIncidentEventItemFunction = (
|
||||
type: TimelineItemType.Note,
|
||||
icon: IconProp.Chat,
|
||||
iconColor: Gray500,
|
||||
attachments: incidentPublicNote.attachments,
|
||||
});
|
||||
|
||||
// If this incident is a sumamry then don't include all the notes .
|
||||
|
||||
@@ -113,6 +113,7 @@ export const getScheduledEventEventItem: GetScheduledEventEventItemFunction = (
|
||||
type: TimelineItemType.Note,
|
||||
icon: IconProp.Chat,
|
||||
iconColor: Gray500,
|
||||
attachments: scheduledMaintenancePublicNote.attachments,
|
||||
});
|
||||
|
||||
if (isSummary) {
|
||||
|
||||
Reference in New Issue
Block a user