Skip to content

Commit 83e7370

Browse files
committed
#1556: Dynamic form - show comment history for Append Only Multiline field
1 parent a22a73f commit 83e7370

7 files changed

Lines changed: 132 additions & 8 deletions

File tree

src/controls/dynamicForm/DynamicForm.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { IInstalledLanguageInfo, IItemUpdateResult, IList, ITermInfo, ChoiceFiel
3737
import { cloneDeep, isEqual } from "lodash";
3838
import { ICustomFormatting, ICustomFormattingBodySection, ICustomFormattingNode } from "../../common/utilities/ICustomFormatting";
3939
import SPservice from "../../services/SPService";
40-
import { IRenderListDataAsStreamClientFormResult } from "../../services/ISPService";
40+
import { IAppendOnlyNoteHistoryEntry, IClientFormTextFieldInfo, IRenderExtendedListFormDataResultNotesField, IRenderExtendedListFormDataResultStatic, IRenderListDataAsStreamClientFormResult } from "../../services/ISPService";
4141
import { ISPField, IUploadImageResult } from "../../common/SPEntities";
4242
import { FormulaEvaluation } from "../../common/utilities/FormulaEvaluation";
4343
import { Context } from "../../common/utilities/FormulaEvaluation.types";
@@ -695,6 +695,21 @@ export class DynamicFormBase extends React.Component<
695695
}
696696
}
697697

698+
// Reload append-only history after save
699+
if (listItemId && this.state.fieldCollection.some(f => f.isAppendOnly)) {
700+
const updatedExtendedInfo = await this._spService.getExtendedListFormData(listId, listItemId, this.webURL);
701+
this.setState(prevState => ({
702+
fieldCollection: prevState.fieldCollection.map(field =>
703+
field.isAppendOnly
704+
? { ...field,
705+
notesAppendOnlyHistory: updatedExtendedInfo[field.columnInternalName],
706+
newValue: '',
707+
value: '' }
708+
: field
709+
)
710+
}));
711+
}
712+
698713
this.setState({
699714
isSaving: false,
700715
etag: newETag,
@@ -1059,6 +1074,7 @@ export class DynamicFormBase extends React.Component<
10591074
let item = null;
10601075
const isEditingItem = listItemId !== undefined && listItemId !== null && listItemId !== 0;
10611076
let etag: string | undefined = undefined;
1077+
let extendedInfo: (IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField) | undefined = undefined;
10621078

10631079
if (isEditingItem) {
10641080
const spListItem = spList.items.getById(listItemId);
@@ -1076,6 +1092,13 @@ export class DynamicFormBase extends React.Component<
10761092
if (respectETag !== false) {
10771093
etag = item["odata.etag"];
10781094
}
1095+
1096+
const appendOnlyFields = listInfo.ClientForms.Edit[contentTypeName]
1097+
.filter(field => field.FieldType === 'Note' && (field as IClientFormTextFieldInfo).AppendOnly);
1098+
1099+
if (appendOnlyFields.length > 0) {
1100+
extendedInfo = await this._spService.getExtendedListFormData(listId, listItemId, this.webURL);
1101+
}
10791102
}
10801103

10811104
// Build the field collection
@@ -1087,7 +1110,8 @@ export class DynamicFormBase extends React.Component<
10871110
listId,
10881111
listItemId,
10891112
disabledFields,
1090-
customIcons
1113+
customIcons,
1114+
extendedInfo
10911115
);
10921116

10931117
const sortedFields = this.props.fieldOrder?.length > 0
@@ -1133,7 +1157,7 @@ export class DynamicFormBase extends React.Component<
11331157
* @returns
11341158
*/
11351159
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1136-
private async buildFieldCollection(listInfo: IRenderListDataAsStreamClientFormResult, contentTypeName: string, item: any, numberFields: ISPField[], listId: string, listItemId: number, disabledFields: string[], customIcons: { [key: string]: string }): Promise<IDynamicFieldProps[]> {
1160+
private async buildFieldCollection(listInfo: IRenderListDataAsStreamClientFormResult, contentTypeName: string, item: any, numberFields: ISPField[], listId: string, listItemId: number, disabledFields: string[], customIcons: { [key: string]: string }, extendedInfo: (IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField) | undefined): Promise<IDynamicFieldProps[]> {
11371161
const { useModernTaxonomyPicker } = this.props;
11381162
const tempFields: IDynamicFieldProps[] = [];
11391163
let order: number = 0;
@@ -1158,6 +1182,7 @@ export class DynamicFormBase extends React.Component<
11581182
let stringValue = null;
11591183
const subPropertyValues: Record<string, unknown> = {};
11601184
let richText = false;
1185+
let appendOnly = false;
11611186
let dateFormat: DateFormat | undefined;
11621187
let principalType = "";
11631188
let cultureName: string;
@@ -1167,7 +1192,7 @@ export class DynamicFormBase extends React.Component<
11671192
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11681193
const selectedTags: any = [];
11691194
let choiceType: ChoiceFieldFormatType | undefined;
1170-
1195+
let notesAppendOnlyHistory: IAppendOnlyNoteHistoryEntry[] | undefined;
11711196
let fieldName = field.InternalName;
11721197
if (fieldName.startsWith('_x') || fieldName.startsWith('_')) {
11731198
fieldName = `OData_${fieldName}`;
@@ -1205,6 +1230,11 @@ export class DynamicFormBase extends React.Component<
12051230
// Setup Note, Number and Currency fields
12061231
if (field.FieldType === "Note") {
12071232
richText = field.RichText;
1233+
appendOnly = field.AppendOnly;
1234+
if (field.AppendOnly) {
1235+
notesAppendOnlyHistory = extendedInfo?.[field.InternalName];
1236+
value = '';
1237+
}
12081238
}
12091239
if (field.FieldType === "Number" || field.FieldType === "Currency") {
12101240
const numberField = numberFields.find(f => f.InternalName === field.InternalName);
@@ -1486,6 +1516,7 @@ export class DynamicFormBase extends React.Component<
14861516
hiddenFieldName: hiddenName,
14871517
Order: order,
14881518
isRichText: richText,
1519+
isAppendOnly: appendOnly,
14891520
dateFormat: dateFormat,
14901521
firstDayOfWeek: defaultDayOfWeek,
14911522
listItemId: listItemId,
@@ -1496,7 +1527,8 @@ export class DynamicFormBase extends React.Component<
14961527
showAsPercentage: showAsPercentage,
14971528
customIcon: customIcons ? customIcons[field.InternalName] : undefined,
14981529
useModernTaxonomyPickerControl: useModernTaxonomyPicker,
1499-
choiceType: choiceType
1530+
choiceType: choiceType,
1531+
notesAppendOnlyHistory: notesAppendOnlyHistory
15001532
});
15011533

15021534
// This may not be necessary now using RenderListDataAsStream

src/controls/dynamicForm/dynamicField/DynamicField.styles.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const getFieldStyles = (
2525
thumbnailFieldButtons: 'thumbnailFieldButtons',
2626
selectedFileContainer: 'selectedFileContainer',
2727
fieldRequired: 'fieldRequired',
28+
appendOnlyHistoryContainer: 'appendOnlyHistoryContainer',
29+
appendOnlyHistoryEntry: 'appendOnlyHistoryEntry',
30+
appendOnlyHistoryAuthor: 'appendOnlyHistoryAuthor',
31+
appendOnlyHistoryDate: 'appendOnlyHistoryDate',
2832
};
2933

3034
const fieldDisplayNoPadding_style: IStyle = {
@@ -131,6 +135,19 @@ export const getFieldStyles = (
131135
globalClassNames.thumbnailFieldButtons,
132136
{ display: 'flex' },
133137
],
138+
appendOnlyHistoryContainer: [globalClassNames.appendOnlyHistoryContainer, { selectors: { p: { margin: 0 } } }],
139+
appendOnlyHistoryEntry: [
140+
globalClassNames.appendOnlyHistoryEntry,
141+
{ display: 'flex', gap: 4, fontSize: 12, padding: '4px 0' },
142+
],
143+
appendOnlyHistoryAuthor: [
144+
globalClassNames.appendOnlyHistoryAuthor,
145+
{ fontWeight: 600 },
146+
],
147+
appendOnlyHistoryDate: [
148+
globalClassNames.appendOnlyHistoryDate,
149+
{ color: palette.themePrimary, cursor: 'pointer' },
150+
],
134151
errormessage: [
135152
globalClassNames.errormessage,
136153
{

src/controls/dynamicForm/dynamicField/DynamicField.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
8383
label,
8484
placeholder,
8585
isRichText,
86+
isAppendOnly,
8687
//bingAPIKey,
8788
dateFormat,
8889
firstDayOfWeek,
@@ -95,6 +96,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
9596
customIcon,
9697
orderBy,
9798
choiceType,
99+
notesAppendOnlyHistory,
98100
useModernTaxonomyPickerControl
99101
} = this.props;
100102

@@ -151,7 +153,17 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
151153
{descriptionEl}
152154
</div>;
153155

154-
case 'Note':
156+
case 'Note': {
157+
const notesHistory: JSX.Element = isAppendOnly && notesAppendOnlyHistory?.length > 0
158+
? <div className={styles.appendOnlyHistoryContainer}>{notesAppendOnlyHistory.map((comment) => (
159+
<div key={comment.versionId} className={styles.appendOnlyHistoryEntry}>
160+
<span className={styles.appendOnlyHistoryAuthor}>{comment.createdTitle}</span>
161+
<span className={styles.appendOnlyHistoryDate}>({comment.createdTime})</span>
162+
<span>: </span>
163+
<span dangerouslySetInnerHTML={{ __html: comment.value }} />
164+
</div>
165+
))}</div>
166+
: null;
155167
if (isRichText) {
156168
const noteValue = valueToDisplay !== undefined ? valueToDisplay : defaultValue;
157169
return <div className={styles.richText}>
@@ -165,6 +177,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
165177
className={styles.fieldDisplay}
166178
onChange={(newText) => { this.onChange(newText); return newText; }}
167179
isEditMode={!disabled} />
180+
{notesHistory}
168181
{descriptionEl}
169182
{errorTextEl}
170183
</div>;
@@ -186,9 +199,11 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
186199
onBlur={this.onBlur}
187200
errorMessage={errorText}
188201
/>
202+
{notesHistory}
189203
{descriptionEl}
190204
</div>;
191205
}
206+
}
192207

193208
case 'Choice': {
194209
let choiceControl: JSX.Element = undefined;

src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IDropdownOption } from "@fluentui/react/lib/Dropdown";
33
import { IStyle, IStyleFunctionOrObject, Theme } from '@fluentui/react';
44
import { IFilePickerResult } from '../../filePicker';
55
import { ChoiceFieldFormatType } from '@pnp/sp/fields';
6+
import { IAppendOnlyNoteHistoryEntry } from '../../../services/ISPService';
67

78
export type DateFormat = 'DateTime' | 'DateOnly';
89
export type FieldChangeAdditionalData = IFilePickerResult;
@@ -87,6 +88,7 @@ export interface IDynamicFieldProps {
8788
// Related to various field types
8889
options?: IDropdownOption[];
8990
isRichText?: boolean;
91+
isAppendOnly?: boolean;
9092
dateFormat?: DateFormat;
9193
firstDayOfWeek: number;
9294
principalType?: string;
@@ -98,6 +100,7 @@ export interface IDynamicFieldProps {
98100
customIcon?: string;
99101
orderBy?: string;
100102
choiceType?: ChoiceFieldFormatType;
103+
notesAppendOnlyHistory?: IAppendOnlyNoteHistoryEntry[];
101104
/** Used for customize component styling */
102105
styles?:IStyleFunctionOrObject<IDynamicFieldStyleProps, IDynamicFieldStyles>;
103106
}
@@ -124,4 +127,8 @@ export interface IDynamicFieldStyles {
124127
thumbnailFieldButtons:IStyle;
125128
selectedFileContainer:IStyle;
126129
fieldRequired:IStyle;
130+
appendOnlyHistoryContainer:IStyle;
131+
appendOnlyHistoryEntry:IStyle;
132+
appendOnlyHistoryAuthor:IStyle;
133+
appendOnlyHistoryDate:IStyle;
127134
}

src/services/ISPService.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,24 @@ export interface IRenderListDataAsStreamClientFormResult {
240240
FormRenderModes: IClientFormRenderModeByContentType;
241241
}
242242

243+
export interface IRenderExtendedListFormDataResultStatic {
244+
ListData: Record<string, unknown>;
245+
ListSchema: { New: IClientFormInfoByContentType; Edit: IClientFormInfoByContentType };
246+
}
247+
248+
export interface IRenderExtendedListFormDataResultNotesField {
249+
[fieldName: string]: IAppendOnlyNoteHistoryEntry[];
250+
}
251+
252+
export interface IAppendOnlyNoteHistoryEntry {
253+
value: string;
254+
versionId: number;
255+
createdEmail: string;
256+
createdTitle: string;
257+
createdId: number;
258+
createdTime: string;
259+
}
260+
243261
export interface ISPService {
244262
/**
245263
* Get the lists from SharePoint
@@ -271,6 +289,15 @@ export interface ISPService {
271289
*/
272290
getAdditionalListFormFieldInfo(listId: string, webUrl?: string): Promise<ISPField[]>;
273291

292+
/**
293+
* Retrieves extended list form data for a list item, including append-only note history.
294+
* Calls RenderExtendedListFormData with options=30 to include version history.
295+
* @param listId - The GUID of the SharePoint list
296+
* @param itemId - The ID of the list item
297+
* @param webUrl - Optional web URL; defaults to the current web
298+
*/
299+
getExtendedListFormData(listId: string, itemId: number, webUrl?: string): Promise<IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField>;
300+
274301
/**
275302
* Get the views from lists or libraries
276303
* @params listId, orderBy, onViewsRetrived

src/services/SPService.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import filter from 'lodash/filter';
44
import find from 'lodash/find';
55
import { ISPContentType, ISPField, ISPList, ISPLists, IUploadImageResult, ISPViews } from "../common/SPEntities";
66
import { SPHelper, urlCombine } from "../common/utilities";
7-
import { IContentTypesOptions, IFieldsOptions, ILibsOptions, IRenderListDataAsStreamClientFormResult, ISPService, LibsOrderBy } from "./ISPService";
7+
import { IContentTypesOptions, IFieldsOptions, ILibsOptions, IRenderExtendedListFormDataResultStatic, IRenderExtendedListFormDataResultNotesField, IRenderListDataAsStreamClientFormResult, ISPService, LibsOrderBy } from "./ISPService";
88
import {orderBy } from '../controls/viewPicker/IViewPicker';
99

1010
interface ICachedListItems {
@@ -809,6 +809,29 @@ export default class SPService implements ISPService {
809809
}
810810
}
811811

812+
/**
813+
* Retrieves extended list form data for a list item, including append-only note history.
814+
* Calls RenderExtendedListFormData with options=30 to include version history.
815+
* @param listId - The GUID of the SharePoint list
816+
* @param itemId - The ID of the list item
817+
* @param webUrl - Optional web URL; defaults to the current web
818+
*/
819+
async getExtendedListFormData(listId: string, itemId: number, webUrl?: string): Promise<IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField> {
820+
try {
821+
const webAbsoluteUrl = !webUrl ? this._context.pageContext.web.absoluteUrl : webUrl;
822+
const apiRequestPath = `/_api/web/lists(guid'${listId}')/RenderExtendedListFormData(itemId=${itemId},formId='editform',mode='2',options=30,cutoffVersion=0)`;
823+
824+
const apiUrl = urlCombine(webAbsoluteUrl, apiRequestPath, false);
825+
const response = await this._context.spHttpClient.post(apiUrl, SPHttpClient.configurations.v1, {});
826+
const { value } = await response.json();
827+
const result = JSON.parse(value) as IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField
828+
return result;
829+
} catch (error) {
830+
console.dir(error);
831+
return Promise.reject(error);
832+
}
833+
}
834+
812835
private _filterListItemsFieldValuesAsText(items: any[], internalColumnName: string, filterText: string | undefined, substringSearch: boolean): any[] { // eslint-disable-line @typescript-eslint/no-explicit-any
813836
const lowercasedFilterText = filterText.toLowerCase();
814837

src/services/SPServiceMock.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ISPService, ILibsOptions, IFieldsOptions, IContentTypesOptions, IRenderListDataAsStreamClientFormResult } from "./ISPService";
1+
import { ISPService, ILibsOptions, IFieldsOptions, IContentTypesOptions, IRenderListDataAsStreamClientFormResult, IRenderExtendedListFormDataResultNotesField, IRenderExtendedListFormDataResultStatic } from "./ISPService";
22
import { ISPContentType, ISPField, ISPLists, ISPViews } from "../common/SPEntities";
33
import {orderBy } from '../controls/viewPicker/IViewPicker';
44

@@ -16,6 +16,9 @@ export default class SPServiceMock implements ISPService {
1616
public getAdditionalListFormFieldInfo(listId: string, webUrl?: string): Promise<ISPField[]> {
1717
throw new Error("Method not implemented.");
1818
}
19+
public getExtendedListFormData(listId: string, itemId: number, webUrl?: string): Promise<IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField> {
20+
throw new Error("Method not implemented.");
21+
}
1922
public getFields(options?: IFieldsOptions): Promise<ISPField[]> {
2023
throw new Error("Method not implemented.");
2124
}

0 commit comments

Comments
 (0)