Add Expiry Indicator functionality with configuration and command set

- Implemented ExpiryItemService to manage expiry date logic for list items.
- Created ExpiryModels to define types and structures for expiry configurations and rules.
- Added RestError utility for handling API response errors.
- Developed ExpiryIndicatorFieldCustomizer to display expiry information in list views.
- Introduced ExpiryIndicatorCommandSet for extending expiry dates and managing settings.
- Added settings dialog for configuring expiry settings.
- Implemented localization support for English and German languages.
- Added TypeScript configuration files for project setup.
- Included TSLint configuration for code quality enforcement.
This commit is contained in:
Torsten Brendgen
2026-07-16 21:55:30 +02:00
commit c1f1d02d9c
33 changed files with 1851 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
import { override } from '@microsoft/decorators';
import {
BaseFieldCustomizer,
IFieldCustomizerCellEventParameters
} from '@microsoft/sp-listview-extensibility';
import { evaluateExpiry, formatDate, parseSharePointDate } from '../../common/ExpiryDateCalculator';
import { ExpiryConfigService } from '../../common/ExpiryConfigService';
import { ExpiryItemService } from '../../common/ExpiryItemService';
import {
createDefaultConfig,
IExpiryConfig,
IExpiryEvaluation,
IItemDateValues,
resolveBehavior
} from '../../common/ExpiryModels';
import styles from './ExpiryIndicatorFieldCustomizer.module.scss';
import * as strings from 'ExpiryIndicatorStrings';
export interface IExpiryIndicatorFieldCustomizerProperties {
}
export default class ExpiryIndicatorFieldCustomizer
extends BaseFieldCustomizer<IExpiryIndicatorFieldCustomizerProperties> {
private _config: IExpiryConfig = createDefaultConfig();
private _itemService: ExpiryItemService;
private _itemCache: { [id: string]: Promise<IItemDateValues> } = {};
@override
public onInit(): Promise<void> {
if (!this.context.pageContext.list) {
return Promise.resolve();
}
const service: ExpiryConfigService = new ExpiryConfigService(
this.context.spHttpClient,
this.context.pageContext.web.absoluteUrl
);
this._itemService = new ExpiryItemService(
this.context.spHttpClient,
this.context.pageContext.web.absoluteUrl,
this.context.pageContext.list.id.toString()
);
return service.getConfig(this.context.pageContext.list.id.toString())
.then((config: IExpiryConfig): void => { this._config = config; })
.catch((): void => { this._config = createDefaultConfig(); });
}
@override
public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
this._clear(event.domElement);
const expiry: Date | undefined = parseSharePointDate(event.fieldValue);
const createdValue: any = event.listItem && event.listItem.getValueByName
? event.listItem.getValueByName(this._config.baseField)
: undefined;
const created: Date | undefined = parseSharePointDate(createdValue);
const idValue: any = event.listItem && event.listItem.getValueByName
? (event.listItem.getValueByName('ID') || event.listItem.getValueByName('Id'))
: undefined;
const itemId: number = Number(idValue);
if (itemId > 0 && this._itemService) {
const requestKey: string = String(itemId);
event.domElement.setAttribute('data-expiry-item-id', requestKey);
this._renderNeutral(event.domElement, strings.Loading);
if (!this._itemCache[requestKey]) {
this._itemCache[requestKey] = this._itemService.getItemDates(itemId, this._config);
}
this._itemCache[requestKey].then((item: IItemDateValues): void => {
if (event.domElement.getAttribute('data-expiry-item-id') !== requestKey) {
return;
}
this._clear(event.domElement);
this._renderValues(event.domElement, item.created, item.expiry, item.fieldValues);
}).catch((): void => {
if (event.domElement.getAttribute('data-expiry-item-id') === requestKey) {
this._clear(event.domElement);
this._renderValues(event.domElement, created, expiry, {});
}
});
return;
}
this._renderValues(event.domElement, created, expiry, {});
}
@override
public onDisposeCell(event: IFieldCustomizerCellEventParameters): void {
this._clear(event.domElement);
super.onDisposeCell(event);
}
private _renderEvaluation(container: HTMLElement, evaluation: IExpiryEvaluation): void {
const wrapper: HTMLSpanElement = document.createElement('span');
wrapper.className = styles.cell + (evaluation.wasCalculated ? ' ' + styles.calculated : '');
if (evaluation.matchedRule) {
wrapper.style.backgroundColor = evaluation.matchedRule.backgroundColor;
wrapper.style.color = evaluation.matchedRule.textColor;
}
const date: HTMLSpanElement = document.createElement('span');
date.className = styles.date;
date.textContent = formatDate(evaluation.effectiveExpiryDate);
wrapper.appendChild(date);
if (evaluation.matchedRule && evaluation.matchedRule.label) {
const label: HTMLSpanElement = document.createElement('span');
label.className = styles.label;
label.textContent = evaluation.matchedRule.label;
wrapper.appendChild(label);
}
const calculationHint: string = evaluation.wasCalculated ? ' ' + strings.CalculatedHint : '';
wrapper.title = strings.DaysUntilExpiry.replace('{0}', String(evaluation.daysUntilExpiry)) + calculationHint;
container.appendChild(wrapper);
}
private _renderValues(
container: HTMLElement,
created: Date | undefined,
expiry: Date | undefined,
fieldValues: { [fieldName: string]: any }
): void {
if (!expiry && !created) {
this._renderNeutral(container, this._config.nullText);
return;
}
const evaluation: IExpiryEvaluation = evaluateExpiry(
created || expiry as Date,
expiry,
this._config,
undefined,
resolveBehavior(this._config, fieldValues)
);
this._renderEvaluation(container, evaluation);
}
private _renderNeutral(container: HTMLElement, text: string): void {
const wrapper: HTMLSpanElement = document.createElement('span');
wrapper.className = styles.cell + ' ' + styles.neutral;
wrapper.textContent = text;
container.appendChild(wrapper);
}
private _clear(container: HTMLElement): void {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
}
}