feat: add ExpiryIndicatorClassic functionality with configuration and rendering
- Implemented ExpiryIndicatorClassic.js for managing expiry dates in SharePoint lists. - Created classic-elements.xml to define the module for ExpiryIndicator assets. - Added upgrade-actions-v2.xml to apply the new element manifests. - Introduced ExpiryModels.test.ts for unit testing the expiry configuration and rules validation.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export type DurationUnit = 'days' | 'months' | 'years';
|
||||
export type ColumnRuleOperator = 'lessThan' | 'lessOrEqual' | 'equal' | 'greaterOrEqual' | 'greaterThan';
|
||||
export type ExpiryLogicalOperator = 'and' | 'or';
|
||||
|
||||
export interface IExpiryDuration {
|
||||
value: number;
|
||||
@@ -19,11 +20,25 @@ export interface IExpiryBehavior {
|
||||
columnRule: IExpiryColumnRule[];
|
||||
}
|
||||
|
||||
export interface IExpiryValueRule extends IExpiryBehavior {
|
||||
export interface IExpiryFieldCondition {
|
||||
columnName: string;
|
||||
columnValue: string;
|
||||
}
|
||||
|
||||
export interface IExpiryConditionGroup {
|
||||
operator: ExpiryLogicalOperator;
|
||||
conditions: IExpiryCondition[];
|
||||
}
|
||||
|
||||
export type IExpiryCondition = IExpiryFieldCondition | IExpiryConditionGroup;
|
||||
|
||||
export interface IExpiryValueRule extends IExpiryBehavior {
|
||||
// Version 1 compatibility. New rules should use condition.
|
||||
columnName?: string;
|
||||
columnValue?: string;
|
||||
condition?: IExpiryConditionGroup;
|
||||
}
|
||||
|
||||
export interface IExpiryConfig {
|
||||
baseField: string;
|
||||
expiryField: string;
|
||||
@@ -89,7 +104,9 @@ export function normalizeConfig(value: any): IExpiryConfig {
|
||||
export function getRuleFieldNames(config: IExpiryConfig): string[] {
|
||||
const fields: string[] = [];
|
||||
config.rules.forEach((rule: IExpiryValueRule): void => {
|
||||
if (isSafeInternalName(rule.columnName) && fields.indexOf(rule.columnName) < 0) {
|
||||
if (rule.condition) {
|
||||
collectConditionFieldNames(rule.condition, fields);
|
||||
} else if (rule.columnName && isSafeInternalName(rule.columnName) && fields.indexOf(rule.columnName) < 0) {
|
||||
fields.push(rule.columnName);
|
||||
}
|
||||
});
|
||||
@@ -102,11 +119,7 @@ export function resolveBehavior(
|
||||
): IExpiryBehavior {
|
||||
for (let index: number = 0; index < config.rules.length; index++) {
|
||||
const rule: IExpiryValueRule = config.rules[index];
|
||||
if (!Object.prototype.hasOwnProperty.call(fieldValues, rule.columnName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueMatches(fieldValues[rule.columnName], rule.columnValue)) {
|
||||
if (ruleMatches(rule, fieldValues)) {
|
||||
return {
|
||||
lifeTime: rule.lifeTime,
|
||||
columnRule: rule.columnRule
|
||||
@@ -153,21 +166,147 @@ function normalizeValueRules(value: any): IExpiryValueRule[] {
|
||||
|
||||
const rules: IExpiryValueRule[] = [];
|
||||
value.forEach((rule: any): void => {
|
||||
if (!rule || !isSafeInternalName(rule.columnName) || typeof rule.columnValue === 'undefined') {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const condition: IExpiryConditionGroup | undefined = normalizeConditionGroup(rule.condition, 0);
|
||||
const hasLegacyCondition: boolean = isSafeInternalName(rule.columnName) &&
|
||||
typeof rule.columnValue !== 'undefined';
|
||||
if (!condition && !hasLegacyCondition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnRules: IExpiryColumnRule[] = normalizeColumnRules(rule.columnRule, []);
|
||||
rules.push({
|
||||
columnName: rule.columnName,
|
||||
columnValue: String(rule.columnValue),
|
||||
const normalizedRule: IExpiryValueRule = {
|
||||
lifeTime: normalizeDuration(rule.lifeTime, { value: 1, unit: 'years' }),
|
||||
columnRule: columnRules
|
||||
});
|
||||
};
|
||||
if (condition) {
|
||||
normalizedRule.condition = condition;
|
||||
} else {
|
||||
normalizedRule.columnName = rule.columnName;
|
||||
normalizedRule.columnValue = String(rule.columnValue);
|
||||
}
|
||||
rules.push(normalizedRule);
|
||||
});
|
||||
return rules;
|
||||
}
|
||||
|
||||
export function validateValueRules(value: any): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return ['rules muss ein JSON-Array sein.'];
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
value.forEach((rule: any, index: number): void => {
|
||||
const path: string = 'rules[' + index + ']';
|
||||
if (!rule || typeof rule !== 'object') {
|
||||
errors.push(path + ' muss ein Objekt sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasLegacyCondition: boolean = isSafeInternalName(rule.columnName) &&
|
||||
typeof rule.columnValue !== 'undefined';
|
||||
if (rule.condition) {
|
||||
validateConditionGroup(rule.condition, path + '.condition', 0, errors);
|
||||
} else if (!hasLegacyCondition) {
|
||||
errors.push(path + ' benötigt condition oder columnName/columnValue.');
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
function normalizeConditionGroup(value: any, depth: number): IExpiryConditionGroup | undefined {
|
||||
if (!value || depth > 10 || !isLogicalOperator(value.operator) || !Array.isArray(value.conditions) ||
|
||||
value.conditions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conditions: IExpiryCondition[] = [];
|
||||
value.conditions.forEach((condition: any): void => {
|
||||
if (condition && typeof condition.operator !== 'undefined') {
|
||||
const group: IExpiryConditionGroup | undefined = normalizeConditionGroup(condition, depth + 1);
|
||||
if (group) {
|
||||
conditions.push(group);
|
||||
}
|
||||
} else if (condition && isSafeInternalName(condition.columnName) &&
|
||||
typeof condition.columnValue !== 'undefined') {
|
||||
conditions.push({
|
||||
columnName: condition.columnName,
|
||||
columnValue: String(condition.columnValue)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return conditions.length > 0 ? { operator: value.operator, conditions: conditions } : undefined;
|
||||
}
|
||||
|
||||
function validateConditionGroup(value: any, path: string, depth: number, errors: string[]): void {
|
||||
if (depth > 10) {
|
||||
errors.push(path + ' überschreitet die maximale Verschachtelungstiefe 10.');
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
errors.push(path + ' muss ein Objekt sein.');
|
||||
return;
|
||||
}
|
||||
if (!isLogicalOperator(value.operator)) {
|
||||
errors.push(path + '.operator muss "and" oder "or" sein.');
|
||||
}
|
||||
if (!Array.isArray(value.conditions) || value.conditions.length === 0) {
|
||||
errors.push(path + '.conditions muss mindestens eine Bedingung enthalten.');
|
||||
return;
|
||||
}
|
||||
|
||||
value.conditions.forEach((condition: any, index: number): void => {
|
||||
const conditionPath: string = path + '.conditions[' + index + ']';
|
||||
if (condition && typeof condition.operator !== 'undefined') {
|
||||
validateConditionGroup(condition, conditionPath, depth + 1, errors);
|
||||
} else if (!condition || !isSafeInternalName(condition.columnName) ||
|
||||
typeof condition.columnValue === 'undefined') {
|
||||
errors.push(conditionPath + ' benötigt einen gültigen columnName und columnValue.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ruleMatches(rule: IExpiryValueRule, fieldValues: { [fieldName: string]: any }): boolean {
|
||||
if (rule.condition) {
|
||||
return conditionMatches(rule.condition, fieldValues);
|
||||
}
|
||||
return !!rule.columnName && typeof rule.columnValue !== 'undefined' &&
|
||||
Object.prototype.hasOwnProperty.call(fieldValues, rule.columnName) &&
|
||||
valueMatches(fieldValues[rule.columnName], rule.columnValue);
|
||||
}
|
||||
|
||||
function conditionMatches(condition: IExpiryCondition, fieldValues: { [fieldName: string]: any }): boolean {
|
||||
if (isConditionGroup(condition)) {
|
||||
if (condition.operator === 'and') {
|
||||
return condition.conditions.every((child: IExpiryCondition): boolean => conditionMatches(child, fieldValues));
|
||||
}
|
||||
return condition.conditions.some((child: IExpiryCondition): boolean => conditionMatches(child, fieldValues));
|
||||
}
|
||||
|
||||
return Object.prototype.hasOwnProperty.call(fieldValues, condition.columnName) &&
|
||||
valueMatches(fieldValues[condition.columnName], condition.columnValue);
|
||||
}
|
||||
|
||||
function collectConditionFieldNames(condition: IExpiryCondition, fields: string[]): void {
|
||||
if (isConditionGroup(condition)) {
|
||||
condition.conditions.forEach((child: IExpiryCondition): void => collectConditionFieldNames(child, fields));
|
||||
} else if (fields.indexOf(condition.columnName) < 0) {
|
||||
fields.push(condition.columnName);
|
||||
}
|
||||
}
|
||||
|
||||
function isConditionGroup(condition: IExpiryCondition): condition is IExpiryConditionGroup {
|
||||
return typeof (condition as IExpiryConditionGroup).operator !== 'undefined';
|
||||
}
|
||||
|
||||
function isLogicalOperator(value: any): value is ExpiryLogicalOperator {
|
||||
return value === 'and' || value === 'or';
|
||||
}
|
||||
|
||||
function normalizeDuration(value: any, fallback: IExpiryDuration): IExpiryDuration {
|
||||
if (!value || typeof value.value !== 'number' || !isFinite(value.value)) {
|
||||
return fallback;
|
||||
|
||||
Reference in New Issue
Block a user