moved code into OSS repo, configured to build and test here
This commit is contained in:
Родитель
39a5f3d9e9
Коммит
be043566e2
|
@ -0,0 +1,19 @@
|
|||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
temp
|
||||
lib
|
||||
jest
|
||||
*.d.ts
|
||||
*.scss.ts
|
||||
package.json
|
||||
|
||||
# don't lint configuration files
|
||||
gulpfile.js
|
||||
.eslintrc.js
|
||||
index.test.ts
|
||||
just-task.js
|
||||
jest.config.js
|
|
@ -0,0 +1,118 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||
],
|
||||
env: {
|
||||
"browser": true
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
"tsconfigRootDir": __dirname,
|
||||
"project": "tsconfig.json",
|
||||
"sourceType": "module",
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off", // this one is giving me an error. Need to look close into it
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{
|
||||
"accessibility": "explicit",
|
||||
"overrides": {
|
||||
"constructors": "no-public"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"no-param-reassign": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"guard-for-in": "warn",
|
||||
"no-caller": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"debug",
|
||||
"info",
|
||||
"dirxml",
|
||||
"warn",
|
||||
"error",
|
||||
"dir",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"timeLog",
|
||||
"trace",
|
||||
"assert",
|
||||
"clear",
|
||||
"count",
|
||||
"countReset",
|
||||
"group",
|
||||
"groupCollapsed",
|
||||
"groupEnd",
|
||||
"table",
|
||||
"Console",
|
||||
"markTimeline",
|
||||
"profile",
|
||||
"profileEnd",
|
||||
"timeline",
|
||||
"timelineEnd",
|
||||
"timeStamp",
|
||||
"context"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-eval": "error",
|
||||
"no-extra-boolean-cast": "off",
|
||||
"no-extra-semi": "error", // in eslint:recommended
|
||||
"no-fallthrough": "error", // in eslint:recommended
|
||||
"no-magic-numbers": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"hoist": "all"
|
||||
}
|
||||
],
|
||||
"no-undef": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unused-vars": "off",
|
||||
'@typescript-eslint/no-unused-vars': ['error', {"argsIgnorePattern": "^_"}],
|
||||
}
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
lib
|
||||
jest
|
||||
temp
|
|
@ -0,0 +1,43 @@
|
|||
module.exports = {
|
||||
collectCoverage: true,
|
||||
coverageDirectory: "<rootDir>/jest",
|
||||
coverageReporters: ["json", "lcov", "text", "cobertura"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 75,
|
||||
functions: 75,
|
||||
lines: 75,
|
||||
statements: 75,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
diagnostics: false,
|
||||
tsConfig: "tsconfig.json",
|
||||
packageJson: "package.json",
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js"],
|
||||
moduleNameMapper: {
|
||||
"\\.(css|scss)$": "identity-obj-proxy",
|
||||
"office-ui-fabric-react/lib/(.*)":
|
||||
"<rootDir>/node_modules/office-ui-fabric-react/lib-commonjs/$1",
|
||||
"^resx-strings/en-us.json":
|
||||
"<rootDir>/node_modules/@microsoft/sp-core-library/lib/resx-strings/en-us.json",
|
||||
},
|
||||
reporters: ["jest-standard-reporter", "jest-junit"],
|
||||
testMatch: ["<rootDir>/src/**/*.test.+(ts|js)?(x)"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest",
|
||||
"^.+\\.(js|jsx)$": "babel-jest",
|
||||
},
|
||||
unmockedModulePathPatterns: ["react"],
|
||||
collectCoverageFrom: [
|
||||
"**/*.ts",
|
||||
"!**/*.d.ts",
|
||||
"!**/*.test.ts",
|
||||
"!**/node_modules/**",
|
||||
"!src/**/index.ts",
|
||||
"!src/**/*.types.ts",
|
||||
],
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
const rimraf = require('rimraf');
|
||||
const { parallel, task } = require('just-task');
|
||||
const { logger, jestTask, tscTask, eslintTask } = require('just-scripts');
|
||||
|
||||
task('tsc', tscTask({}));
|
||||
task('eslint', eslintTask());
|
||||
task('test', jestTask());
|
||||
task('clean', async () => {
|
||||
const makeCb = (dirName, res) => (error) => {
|
||||
if (error) {
|
||||
logger.error(`Error removing ${dirName}`, error);
|
||||
}
|
||||
|
||||
logger.info(`Deleted ${dirName}`);
|
||||
res();
|
||||
};
|
||||
|
||||
const delPromises = [];
|
||||
|
||||
delPromises.push(new Promise(res => rimraf('junit.xml', makeCb('junit.xml', res))));
|
||||
delPromises.push(new Promise(res => rimraf('jest', makeCb('jest', res))));
|
||||
delPromises.push(new Promise(res => rimraf('lib', makeCb('lib', res))));
|
||||
delPromises.push(new Promise(res => rimraf('*.log', makeCb('*.log', res))));
|
||||
});
|
||||
|
||||
task('build', parallel('tsc', 'eslint'));
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@microsoft/sharepoint-recurring-events",
|
||||
"version": "0.0.1",
|
||||
"author": "Microsoft Corporation",
|
||||
"license": "MIT",
|
||||
"description": "Help work with recurring events in SharePoint",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/sharepoint-recurring-events.git"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"start": "node ./lib/index.js",
|
||||
"build": "just build",
|
||||
"bundle": "",
|
||||
"clean": "just clean",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"package-solution": "",
|
||||
"serve": "",
|
||||
"silent-test": "jest --config=./jest.config.js --reporters=\"jest-junit\" --coverageReporters=\"cobertura\"",
|
||||
"test": "just test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"eslint": "7.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.7.0",
|
||||
"@typescript-eslint/parser": "3.7.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "3.7.0",
|
||||
"gulp-eslint": "6.0.0",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/lodash": "4.14.168",
|
||||
"lodash": "4.17.21",
|
||||
"jest": "^25.5.4",
|
||||
"jest-junit": "^11.1.0",
|
||||
"jest-standard-reporter": "1.0.4",
|
||||
"just-scripts": "^0.44.2",
|
||||
"just-task": "^0.17.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "~3.7.2",
|
||||
"webpack-stream": "^5.2.1"
|
||||
},
|
||||
"jest-junit": {
|
||||
"outputDirectory": "./jest/",
|
||||
"outputName": "junit.xml"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,296 @@
|
|||
/* eslint no-magic-numbers: ["error", {"ignore": [0,1, 5]}] */
|
||||
import 'jest';
|
||||
import {
|
||||
isNotAtEnd,
|
||||
createNewEvent,
|
||||
ispe,
|
||||
setAttributes,
|
||||
expandEvent,
|
||||
} from './expandEvents';
|
||||
|
||||
describe("expand events utitlity functions", () => {
|
||||
it('should be able to determine when we are at the end', () => {
|
||||
const start: Date = new Date('2021-03-16T00:00:00');
|
||||
const end: Date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 1);
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
const endBound: Date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 2);
|
||||
const recurrenceTotal = 5;
|
||||
|
||||
expect(start < end).toBeTruthy();
|
||||
expect(end < endBound).toBeTruthy();
|
||||
expect(recurrenceTotal).toBe(5);
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 0)).toBe(true);
|
||||
|
||||
// start is after end
|
||||
start.setDate(end.getDate() + 1);
|
||||
expect(start > end).toBeTruthy();
|
||||
expect(end < endBound).toBeTruthy();
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 0)).toBe(false);
|
||||
|
||||
end.setDate(endBound.getDate() + 1);
|
||||
expect(start < end).toBeTruthy();
|
||||
expect(end > endBound).toBeTruthy();
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 0)).toBe(false);
|
||||
|
||||
endBound.setDate(end.getDate() + 1);
|
||||
expect(start < end).toBeTruthy();
|
||||
expect(end < endBound).toBeTruthy();
|
||||
/* eslint-disable no-magic-numbers */
|
||||
expect(recurrenceTotal).toBe(5);
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 1)).toBe(true);
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 5)).toBe(false);
|
||||
expect(isNotAtEnd(start, end, endBound, recurrenceTotal, 6)).toBe(false);
|
||||
/* eslint-enable no-magic-numbers */
|
||||
|
||||
// ignore bound if it is null
|
||||
expect(isNotAtEnd(start, end, null, recurrenceTotal, 0)).toBe(true);
|
||||
|
||||
start.setDate(end.getDate() + 1);
|
||||
expect(start > end).toBeTruthy();
|
||||
expect(isNotAtEnd(start, end, null, recurrenceTotal, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it('properly creates a new event with no recurrence data', () => {
|
||||
const oldEvent: ispe = {
|
||||
fRecurrence: false,
|
||||
EventDate: "2021-03-16T00:00:00Z",
|
||||
EndDate: "2021-03-16T01:00:00Z",
|
||||
Duration: 3600,
|
||||
fAllDayEvent: false,
|
||||
RecurrenceData: null,
|
||||
};
|
||||
const newStartDate = new Date('2021-03-18T00:00:00Z');
|
||||
const newEvent = createNewEvent(oldEvent, newStartDate);
|
||||
|
||||
expect(newEvent.fRecurrence).toBe(false);
|
||||
expect(newEvent.EventDate).toBe('2021-03-18T00:00:00.000Z');
|
||||
expect(newEvent.EndDate).toBe('2021-03-18T01:00:00.000Z');
|
||||
expect(newEvent.fAllDayEvent).toBe(false);
|
||||
expect(newEvent.RecurrenceData).toBeNull();
|
||||
// it occurs to me, this would never run on an event with false recurrence data, but that's okay.
|
||||
});
|
||||
it('properly creates a new event with recurrence data', () => {
|
||||
const recurData = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><yearlyByDay yearFrequency="1" mo="TRUE" weekdayOfMonth="second" month="2" /></repeat><repeatInstances>10</repeatInstances></rule></recurrence>`;
|
||||
const oldEvent: ispe = {
|
||||
fRecurrence: true,
|
||||
EventDate: "2021-03-16T00:00:00Z",
|
||||
EndDate: "2021-03-16T01:00:00Z",
|
||||
Duration: 3600,
|
||||
fAllDayEvent: false,
|
||||
RecurrenceData: recurData,
|
||||
};
|
||||
const newStartDate = new Date('2021-03-18T00:00:00Z');
|
||||
const newEvent = createNewEvent(oldEvent, newStartDate);
|
||||
|
||||
expect(newEvent.fRecurrence).toBe(true);
|
||||
expect(newEvent.EventDate).toBe('2021-03-18T00:00:00.000Z');
|
||||
expect(newEvent.EndDate).toBe('2021-03-18T01:00:00.000Z');
|
||||
expect(newEvent.fAllDayEvent).toBe(false);
|
||||
expect(newEvent.RecurrenceData).toBe(recurData);
|
||||
// it occurs to me, this would never run on an event with false recurrence data, but that's okay.
|
||||
});
|
||||
it('properly creates a new all day event with recurrence data', () => {
|
||||
const recurData = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><yearlyByDay yearFrequency="1" mo="TRUE" weekdayOfMonth="second" month="2" /></repeat><repeatInstances>10</repeatInstances></rule></recurrence>`;
|
||||
const oldEvent: ispe = {
|
||||
fRecurrence: true,
|
||||
EventDate: "2021-03-16T00:00:00Z",
|
||||
EndDate: "2021-03-16T23:59:00Z",
|
||||
Duration: 86340,
|
||||
fAllDayEvent: true,
|
||||
RecurrenceData: recurData,
|
||||
};
|
||||
const newStartDate = new Date('2021-03-18T08:00:00Z'); // as if we were in another timezone
|
||||
const newEvent = createNewEvent(oldEvent, newStartDate);
|
||||
|
||||
expect(newEvent.fRecurrence).toBe(true);
|
||||
expect(newEvent.EventDate).toBe('2021-03-18T00:00:00.000Z');
|
||||
expect(newEvent.EndDate).toBe('2021-03-18T23:59:00.000Z');
|
||||
expect(newEvent.fAllDayEvent).toBe(true);
|
||||
expect(newEvent.RecurrenceData).toBe(recurData);
|
||||
// it occurs to me, this would never run on an event with false recurrence data, but that's okay.
|
||||
});
|
||||
|
||||
it('can set multiple attributes', () => {
|
||||
const ele = setAttributes(document.createElement('div'), [
|
||||
['test', 'one'],
|
||||
['ben', 'clocks'],
|
||||
['darin', 'Rocks'],
|
||||
]);
|
||||
expect(ele.hasAttribute('test')).toBe(true);
|
||||
expect(ele.getAttribute('test')).toBe('one');
|
||||
expect(ele.hasAttribute('ben')).toBe(true);
|
||||
expect(ele.getAttribute('ben')).toBe('clocks');
|
||||
expect(ele.hasAttribute('darin')).toBe(true);
|
||||
expect(ele.getAttribute('darin')).toBe('Rocks');
|
||||
});
|
||||
});
|
||||
|
||||
const dailyRecurringEvent = {
|
||||
"Id": 12,
|
||||
"Title": "daily",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-26T00:00:00Z",
|
||||
"EndDate": "2021-03-07T00:30:00Z",
|
||||
"Description": "<p>daily<br></p><p>every 1 day</p><p>start date: 2/25/2021<br></p><p>end after 10 occurences<br></p>",
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 1800,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><daily dayFrequency=\"1\" /></repeat><repeatInstances>10</repeatInstances></rule></recurrence>",
|
||||
"Category": "Holiday",
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 12,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
const monthRecurringEvent = {
|
||||
"Id": 4,
|
||||
"Title": "mothly recurring event",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-06T10:00:00Z",
|
||||
"EndDate": "2021-11-06T10:00:00Z",
|
||||
"Description": "<p>Day 5 of every month<br></p>",
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 3600,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><monthly monthFrequency=\"1\" day=\"5\" /></repeat><repeatInstances>10</repeatInstances></rule></recurrence>",
|
||||
"Category": "Holiday",
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 4,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
const monthByDayEvent = {
|
||||
"Id": 5,
|
||||
"Title": "friday monthly by day recurring event",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-13T01:00:00Z",
|
||||
"EndDate": "2104-04-19T01:00:00Z",
|
||||
"Description": "<p>the third friday of every month<br></p>",
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 3600,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><monthlyByDay fr=\"TRUE\" weekdayOfMonth=\"third\" monthFrequency=\"1\" /></repeat><repeatForever>FALSE</repeatForever></rule></recurrence>",
|
||||
"Category": "Meeting",
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 5,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
const yearEvent = {
|
||||
"Id": 8,
|
||||
"Title": "yearly recurring event",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-17T01:00:00Z",
|
||||
"EndDate": "2170-02-17T02:00:00Z",
|
||||
"Description": "<p>occurs every Feb 16<br></p>",
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 3600,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><yearly yearFrequency=\"1\" month=\"2\" day=\"16\" /></repeat><repeatForever>FALSE</repeatForever></rule></recurrence>",
|
||||
"Category": "Holiday",
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 8,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
const weekDayEvent = {
|
||||
"Id": 9,
|
||||
"Title": "weekday recurring",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-22T20:00:00Z",
|
||||
"EndDate": "2024-12-19T21:00:00Z",
|
||||
"Description": null,
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 3600,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><daily weekday=\"TRUE\" /></repeat><repeatForever>FALSE</repeatForever></rule></recurrence>",
|
||||
"Category": "Work hours",
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 9,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
const yearByDayEvent = {
|
||||
"Id": 18,
|
||||
"Title": "year by day",
|
||||
"Location": null,
|
||||
"EventDate": "2021-02-19T00:00:00Z",
|
||||
"EndDate": "2031-02-07T01:00:00Z",
|
||||
"Description": null,
|
||||
"fAllDayEvent": false,
|
||||
"fRecurrence": true,
|
||||
"Duration": 3600,
|
||||
"RecurrenceData": "<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat><yearlyByDay yearFrequency=\"1\" th=\"TRUE\" weekdayOfMonth=\"first\" month=\"2\" /></repeat><repeatInstances>10</repeatInstances></rule></recurrence>",
|
||||
"Category": null,
|
||||
"BIC_DateSelection": "Specific Date",
|
||||
"BIC_Contact": null,
|
||||
"ID": 18,
|
||||
"Attachments": false,
|
||||
"AttachmentFiles": [],
|
||||
};
|
||||
|
||||
describe('expand events function', () => {
|
||||
it('daily recurrence', () => {
|
||||
const expandedEvents = expandEvent(dailyRecurringEvent);
|
||||
expect(expandedEvents.length).toBe(10); // eslint-disable-line no-magic-numbers
|
||||
});
|
||||
it('monthly recurrence', () => {
|
||||
// unbounded
|
||||
const expandedEvents = expandEvent(monthRecurringEvent);
|
||||
expect(expandedEvents.length).toBe(10); // eslint-disable-line no-magic-numbers
|
||||
|
||||
// one month
|
||||
const oneMonth = expandEvent(monthRecurringEvent, {start: null, end: new Date('2021-03-01T00:00:00Z')});
|
||||
expect(oneMonth.length).toBe(1);
|
||||
});
|
||||
it('does not recur', () => {
|
||||
const event: ispe = {
|
||||
fRecurrence: false,
|
||||
EventDate: new Date().toISOString(),
|
||||
EndDate: new Date().toISOString(),
|
||||
fAllDayEvent: false,
|
||||
Duration: 1800,
|
||||
RecurrenceData: null,
|
||||
};
|
||||
const expandedEvents = expandEvent(event);
|
||||
expect(expandedEvents.length).toBe(1);
|
||||
});
|
||||
it('recurs monthly by day', () => {
|
||||
const expandedEvents = expandEvent(monthByDayEvent);
|
||||
expect(expandedEvents.length).toBe(999); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const bounded = expandEvent(monthByDayEvent, {start: new Date('2022-05-01T00:00:00Z'), end: new Date('2022-07-01T00:00:00Z')});
|
||||
expect(bounded.length).toBe(2); //eslint-disable-line no-magic-numbers
|
||||
});
|
||||
it('year recurrence', () => {
|
||||
const expandedEvents = expandEvent(yearEvent);
|
||||
expect(expandedEvents.length).toBe(150); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const bounded = expandEvent(yearEvent, {start: new Date('2023-01-01T00:00:00Z'), end: new Date('2023-03-01T00:00:00Z')});
|
||||
expect(bounded.length).toBe(1);
|
||||
});
|
||||
it('weekday recurrence', () => {
|
||||
const expandedEvents = expandEvent(weekDayEvent);
|
||||
expect(expandedEvents.length).toBe(1000); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const bounded = expandEvent(weekDayEvent, {start: new Date('2023-01-01T00:00:00Z'), end: new Date('2023-03-01T00:00:00Z')});
|
||||
expect(bounded.length).toBe(45); // eslint-disable-line no-magic-numbers
|
||||
});
|
||||
it('year by day recurrence', () => {
|
||||
const expandedEvents = expandEvent(yearByDayEvent);
|
||||
expect(expandedEvents.length).toBe(10); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const bounded = expandEvent(yearByDayEvent, {start: new Date('2023-01-01T00:00:00Z'), end: new Date('2023-03-01T00:00:00Z')});
|
||||
expect(bounded.length).toBe(1); // eslint-disable-line no-magic-numbers
|
||||
});
|
||||
});
|
|
@ -0,0 +1,319 @@
|
|||
/**
|
||||
* external exports can be found at the bottom of the file,
|
||||
* all other exports are for testing purposes
|
||||
*/
|
||||
import { unescape, cloneDeep } from 'lodash';
|
||||
const NUMBER_OF_WEEKDAYS = 7;
|
||||
const MONTH_OFFSET = 1;
|
||||
const DEFAULT_RECURRENCE_TOTAL = 0;
|
||||
const FIRST_DAY_OF_MONTH = 1;
|
||||
const SUNDAY = 0;
|
||||
const MIDNIGHT = 0;
|
||||
|
||||
interface ispe {
|
||||
fRecurrence: boolean;
|
||||
EventDate: string;
|
||||
EndDate: string;
|
||||
Duration: number;
|
||||
RecurrenceData: string | null;
|
||||
fAllDayEvent: boolean;
|
||||
}
|
||||
|
||||
interface IBounds {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
}
|
||||
|
||||
const expandEvent = <T extends ispe>(event: T, bounds: IBounds = {start: null, end: null}): T[] => {
|
||||
// if it's not a recurring event, just return it
|
||||
if(!event.fRecurrence || event.RecurrenceData === null) return [event];
|
||||
|
||||
|
||||
const weekDays = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'];
|
||||
const weekOfMonths = ['first', 'second', 'third', 'fourth'];
|
||||
|
||||
const startDate: Date = event.fAllDayEvent
|
||||
// remove the "Z"/timezone info from the ISO date string so it parses as midnight local time, not UTC
|
||||
// (allow magic numbers to perform this calculation)
|
||||
? new Date(event.EventDate.substring(0, event.EventDate.length - 1)) // eslint-disable-line no-magic-numbers
|
||||
: new Date(event.EventDate);
|
||||
const endDate = new Date(event.EndDate);
|
||||
const xmlDom = (new DOMParser()).parseFromString(unescape(event.RecurrenceData), 'text/xml');
|
||||
|
||||
|
||||
const eventReturn: T[] = []; // still pushed to
|
||||
|
||||
const repeatInstances = xmlDom.querySelector('repeatInstances');
|
||||
const rTotal = !!repeatInstances ? parseInt(repeatInstances.textContent!) : DEFAULT_RECURRENCE_TOTAL;
|
||||
|
||||
const dailyNode = xmlDom.querySelector('daily');
|
||||
// basically, we are calculating the start time of each occurence, then the createNewEvent function
|
||||
// can create the end time and the rest of the event.
|
||||
if(!!dailyNode) {
|
||||
const dayFreq: number = parseInt(dailyNode.getAttribute('dayFrequency')!);
|
||||
if(!!dayFreq) {
|
||||
const recurStart = new Date(startDate.toString());
|
||||
let total = 0;
|
||||
// while((recurStart.getTime() < endDate.getTime()
|
||||
// && (!bounds.end || (bounds.end && recurStart.getTime() < bounds.end.getTime())))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
// probably need to put something here, move the date forward and increse total. "Increment" the loop conditions
|
||||
if(recurStart.getTime() >= startDate.getTime()
|
||||
&& (!bounds.start || (bounds.start && recurStart.getTime() >= bounds.start.getTime()))) { // put start bound check here? or maybe I just set recurStart to it, if it's further in the future than event.EventDate...
|
||||
|
||||
const newStart = new Date(recurStart.toString());
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
|
||||
}
|
||||
// loop "increment" statements, should happen every iteration
|
||||
total++;
|
||||
recurStart.setDate(recurStart.getDate() + dayFreq);
|
||||
}
|
||||
}
|
||||
else if(dailyNode.hasAttribute('weekday') && dailyNode.getAttribute('weekday') === 'TRUE') {
|
||||
const weekly = setAttributes(document.createElement('weekly'), [
|
||||
['mo', 'TRUE'],
|
||||
['tu', 'TRUE'],
|
||||
['we', 'TRUE'],
|
||||
['th', 'TRUE'],
|
||||
['fr', 'TRUE'],
|
||||
['weekFrequency', '1'], // attr key will always be lower case
|
||||
]);
|
||||
xmlDom.querySelector('repeat')!.appendChild( weekly );
|
||||
}
|
||||
}
|
||||
|
||||
const weeklyNode = xmlDom.querySelector('weekly');
|
||||
if(!!weeklyNode) {
|
||||
|
||||
// uppercase for weekly from SharePoint, lower case for "weekday" recurrence, node set above ^^
|
||||
const weekFreq = parseInt(weeklyNode.getAttribute('weekFrequency') || weeklyNode.getAttribute('weekfrequency')!);
|
||||
const recurStart = new Date(startDate.toString()); // date still modified
|
||||
let recurDay = recurStart.getDay();
|
||||
let total = 0;
|
||||
// while((recurStart.getTime() <= endDate.getTime())
|
||||
// && (!bounds.end || (bounds.end && recurStart <= bounds.end))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
|
||||
// every time week is incremented by freq, check every weekday (su-sa) to create all events for the week
|
||||
weekDays.forEach((weekDay, index) => {
|
||||
if((weeklyNode.hasAttribute(weekDay) && weeklyNode.getAttribute(weekDay) === 'TRUE')
|
||||
&& (rTotal === DEFAULT_RECURRENCE_TOTAL || rTotal > total)) {
|
||||
total++; //increment total here, in case we hit max number in middle of week
|
||||
const newStart = new Date(recurStart.toString()); // create a copy of the loop Date
|
||||
newStart.setDate(newStart.getDate() + (index - recurDay)); // update the weekday
|
||||
|
||||
if(!bounds.start || (bounds.start && newStart.getTime() >= bounds.start.getTime())){ // start bound check, use newStart in case bound is in the middle of the week
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
}
|
||||
}
|
||||
});
|
||||
// increment to the next week that has events
|
||||
recurStart.setDate(recurStart.getDate() + ((NUMBER_OF_WEEKDAYS*weekFreq) - recurDay));
|
||||
recurDay = SUNDAY;
|
||||
}
|
||||
}
|
||||
|
||||
const monthlyNode = xmlDom.querySelector('monthly');
|
||||
if(!!monthlyNode) {
|
||||
|
||||
// mostly copy-paste from daily
|
||||
const monthFreq = parseInt(monthlyNode.getAttribute('monthFrequency')!);
|
||||
const day = parseInt(monthlyNode.getAttribute('day')!);
|
||||
const recurStart = new Date(startDate.toString()); // date still modified
|
||||
let total = 0;
|
||||
|
||||
if(!!monthFreq) {
|
||||
// while((recurStart.getTime() < endDate.getTime())
|
||||
// &&(!bounds.end || (bounds.end && recurStart.getTime() < bounds.end.getTime()))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
if(recurStart.getTime() >= startDate.getTime()) {
|
||||
const newStart = new Date(recurStart.toString());
|
||||
newStart.setDate(day);
|
||||
if(newStart.getMonth() === recurStart.getMonth()
|
||||
&& (!bounds.start || (bounds.start && newStart.getTime() >= bounds.start.getTime()))) {
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
}
|
||||
}
|
||||
// loop increment statements
|
||||
total++; // should this only be updated if the above if statement succeeds?
|
||||
recurStart.setMonth(recurStart.getMonth() + monthFreq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const monthlyByDayNode = xmlDom.querySelector('monthlyByDay');
|
||||
if(!!monthlyByDayNode) {
|
||||
|
||||
// montly copy-paste from yearlyByDay
|
||||
const monthFreq = parseInt(monthlyByDayNode.getAttribute('monthFrequency')!);
|
||||
const weekdayOfMonth = monthlyByDayNode.getAttribute('weekdayOfMonth')!;
|
||||
const day: number = weekDays.reduce((acc, d, index) => // find which day attribute is present, I think only one can be present
|
||||
monthlyByDayNode.hasAttribute(d) && monthlyByDayNode.getAttribute(d) === 'TRUE'
|
||||
? index
|
||||
: acc
|
||||
, SUNDAY);
|
||||
const recurStart = new Date(startDate.toString());
|
||||
let total = 0;
|
||||
|
||||
// while((recurStart.getTime() < endDate.getTime())
|
||||
// && (!bounds.end || (bounds.end && recurStart.getTime() < bounds.end.getTime()))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
|
||||
let newStart = new Date(recurStart.toString());
|
||||
if(recurStart.getTime() >= startDate.getTime()) { // add start bound here, I think?
|
||||
|
||||
total++; // this should be updated when outside bounds, but not recurStart
|
||||
if(!bounds.start || (bounds.start && recurStart.getTime() >= bounds.start.getTime())) { // add start bound here, I think?
|
||||
newStart.setDate(FIRST_DAY_OF_MONTH);
|
||||
const dayOfMonth = newStart.getDay();
|
||||
if (day < dayOfMonth) newStart.setDate(newStart.getDate() + ((NUMBER_OF_WEEKDAYS - dayOfMonth) + day)); //first instance of this day in the selected month
|
||||
else newStart.setDate(newStart.getDate() + (day - dayOfMonth));
|
||||
// find the date
|
||||
if(weekdayOfMonth === 'last') { // needs tested
|
||||
const temp = new Date(newStart.toString());
|
||||
while(temp.getMonth() === recurStart.getMonth()) {
|
||||
newStart = new Date(temp.toString());
|
||||
temp.setDate(temp.getDate() + NUMBER_OF_WEEKDAYS); // loop through month
|
||||
}
|
||||
} else {
|
||||
newStart.setDate(newStart.getDate() + (NUMBER_OF_WEEKDAYS * weekOfMonths.indexOf(weekdayOfMonth)));
|
||||
}
|
||||
|
||||
if(newStart.getMonth() === recurStart.getMonth()) { // make sure it's still the same month
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
recurStart.setMonth(recurStart.getMonth() + monthFreq);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const yearlyNode = xmlDom.querySelector('yearly');
|
||||
if(!!yearlyNode) {
|
||||
// mostly copy-paste from monthly
|
||||
const yearFreq = parseInt(yearlyNode.getAttribute('yearFrequency')!);
|
||||
const month = parseInt(yearlyNode.getAttribute('month')!) - MONTH_OFFSET; // months are zero-based in javascript, but one-based in SharePoint
|
||||
const day = parseInt(yearlyNode.getAttribute('day')!);
|
||||
const recurStart = new Date(startDate.toString()); // date still modified
|
||||
let total = 0;
|
||||
|
||||
if(!!yearFreq) {
|
||||
// while((recurStart.getTime() < endDate.getTime())
|
||||
// && (!bounds.end || (bounds.end && recurStart.getTime() < bounds.end.getTime()))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
if(recurStart.getTime() >= startDate.getTime()
|
||||
&& (!bounds.start || (bounds.start && recurStart.getTime() >= bounds.start.getTime()))) {
|
||||
const newStart = new Date(recurStart.toString());
|
||||
newStart.setMonth(month);
|
||||
newStart.setDate(day);
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
}
|
||||
// loop increment statements
|
||||
total++;
|
||||
recurStart.setFullYear(recurStart.getFullYear() + yearFreq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const yearlyByDayNode = xmlDom.querySelector('yearlyByDay');
|
||||
if(!!yearlyByDayNode) {
|
||||
const yearFreq: number = parseInt(yearlyByDayNode.getAttribute('yearFrequency')!);
|
||||
const month: number = parseInt(yearlyByDayNode.getAttribute('month')!) - MONTH_OFFSET;
|
||||
// no matter what the attribute name implies, this is the week of the month
|
||||
// and has nothing to do with which weekday
|
||||
const weekOfMonth: string = yearlyByDayNode.getAttribute('weekdayOfMonth')!;
|
||||
const recurStart: Date = new Date(startDate.toString());
|
||||
const day: number = weekDays.reduce((acc, d, index) => // find which day attribute is present, I guess only one can be?
|
||||
yearlyByDayNode.hasAttribute(d) && yearlyByDayNode.getAttribute(d) === 'TRUE'
|
||||
? index
|
||||
: acc
|
||||
, SUNDAY);
|
||||
let total = 0;
|
||||
// I think this is the exact same check for _every single_ recurrence type
|
||||
// while((recurStart.getTime() < endDate.getTime())
|
||||
// && (!bounds.end || (bounds.end && recurStart.getTime() < bounds.end.getTime()))
|
||||
// && (rTotal === 0 || rTotal > total)) {
|
||||
while(isNotAtEnd(recurStart, endDate, bounds.end, rTotal, total)) {
|
||||
|
||||
let newStart = new Date(recurStart.toString());
|
||||
newStart.setMonth(month);
|
||||
if(recurStart.getTime() >= startDate.getTime()) { // this _should always_ be true
|
||||
total++; // loop incrementing, could this be moved to the bottom? Or does it depend on the above conditional?
|
||||
if(!bounds.start || (bounds.start && recurStart.getTime() >= bounds.start.getTime())) { // add start bound here, I think?
|
||||
newStart.setDate(FIRST_DAY_OF_MONTH);
|
||||
const dayOfMonth = newStart.getDay();
|
||||
if (day < dayOfMonth) newStart.setDate(newStart.getDate() + ((NUMBER_OF_WEEKDAYS - dayOfMonth) + day)); //first instance of this day in the selected month
|
||||
else newStart.setDate(newStart.getDate() + (day - dayOfMonth));
|
||||
// find the date
|
||||
if(weekOfMonth === 'last') { // needs tested
|
||||
const temp = new Date(newStart.toString());
|
||||
while(temp.getMonth() === month) {
|
||||
newStart = new Date(temp.toString());
|
||||
temp.setDate(temp.getDate() + NUMBER_OF_WEEKDAYS); // loop through month
|
||||
}
|
||||
} else {
|
||||
newStart.setDate(newStart.getDate() + (NUMBER_OF_WEEKDAYS * weekOfMonths.indexOf(weekOfMonth)));
|
||||
}
|
||||
|
||||
if(newStart.getMonth() === month) { // make sure it's still the same month
|
||||
// within this if statement seems to be everything that is shared between
|
||||
// types of recurrences, and could probably be moved out into a separate function
|
||||
const newEvent = createNewEvent(event, newStart);
|
||||
eventReturn.push(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
// loop incrementing statements
|
||||
recurStart.setFullYear(recurStart.getFullYear() + yearFreq);
|
||||
recurStart.setMonth(month);
|
||||
recurStart.setDate(FIRST_DAY_OF_MONTH);
|
||||
}
|
||||
}
|
||||
return eventReturn;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/** sets all of the listed attributes (attrs) on Element e */
|
||||
export const setAttributes = (e: Element, attrs: [string, string][]): Element => {
|
||||
attrs.forEach(([key, value]) => {
|
||||
e.setAttribute(key, value);
|
||||
});
|
||||
return e;
|
||||
};
|
||||
|
||||
/**
|
||||
* this function takes the parts of the above if statement structure to help avoid
|
||||
* duplication of code and simplify the calculations being done in the expandEvents function
|
||||
* @param oldEvent the event with the recurrence information
|
||||
* @param newStart the calculcated start time of the current instance of the recurring event
|
||||
*/
|
||||
export const createNewEvent = <T extends ispe>(oldEvent: T, newStart: Date): T => {
|
||||
if(oldEvent.fAllDayEvent) newStart.setUTCHours(MIDNIGHT);
|
||||
const newEnd = new Date(newStart.toString());
|
||||
newEnd.setSeconds(newEnd.getSeconds() + oldEvent.Duration);
|
||||
const newEvent = cloneDeep(oldEvent);
|
||||
newEvent.EventDate = newStart.toISOString();
|
||||
newEvent.EndDate = newEnd.toISOString();
|
||||
return newEvent;
|
||||
};
|
||||
|
||||
export const isNotAtEnd = (recurStart: Date, endDate: Date, endBound: Date | null, recurrenceTotal: number, total: number): boolean =>
|
||||
(recurStart.getTime() < endDate.getTime())
|
||||
&& (!endBound || (endBound && recurStart.getTime() < endBound.getTime()))
|
||||
&& (recurrenceTotal === DEFAULT_RECURRENCE_TOTAL || recurrenceTotal > total);
|
||||
|
||||
|
||||
export { expandEvent, ispe };
|
|
@ -0,0 +1 @@
|
|||
export { expandEvent, ispe} from './expandEvents';
|
|
@ -0,0 +1 @@
|
|||
export * from './expandEvent';
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"outDir": "lib",
|
||||
"rootDir": "./src/",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["jest"],
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"declarationMap": false,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"removeComments": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
},
|
||||
"include": ["**/src/**/*.ts", "**/src/**/*.test.ts"],
|
||||
"exclude": ["../../../../node_modules", "../../../../lib"]
|
||||
}
|
Загрузка…
Ссылка в новой задаче