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

Merge pull request #1 from microsoft/initial

Initial PR
This commit is contained in:
Ben 2021-03-08 12:20:48 -08:00 коммит произвёл GitHub
Родитель 773c5e3049 be043566e2
Коммит b1cd6667bd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 10818 добавлений и 19 удалений

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

Просмотреть файл

@ -1,11 +1,15 @@
# Project
# sp-recurring-events
The purpose of this package is to help us work with recurring events in SharePoint. This is necessary because the SharePoint REST API endpoint does not expand events for us, but instead returns events with an XML based recurrence information. This library takes that recurrence information and expands it to a list of events.
Ideally, eventually this project will also allow for the reverse: creating recurring events with that same recurrence structure but in an easier to use API that will convert or build the XML structure for the user.
> Project
> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
As the maintainer of this project, please make a few updates:
- Improving this README.MD file to provide a great experience
- Updating SUPPORT.MD with content about this project's support experience
- Understanding the security reporting process in SECURITY.MD
- Remove this section from the README
@ -31,3 +35,7 @@ trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.
## Telemetry Notice
This package does not collect any telemetry for Microsoft or any other organization. Therefore, there is nothing to turn off.

Просмотреть файл

@ -1,25 +1,11 @@
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
# Support
## How to file issues and get help
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue.
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
For help and questions about using this project, please raise an issue.
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
Support for this project is limited to the resources listed above.

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