Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions src/controls/dynamicForm/DynamicForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { IInstalledLanguageInfo, IItemUpdateResult, IList, ITermInfo, ChoiceFiel
import { cloneDeep, isEqual } from "lodash";
import { ICustomFormatting, ICustomFormattingBodySection, ICustomFormattingNode } from "../../common/utilities/ICustomFormatting";
import SPservice from "../../services/SPService";
import { IRenderListDataAsStreamClientFormResult } from "../../services/ISPService";
import { IAppendOnlyNoteHistoryEntry, IClientFormTextFieldInfo, IRenderExtendedListFormDataResultNotesField, IRenderExtendedListFormDataResultStatic, IRenderListDataAsStreamClientFormResult } from "../../services/ISPService";
import { ISPField, IUploadImageResult } from "../../common/SPEntities";
import { FormulaEvaluation } from "../../common/utilities/FormulaEvaluation";
import { Context } from "../../common/utilities/FormulaEvaluation.types";
Expand Down Expand Up @@ -695,6 +695,21 @@ export class DynamicFormBase extends React.Component<
}
}

// Reload append-only history after save
if (listItemId && this.state.fieldCollection.some(f => f.isAppendOnly)) {
const updatedExtendedInfo = await this._spService.getExtendedListFormData(listId, listItemId, this.webURL);
this.setState(prevState => ({
fieldCollection: prevState.fieldCollection.map(field =>
field.isAppendOnly
? { ...field,
notesAppendOnlyHistory: updatedExtendedInfo[field.columnInternalName],
newValue: '',
value: '' }
: field
)
}));
}

this.setState({
isSaving: false,
etag: newETag,
Expand Down Expand Up @@ -1059,6 +1074,7 @@ export class DynamicFormBase extends React.Component<
let item = null;
const isEditingItem = listItemId !== undefined && listItemId !== null && listItemId !== 0;
let etag: string | undefined = undefined;
let extendedInfo: (IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField) | undefined = undefined;

if (isEditingItem) {
const spListItem = spList.items.getById(listItemId);
Expand All @@ -1076,6 +1092,13 @@ export class DynamicFormBase extends React.Component<
if (respectETag !== false) {
etag = item["odata.etag"];
}

const appendOnlyFields = listInfo.ClientForms.Edit[contentTypeName]
.filter(field => field.FieldType === 'Note' && (field as IClientFormTextFieldInfo).AppendOnly);

if (appendOnlyFields.length > 0) {
extendedInfo = await this._spService.getExtendedListFormData(listId, listItemId, this.webURL);
}
}

// Build the field collection
Expand All @@ -1087,7 +1110,8 @@ export class DynamicFormBase extends React.Component<
listId,
listItemId,
disabledFields,
customIcons
customIcons,
extendedInfo
);

const sortedFields = this.props.fieldOrder?.length > 0
Expand Down Expand Up @@ -1133,7 +1157,7 @@ export class DynamicFormBase extends React.Component<
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async buildFieldCollection(listInfo: IRenderListDataAsStreamClientFormResult, contentTypeName: string, item: any, numberFields: ISPField[], listId: string, listItemId: number, disabledFields: string[], customIcons: { [key: string]: string }): Promise<IDynamicFieldProps[]> {
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[]> {
const { useModernTaxonomyPicker } = this.props;
const tempFields: IDynamicFieldProps[] = [];
let order: number = 0;
Expand All @@ -1158,6 +1182,7 @@ export class DynamicFormBase extends React.Component<
let stringValue = null;
const subPropertyValues: Record<string, unknown> = {};
let richText = false;
let appendOnly = false;
let dateFormat: DateFormat | undefined;
let principalType = "";
let cultureName: string;
Expand All @@ -1167,7 +1192,7 @@ export class DynamicFormBase extends React.Component<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const selectedTags: any = [];
let choiceType: ChoiceFieldFormatType | undefined;

let notesAppendOnlyHistory: IAppendOnlyNoteHistoryEntry[] | undefined;
let fieldName = field.InternalName;
if (fieldName.startsWith('_x') || fieldName.startsWith('_')) {
fieldName = `OData_${fieldName}`;
Expand Down Expand Up @@ -1205,6 +1230,11 @@ export class DynamicFormBase extends React.Component<
// Setup Note, Number and Currency fields
if (field.FieldType === "Note") {
richText = field.RichText;
appendOnly = field.AppendOnly;
if (field.AppendOnly) {
notesAppendOnlyHistory = extendedInfo?.[field.InternalName];
value = '';
}
}
if (field.FieldType === "Number" || field.FieldType === "Currency") {
const numberField = numberFields.find(f => f.InternalName === field.InternalName);
Expand Down Expand Up @@ -1486,6 +1516,7 @@ export class DynamicFormBase extends React.Component<
hiddenFieldName: hiddenName,
Order: order,
isRichText: richText,
isAppendOnly: appendOnly,
dateFormat: dateFormat,
firstDayOfWeek: defaultDayOfWeek,
listItemId: listItemId,
Expand All @@ -1496,7 +1527,8 @@ export class DynamicFormBase extends React.Component<
showAsPercentage: showAsPercentage,
customIcon: customIcons ? customIcons[field.InternalName] : undefined,
useModernTaxonomyPickerControl: useModernTaxonomyPicker,
choiceType: choiceType
choiceType: choiceType,
notesAppendOnlyHistory: notesAppendOnlyHistory
});

// This may not be necessary now using RenderListDataAsStream
Expand Down
17 changes: 17 additions & 0 deletions src/controls/dynamicForm/dynamicField/DynamicField.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const getFieldStyles = (
thumbnailFieldButtons: 'thumbnailFieldButtons',
selectedFileContainer: 'selectedFileContainer',
fieldRequired: 'fieldRequired',
appendOnlyHistoryContainer: 'appendOnlyHistoryContainer',
appendOnlyHistoryEntry: 'appendOnlyHistoryEntry',
appendOnlyHistoryAuthor: 'appendOnlyHistoryAuthor',
appendOnlyHistoryDate: 'appendOnlyHistoryDate',
};

const fieldDisplayNoPadding_style: IStyle = {
Expand Down Expand Up @@ -131,6 +135,19 @@ export const getFieldStyles = (
globalClassNames.thumbnailFieldButtons,
{ display: 'flex' },
],
appendOnlyHistoryContainer: [globalClassNames.appendOnlyHistoryContainer, { selectors: { p: { margin: 0 } } }],
appendOnlyHistoryEntry: [
globalClassNames.appendOnlyHistoryEntry,
{ display: 'flex', gap: 4, fontSize: 12, padding: '4px 0' },
],
appendOnlyHistoryAuthor: [
globalClassNames.appendOnlyHistoryAuthor,
{ fontWeight: 600 },
],
appendOnlyHistoryDate: [
globalClassNames.appendOnlyHistoryDate,
{ color: palette.themePrimary, cursor: 'pointer' },
],
errormessage: [
globalClassNames.errormessage,
{
Expand Down
17 changes: 16 additions & 1 deletion src/controls/dynamicForm/dynamicField/DynamicField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
label,
placeholder,
isRichText,
isAppendOnly,
//bingAPIKey,
dateFormat,
firstDayOfWeek,
Expand All @@ -95,6 +96,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
customIcon,
orderBy,
choiceType,
notesAppendOnlyHistory,
useModernTaxonomyPickerControl
} = this.props;

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

case 'Note':
case 'Note': {
const notesHistory: JSX.Element = isAppendOnly && notesAppendOnlyHistory?.length > 0
? <div className={styles.appendOnlyHistoryContainer}>{notesAppendOnlyHistory.map((comment) => (
<div key={comment.versionId} className={styles.appendOnlyHistoryEntry}>
<span className={styles.appendOnlyHistoryAuthor}>{comment.createdTitle}</span>
<span className={styles.appendOnlyHistoryDate}>({comment.createdTime})</span>
<span>: </span>
<span dangerouslySetInnerHTML={{ __html: comment.value }} />
</div>
))}</div>
: null;
if (isRichText) {
const noteValue = valueToDisplay !== undefined ? valueToDisplay : defaultValue;
return <div className={styles.richText}>
Expand All @@ -165,6 +177,7 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
className={styles.fieldDisplay}
onChange={(newText) => { this.onChange(newText); return newText; }}
isEditMode={!disabled} />
{notesHistory}
{descriptionEl}
{errorTextEl}
</div>;
Expand All @@ -186,9 +199,11 @@ export class DynamicFieldBase extends React.Component<IDynamicFieldProps, IDynam
onBlur={this.onBlur}
errorMessage={errorText}
/>
{notesHistory}
{descriptionEl}
</div>;
}
}

case 'Choice': {
let choiceControl: JSX.Element = undefined;
Expand Down
7 changes: 7 additions & 0 deletions src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IDropdownOption } from "@fluentui/react/lib/Dropdown";
import { IStyle, IStyleFunctionOrObject, Theme } from '@fluentui/react';
import { IFilePickerResult } from '../../filePicker';
import { ChoiceFieldFormatType } from '@pnp/sp/fields';
import { IAppendOnlyNoteHistoryEntry } from '../../../services/ISPService';

export type DateFormat = 'DateTime' | 'DateOnly';
export type FieldChangeAdditionalData = IFilePickerResult;
Expand Down Expand Up @@ -87,6 +88,7 @@ export interface IDynamicFieldProps {
// Related to various field types
options?: IDropdownOption[];
isRichText?: boolean;
isAppendOnly?: boolean;
dateFormat?: DateFormat;
firstDayOfWeek: number;
principalType?: string;
Expand All @@ -98,6 +100,7 @@ export interface IDynamicFieldProps {
customIcon?: string;
orderBy?: string;
choiceType?: ChoiceFieldFormatType;
notesAppendOnlyHistory?: IAppendOnlyNoteHistoryEntry[];
/** Used for customize component styling */
styles?:IStyleFunctionOrObject<IDynamicFieldStyleProps, IDynamicFieldStyles>;
}
Expand All @@ -124,4 +127,8 @@ export interface IDynamicFieldStyles {
thumbnailFieldButtons:IStyle;
selectedFileContainer:IStyle;
fieldRequired:IStyle;
appendOnlyHistoryContainer:IStyle;
appendOnlyHistoryEntry:IStyle;
appendOnlyHistoryAuthor:IStyle;
appendOnlyHistoryDate:IStyle;
}
27 changes: 27 additions & 0 deletions src/services/ISPService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,24 @@ export interface IRenderListDataAsStreamClientFormResult {
FormRenderModes: IClientFormRenderModeByContentType;
}

export interface IRenderExtendedListFormDataResultStatic {
ListData: Record<string, unknown>;
ListSchema: { New: IClientFormInfoByContentType; Edit: IClientFormInfoByContentType };
}

export interface IRenderExtendedListFormDataResultNotesField {
[fieldName: string]: IAppendOnlyNoteHistoryEntry[];
}

export interface IAppendOnlyNoteHistoryEntry {
value: string;
versionId: number;
createdEmail: string;
createdTitle: string;
createdId: number;
createdTime: string;
}

export interface ISPService {
/**
* Get the lists from SharePoint
Expand Down Expand Up @@ -271,6 +289,15 @@ export interface ISPService {
*/
getAdditionalListFormFieldInfo(listId: string, webUrl?: string): Promise<ISPField[]>;

/**
* Retrieves extended list form data for a list item, including append-only note history.
* Calls RenderExtendedListFormData with options=30 to include version history.
* @param listId - The GUID of the SharePoint list
* @param itemId - The ID of the list item
* @param webUrl - Optional web URL; defaults to the current web
*/
getExtendedListFormData(listId: string, itemId: number, webUrl?: string): Promise<IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField>;

/**
* Get the views from lists or libraries
* @params listId, orderBy, onViewsRetrived
Expand Down
25 changes: 24 additions & 1 deletion src/services/SPService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import filter from 'lodash/filter';
import find from 'lodash/find';
import { ISPContentType, ISPField, ISPList, ISPLists, IUploadImageResult, ISPViews } from "../common/SPEntities";
import { SPHelper, urlCombine } from "../common/utilities";
import { IContentTypesOptions, IFieldsOptions, ILibsOptions, IRenderListDataAsStreamClientFormResult, ISPService, LibsOrderBy } from "./ISPService";
import { IContentTypesOptions, IFieldsOptions, ILibsOptions, IRenderExtendedListFormDataResultStatic, IRenderExtendedListFormDataResultNotesField, IRenderListDataAsStreamClientFormResult, ISPService, LibsOrderBy } from "./ISPService";
import {orderBy } from '../controls/viewPicker/IViewPicker';

interface ICachedListItems {
Expand Down Expand Up @@ -809,6 +809,29 @@ export default class SPService implements ISPService {
}
}

/**
* Retrieves extended list form data for a list item, including append-only note history.
* Calls RenderExtendedListFormData with options=30 to include version history.
* @param listId - The GUID of the SharePoint list
* @param itemId - The ID of the list item
* @param webUrl - Optional web URL; defaults to the current web
*/
async getExtendedListFormData(listId: string, itemId: number, webUrl?: string): Promise<IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField> {
try {
const webAbsoluteUrl = !webUrl ? this._context.pageContext.web.absoluteUrl : webUrl;
const apiRequestPath = `/_api/web/lists(guid'${listId}')/RenderExtendedListFormData(itemId=${itemId},formId='editform',mode='2',options=30,cutoffVersion=0)`;

const apiUrl = urlCombine(webAbsoluteUrl, apiRequestPath, false);
const response = await this._context.spHttpClient.post(apiUrl, SPHttpClient.configurations.v1, {});
const { value } = await response.json();
const result = JSON.parse(value) as IRenderExtendedListFormDataResultStatic & IRenderExtendedListFormDataResultNotesField
return result;
} catch (error) {
console.dir(error);
return Promise.reject(error);
}
}

private _filterListItemsFieldValuesAsText(items: any[], internalColumnName: string, filterText: string | undefined, substringSearch: boolean): any[] { // eslint-disable-line @typescript-eslint/no-explicit-any
const lowercasedFilterText = filterText.toLowerCase();

Expand Down
5 changes: 4 additions & 1 deletion src/services/SPServiceMock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ISPService, ILibsOptions, IFieldsOptions, IContentTypesOptions, IRenderListDataAsStreamClientFormResult } from "./ISPService";
import { ISPService, ILibsOptions, IFieldsOptions, IContentTypesOptions, IRenderListDataAsStreamClientFormResult, IRenderExtendedListFormDataResultNotesField, IRenderExtendedListFormDataResultStatic } from "./ISPService";
import { ISPContentType, ISPField, ISPLists, ISPViews } from "../common/SPEntities";
import {orderBy } from '../controls/viewPicker/IViewPicker';

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