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:
240
src/common/ExpiryModels.ts
Normal file
240
src/common/ExpiryModels.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
export type DurationUnit = 'days' | 'months' | 'years';
|
||||
export type ColumnRuleOperator = 'lessThan' | 'lessOrEqual' | 'equal' | 'greaterOrEqual' | 'greaterThan';
|
||||
|
||||
export interface IExpiryDuration {
|
||||
value: number;
|
||||
unit: DurationUnit;
|
||||
}
|
||||
|
||||
export interface IExpiryColumnRule {
|
||||
daysUntilExpiry: number;
|
||||
operator: ColumnRuleOperator;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface IExpiryBehavior {
|
||||
lifeTime: IExpiryDuration;
|
||||
columnRule: IExpiryColumnRule[];
|
||||
}
|
||||
|
||||
export interface IExpiryValueRule extends IExpiryBehavior {
|
||||
columnName: string;
|
||||
columnValue: string;
|
||||
}
|
||||
|
||||
export interface IExpiryConfig {
|
||||
baseField: string;
|
||||
expiryField: string;
|
||||
default: IExpiryBehavior;
|
||||
rules: IExpiryValueRule[];
|
||||
nullText: string;
|
||||
confirmExtension: boolean;
|
||||
}
|
||||
|
||||
export interface IExpiryEvaluation {
|
||||
effectiveExpiryDate: Date;
|
||||
daysUntilExpiry: number;
|
||||
matchedRule?: IExpiryColumnRule;
|
||||
wasCalculated: boolean;
|
||||
}
|
||||
|
||||
export interface IItemDateValues {
|
||||
id: number;
|
||||
created: Date;
|
||||
expiry?: Date;
|
||||
fieldValues: { [fieldName: string]: any };
|
||||
}
|
||||
|
||||
export interface IUpdateResult {
|
||||
id: number;
|
||||
succeeded: boolean;
|
||||
newExpiryDate?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function createDefaultConfig(): IExpiryConfig {
|
||||
return {
|
||||
baseField: 'Created',
|
||||
expiryField: 'ExpiryDate',
|
||||
default: {
|
||||
lifeTime: { value: 2, unit: 'years' },
|
||||
columnRule: createDefaultColumnRules()
|
||||
},
|
||||
rules: [],
|
||||
nullText: 'Kein Ablaufdatum',
|
||||
confirmExtension: true
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeConfig(value: any): IExpiryConfig {
|
||||
const defaults: IExpiryConfig = createDefaultConfig();
|
||||
const config: any = value || {};
|
||||
const defaultBehavior: any = config.default || {};
|
||||
|
||||
return {
|
||||
baseField: isSafeInternalName(config.baseField) ? config.baseField : defaults.baseField,
|
||||
expiryField: isSafeInternalName(config.expiryField) ? config.expiryField : defaults.expiryField,
|
||||
default: {
|
||||
lifeTime: normalizeDuration(defaultBehavior.lifeTime, defaults.default.lifeTime),
|
||||
columnRule: normalizeColumnRules(defaultBehavior.columnRule, defaults.default.columnRule)
|
||||
},
|
||||
rules: normalizeValueRules(config.rules),
|
||||
nullText: typeof config.nullText === 'string' ? config.nullText : defaults.nullText,
|
||||
confirmExtension: typeof config.confirmExtension === 'boolean' ? config.confirmExtension : defaults.confirmExtension
|
||||
};
|
||||
}
|
||||
|
||||
export function getRuleFieldNames(config: IExpiryConfig): string[] {
|
||||
const fields: string[] = [];
|
||||
config.rules.forEach((rule: IExpiryValueRule): void => {
|
||||
if (isSafeInternalName(rule.columnName) && fields.indexOf(rule.columnName) < 0) {
|
||||
fields.push(rule.columnName);
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function resolveBehavior(
|
||||
config: IExpiryConfig,
|
||||
fieldValues: { [fieldName: string]: any }
|
||||
): 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)) {
|
||||
return {
|
||||
lifeTime: rule.lifeTime,
|
||||
columnRule: rule.columnRule
|
||||
};
|
||||
}
|
||||
}
|
||||
return config.default;
|
||||
}
|
||||
|
||||
export function isSafeInternalName(value: string): boolean {
|
||||
return typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
||||
}
|
||||
|
||||
function createDefaultColumnRules(): IExpiryColumnRule[] {
|
||||
return [
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeValueRules(value: any): IExpiryValueRule[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rules: IExpiryValueRule[] = [];
|
||||
value.forEach((rule: any): void => {
|
||||
if (!rule || !isSafeInternalName(rule.columnName) || typeof rule.columnValue === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnRules: IExpiryColumnRule[] = normalizeColumnRules(rule.columnRule, []);
|
||||
rules.push({
|
||||
columnName: rule.columnName,
|
||||
columnValue: String(rule.columnValue),
|
||||
lifeTime: normalizeDuration(rule.lifeTime, { value: 1, unit: 'years' }),
|
||||
columnRule: columnRules
|
||||
});
|
||||
});
|
||||
return rules;
|
||||
}
|
||||
|
||||
function normalizeDuration(value: any, fallback: IExpiryDuration): IExpiryDuration {
|
||||
if (!value || typeof value.value !== 'number' || !isFinite(value.value)) {
|
||||
return fallback;
|
||||
}
|
||||
if (value.unit !== 'days' && value.unit !== 'months' && value.unit !== 'years') {
|
||||
return fallback;
|
||||
}
|
||||
return { value: Math.floor(value.value), unit: value.unit };
|
||||
}
|
||||
|
||||
function normalizeColumnRules(value: any, fallback: IExpiryColumnRule[]): IExpiryColumnRule[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const rules: IExpiryColumnRule[] = [];
|
||||
value.forEach((rule: any): void => {
|
||||
if (!rule || typeof rule.daysUntilExpiry !== 'number' || !isFinite(rule.daysUntilExpiry)) {
|
||||
return;
|
||||
}
|
||||
if (!isColumnRuleOperator(rule.operator) || !isColor(rule.backgroundColor) || !isColor(rule.textColor)) {
|
||||
return;
|
||||
}
|
||||
rules.push({
|
||||
daysUntilExpiry: Math.floor(rule.daysUntilExpiry),
|
||||
operator: rule.operator,
|
||||
backgroundColor: rule.backgroundColor,
|
||||
textColor: rule.textColor,
|
||||
label: typeof rule.label === 'string' ? rule.label : ''
|
||||
});
|
||||
});
|
||||
return rules.length > 0 || fallback.length === 0 ? rules : fallback;
|
||||
}
|
||||
|
||||
function valueMatches(rawValue: any, configuredValue: string): boolean {
|
||||
if (rawValue === null || typeof rawValue === 'undefined') {
|
||||
return configuredValue === '';
|
||||
}
|
||||
|
||||
let values: any[];
|
||||
if (Array.isArray(rawValue)) {
|
||||
values = rawValue;
|
||||
} else if (rawValue.results && Array.isArray(rawValue.results)) {
|
||||
values = rawValue.results;
|
||||
} else {
|
||||
values = [rawValue];
|
||||
}
|
||||
|
||||
return values.some((value: any): boolean => {
|
||||
if (value && typeof value === 'object') {
|
||||
return String(value.Title || value.LookupValue || value.Label || value.Value || '') === configuredValue;
|
||||
}
|
||||
return String(value) === configuredValue;
|
||||
});
|
||||
}
|
||||
|
||||
function isColumnRuleOperator(value: string): boolean {
|
||||
return value === 'lessThan' || value === 'lessOrEqual' || value === 'equal' ||
|
||||
value === 'greaterOrEqual' || value === 'greaterThan';
|
||||
}
|
||||
|
||||
function isColor(value: string): boolean {
|
||||
return typeof value === 'string' && (
|
||||
/^#[0-9a-fA-F]{3}$/.test(value) ||
|
||||
/^#[0-9a-fA-F]{6}$/.test(value) ||
|
||||
/^rgba?\([0-9.,% ]+\)$/.test(value) ||
|
||||
/^[a-zA-Z]+$/.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user