- Implemented the PortalSettings Web Part with a new manifest and TypeScript file. - Created SCSS styles for the Web Part, defining various UI elements and responsive design. - Added localization support for English and German languages. - Developed tests for Provider Registry and static assets validation. - Included PowerShell script for project validation.
170 lines
8.7 KiB
TypeScript
170 lines
8.7 KiB
TypeScript
import { ISettingsProvider } from '../core/ProviderContracts';
|
|
|
|
export interface IExpiryIndicatorSettings {
|
|
baseField: string;
|
|
expiryField: string;
|
|
default: {
|
|
lifeTime: { value: number; unit: 'days' | 'months' | 'years' };
|
|
columnRule: any[]; // tslint:disable-line:no-any
|
|
};
|
|
rules: any[]; // tslint:disable-line:no-any
|
|
nullText: string;
|
|
confirmExtension: boolean;
|
|
[key: string]: any; // tslint:disable-line:no-any
|
|
}
|
|
|
|
export const ExpiryIndicatorSettingsProvider: ISettingsProvider<IExpiryIndicatorSettings> = {
|
|
key: 'expiryindicator',
|
|
displayName: 'ExpiryIndicator',
|
|
description: 'Zentrale Standards, Vererbung und Bindungen für Listen und Bibliotheken.',
|
|
iconName: 'DateTime2',
|
|
schemaVersion: 2,
|
|
minimumPortalSettingsVersion: '3.0.0',
|
|
componentIds: [
|
|
'a555f4fc-d6a6-4421-8189-457449d9bbde',
|
|
'95691218-bcb1-4fad-adda-02bb12014d15',
|
|
'cd58f5d9-ffc8-4df7-a910-3054842d7abf'
|
|
],
|
|
storage: { kind: 'rootPropertyBag', propertyKey: 'PortalSettings.ExpiryIndicator.Defaults' },
|
|
sections: [
|
|
{
|
|
key: 'scope', title: 'Gültigkeitsbereich', layout: 'singleColumn', iconName: 'Globe',
|
|
elements: [
|
|
{
|
|
type: 'notice', appearance: 'info', title: 'Site-Collection-Standard',
|
|
text: 'Diese Werte gelten für alle vererbenden Bindungen im Root Web und in Unterwebs.', columnSpan: 12
|
|
},
|
|
{ type: 'bindingInventory', key: 'bindings', title: 'Registrierte Bindungen', columnSpan: 12 }
|
|
]
|
|
},
|
|
{
|
|
key: 'dates', title: 'Datumsberechnung', layout: 'twoColumns', iconName: 'Calendar',
|
|
elements: [
|
|
{
|
|
type: 'textField', key: 'baseField', path: 'baseField', label: 'Basis-Datumsfeld',
|
|
description: 'Interner Feldname, normalerweise Created.', required: true, columnSpan: 6
|
|
},
|
|
{
|
|
type: 'textField', key: 'expiryField', path: 'expiryField', label: 'Ablaufdatumsfeld',
|
|
description: 'Standardwert für neue Bindungen; vorhandene Bindungen behalten ihren Feldnamen.', required: true, columnSpan: 6
|
|
},
|
|
{
|
|
type: 'number', key: 'lifeTimeValue', path: 'default.lifeTime.value', label: 'Standardlaufzeit',
|
|
minimum: 1, maximum: 10000, columnSpan: 6
|
|
},
|
|
{
|
|
type: 'choice', key: 'lifeTimeUnit', path: 'default.lifeTime.unit', label: 'Einheit', columnSpan: 6,
|
|
options: [
|
|
{ key: 'days', text: 'Tage' }, { key: 'months', text: 'Monate' }, { key: 'years', text: 'Jahre' }
|
|
]
|
|
},
|
|
{
|
|
type: 'textField', key: 'nullText', path: 'nullText', label: 'Text ohne Ablaufdatum', columnSpan: 6
|
|
},
|
|
{
|
|
type: 'boolean', key: 'confirmExtension', path: 'confirmExtension',
|
|
label: 'Verlängerung bestätigen', description: 'Zeigt vor der Verlängerung eine Sicherheitsabfrage.', columnSpan: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'rules', title: 'Farben und fachliche Regeln', layout: 'twoColumns', iconName: 'BranchFork',
|
|
elements: [
|
|
{
|
|
type: 'json', key: 'columnRule', path: 'default.columnRule', label: 'Standard-Farbregeln',
|
|
description: 'Die erste passende Schwelle gewinnt.', columnSpan: 6
|
|
},
|
|
{
|
|
type: 'json', key: 'rules', path: 'rules', label: 'Feldabhängige Regeln',
|
|
description: 'Unterstützt Legacy-Regeln und verschachtelte AND-/OR-Bedingungen.', columnSpan: 6
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'preview', title: 'Zusammenfassung', layout: 'singleColumn',
|
|
elements: [
|
|
{ type: 'propertySummary', title: 'Aktuelle Werte', columnSpan: 12 },
|
|
{ type: 'jsonPreview', title: 'Zentraler Standard', collapsedByDefault: true, columnSpan: 12 }
|
|
]
|
|
}
|
|
],
|
|
createDefault: (): IExpiryIndicatorSettings => ({
|
|
baseField: 'Created',
|
|
expiryField: 'ExpiryDate',
|
|
default: {
|
|
lifeTime: { value: 2, unit: 'years' },
|
|
columnRule: [
|
|
{ daysUntilExpiry: 0, operator: 'lessOrEqual', backgroundColor: '#a4262c', textColor: '#ffffff', label: 'Abgelaufen' },
|
|
{ daysUntilExpiry: 30, operator: 'lessOrEqual', backgroundColor: '#ffaa44', textColor: '#000000', label: 'Läuft bald ab' },
|
|
{ daysUntilExpiry: 90, operator: 'lessOrEqual', backgroundColor: '#fff4ce', textColor: '#000000', label: 'Beobachten' }
|
|
]
|
|
},
|
|
rules: [], nullText: 'Kein Ablaufdatum', confirmExtension: true
|
|
}),
|
|
normalize: (value: any): IExpiryIndicatorSettings => { // tslint:disable-line:no-any
|
|
const defaults: IExpiryIndicatorSettings = ExpiryIndicatorSettingsProvider.createDefault();
|
|
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
|
|
const result: any = {}; // tslint:disable-line:no-any
|
|
Object.keys(source).forEach((key: string): void => { result[key] = source[key]; });
|
|
result.baseField = safeName(source.baseField) ? source.baseField : defaults.baseField;
|
|
result.expiryField = safeName(source.expiryField) ? source.expiryField : defaults.expiryField;
|
|
result.default = source.default && typeof source.default === 'object' ? source.default : defaults.default;
|
|
result.default.lifeTime = result.default.lifeTime || defaults.default.lifeTime;
|
|
result.default.columnRule = Array.isArray(result.default.columnRule) ? result.default.columnRule : defaults.default.columnRule;
|
|
result.rules = Array.isArray(source.rules) ? source.rules : [];
|
|
result.nullText = typeof source.nullText === 'string' ? source.nullText : defaults.nullText;
|
|
result.confirmExtension = source.confirmExtension !== false;
|
|
return result as IExpiryIndicatorSettings;
|
|
},
|
|
validate: (value: IExpiryIndicatorSettings): string[] => {
|
|
const errors: string[] = [];
|
|
if (!safeName(value.baseField)) { errors.push('Das Basis-Datumsfeld ist ungültig.'); }
|
|
if (!safeName(value.expiryField)) { errors.push('Das Ablaufdatumsfeld ist ungültig.'); }
|
|
if (!value.default || !value.default.lifeTime || value.default.lifeTime.value < 1 ||
|
|
['days', 'months', 'years'].indexOf(value.default.lifeTime.unit) < 0) { errors.push('Die Standardlaufzeit ist ungültig.'); }
|
|
if (!value.default || !Array.isArray(value.default.columnRule)) { errors.push('Die Standard-Farbregeln sind ungültig.'); }
|
|
if (!Array.isArray(value.rules)) { errors.push('Die fachlichen Regeln sind ungültig.'); }
|
|
if (value.default) { validateColorRules(value.default.columnRule, 'Standard-Farbregeln', errors); }
|
|
for (let i: number = 0; i < value.rules.length; i++) {
|
|
const rule: any = value.rules[i]; // tslint:disable-line:no-any
|
|
if (!rule || (!rule.condition && (!safeName(rule.columnName) || rule.columnValue === undefined))) {
|
|
errors.push('Regel ' + (i + 1) + ' benötigt eine Bedingung oder columnName/columnValue.');
|
|
}
|
|
if (rule && rule.condition) { validateCondition(rule.condition, 'Regel ' + (i + 1), 1, errors); }
|
|
if (!rule || !rule.lifeTime || Number(rule.lifeTime.value) < 1 || ['days', 'months', 'years'].indexOf(rule.lifeTime.unit) < 0) {
|
|
errors.push('Regel ' + (i + 1) + ' enthält eine ungültige Laufzeit.');
|
|
}
|
|
if (rule) { validateColorRules(rule.columnRule, 'Regel ' + (i + 1), errors); }
|
|
}
|
|
return errors;
|
|
}
|
|
};
|
|
|
|
function safeName(value: string): boolean {
|
|
return typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
}
|
|
|
|
function validateColorRules(rules: any[], path: string, errors: string[]): void { // tslint:disable-line:no-any
|
|
if (!Array.isArray(rules)) { return; }
|
|
const operators: string[] = ['lessThan', 'lessOrEqual', 'equal', 'greaterOrEqual', 'greaterThan'];
|
|
for (let i: number = 0; i < rules.length; i++) {
|
|
const rule: any = rules[i]; // tslint:disable-line:no-any
|
|
if (!rule || typeof rule.daysUntilExpiry !== 'number' || operators.indexOf(rule.operator) < 0 ||
|
|
typeof rule.backgroundColor !== 'string' || typeof rule.textColor !== 'string') {
|
|
errors.push(path + '[' + i + '] ist ungültig.');
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateCondition(condition: any, path: string, depth: number, errors: string[]): void { // tslint:disable-line:no-any
|
|
if (depth > 10) { errors.push(path + ' überschreitet die maximale Bedingungstiefe.'); return; }
|
|
if (!condition || (condition.operator !== 'and' && condition.operator !== 'or') || !Array.isArray(condition.conditions) || !condition.conditions.length) {
|
|
errors.push(path + ' enthält eine ungültige AND-/OR-Bedingungsgruppe.'); return;
|
|
}
|
|
for (let i: number = 0; i < condition.conditions.length; i++) {
|
|
const child: any = condition.conditions[i]; // tslint:disable-line:no-any
|
|
if (child && child.operator) { validateCondition(child, path + '.' + i, depth + 1, errors); }
|
|
else if (!child || !safeName(child.columnName) || child.columnValue === undefined) { errors.push(path + '.' + i + ' ist keine gültige Feldbedingung.'); }
|
|
}
|
|
}
|