Коммит
b1cd6667bd
|
@ -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
|
12
README.md
12
README.md
|
@ -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.
|
20
SUPPORT.md
20
SUPPORT.md
|
@ -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.
|
||||
|
|
|
@ -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"]
|
||||
}
|
Загрузка…
Ссылка в новой задаче