зеркало из
1
0
Форкнуть 0

moved code into OSS repo, configured to build and test here

This commit is contained in:
Benjamin Kenawell 2021-03-05 12:36:02 -08:00
Родитель 39a5f3d9e9
Коммит be043566e2
12 изменённых файлов: 10805 добавлений и 0 удалений

19
.eslintignore Normal file
Просмотреть файл

@ -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

118
.eslintrc.js Normal file
Просмотреть файл

@ -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": "^_"}],
}
};

4
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
node_modules
lib
jest
temp

43
jest.config.js Normal file
Просмотреть файл

@ -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",
],
};

26
just-task.js Normal file
Просмотреть файл

@ -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'));

9905
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

48
package.json Normal file
Просмотреть файл

@ -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&#58; 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 };

1
src/expandEvent/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export { expandEvent, ispe} from './expandEvents';

1
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export * from './expandEvent';

25
tsconfig.json Normal file
Просмотреть файл

@ -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"]
}