Merge branch 'pr/5176'

This commit is contained in:
Hugo Bernier 2024-07-14 22:01:07 -04:00
commit 38442a56f1
22 changed files with 13984 additions and 32139 deletions

View File

@ -1 +1 @@
v14.17.6
v12.13.0

View File

@ -115,6 +115,7 @@ Version|Date|Comments
1.0.16|December 21, 2021|Upgraded to SPFx 1.12.1
1.0.17|October 25, 2022|Fixed issue deleting events (#2693)
1.0.18|December 29, 2022|Fixed stylelint issue (#4029)| Cleaned up old Type script versions and Upgraded Type script version
1.0.19|June 28, 2024| added filter by category | fixed the packages.json issues which prevent solution to build successfully.
## Minimal Path to Awesome

View File

@ -9,7 +9,7 @@
"This Web Part allows you to manage events in a calendar. Uses a list of existing calendars on any website. The location and name of the list and the dates of the events to be displayed are defined in the properties of the web part."
],
"creationDateTime": "2020-12-04",
"updateDateTime": "2023-12-29",
"updateDateTime": "2024-06-28",
"products": [
"SharePoint"
],

View File

@ -3,7 +3,7 @@
"solution": {
"name": "react-calendar-client-side-solution",
"id": "3a13208b-3874-4036-9262-4edd22e88187",
"version": "1.0.18.0",
"version": "1.0.19.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false

View File

@ -1,10 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
"initialPage": "https://cotoso.sharepoint.com/_layouts/workbench.aspx"
}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@
"main": "lib/index.js",
"version": "1.0.12",
"private": true,
"engines": {
"node": ">=12.13.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
@ -23,39 +26,40 @@
"@pnp/spfx-property-controls": "1.14.1",
"@types/draft-js": "^0.10.30",
"@types/globalize": "0.0.34",
"@types/jquery": "^3.3.29",
"@types/jquery": "3.5.30",
"@types/react-big-calendar": "^0.24.6",
"@uifabric/fluent-theme": "^0.16.7",
"browserslist": "^4.12.0",
"browserslist": "4.23.1",
"draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.4",
"globalize": "^1.4.2",
"immutable": "^4.0.0-rc.12",
"jquery": "^3.5.0",
"moment": "^2.29.2",
"immutable": "4.3.6",
"jquery": "3.7.1",
"moment": "2.30.1",
"office-ui-fabric-react": "7.156.0",
"react": "16.9.0",
"react-big-calendar": "^0.24.6",
"react-dom": "16.9.0",
"react-draft-wysiwyg": "^1.13.2",
"react-draft-wysiwyg": "1.15.0",
"react-select": "^4.3.1",
"spfx-uifabric-themes": "^0.6.0",
"string-format": "^2.0.0",
"typescript": "^3.2.4",
"xml2js": "^0.5.0"
"typescript": "3.9.10",
"xml2js": "^0.4.19"
},
"resolutions": {
"@types/react": "16.7.22"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"@microsoft/sp-build-web": "1.18.2",
"@microsoft/sp-build-web": "1.12.1",
"@microsoft/sp-module-interfaces": "1.12.1",
"@microsoft/sp-tslint-rules": "1.12.1",
"@microsoft/sp-webpart-workbench": "1.12.1",
"@types/es6-promise": "0.0.33",
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@types/webpack-env": "1.13.1",
"@types/xml2js": "^0.4.4",
"@types/xml2js": "0.4.14",
"@voitanos/jest-preset-spfx-react16": "^1.1.0",
"ajv": "~5.2.2",
"gulp": "4.0.2",
@ -63,9 +67,12 @@
"gulp-stylelint": "^8.0.0",
"jest": "^23.6.0",
"karma-junit-reporter": "^1.2.0",
"spfx-uifabric-themes": "^0.6.0",
"tslint-microsoft-contrib": "6.2.0",
"webpack-bundle-analyzer": "^4.10.2"
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.5.4",
"tslint": "^6.1.3",
"tslint-microsoft-contrib": "^6.2.0",
"webpack-bundle-analyzer": "^3.1.0"
},
"solution": {
"developer": {

View File

@ -0,0 +1,73 @@
import * as strings from "CalendarWebPartStrings";
import { IDatePickerStrings } from "office-ui-fabric-react";
export const Constants = {
CategoryColumn: "Category",
MetaDataFieldType: "SP.Taxonomy.TaxonomyFieldValue",
EventResult_LocalStorage: "eventResult",
CalendarEventsWithLocalTime_LocalStorage: "calendarEventsWithLocalTime",
AndConditionStart: "<And>",
AndConditionEnd: "</And>",
OrConditionStart: "<Or>",
OrConditionEnd: "</Or>",
latitude: 58.485601,
longitude: 19.807854,
eventLayoutOverviewPageURL: `{0}/_layouts/15/Event.aspx?ListGuid={1}&ItemId={2}`
};
export const DayPickerStrings: IDatePickerStrings = {
months: [
strings.January,
strings.February,
strings.March,
strings.April,
strings.May,
strings.June,
strings.July,
strings.August,
strings.September,
strings.October,
strings.November,
strings.December,
],
shortMonths: [
strings.Jan,
strings.Feb,
strings.Mar,
strings.Apr,
strings.May,
strings.Jun,
strings.Jul,
strings.Aug,
strings.Sep,
strings.Oct,
strings.Nov,
strings.Dez,
],
days: [
strings.Sunday,
strings.Monday,
strings.Tuesday,
strings.Wednesday,
strings.Thursday,
strings.Friday,
strings.Saturday,
],
shortDays: [
strings.ShortDay_S,
strings.ShortDay_M,
strings.ShortDay_T,
strings.ShortDay_W,
strings.ShortDay_Thursday,
strings.ShortDay_Friday,
strings.ShortDay_Sunday,
],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
prevYearAriaLabel: strings.PrevYear,
nextYearAriaLabel: strings.NextYear,
closeButtonAriaLabel: strings.CloseDate,
isRequiredErrorMessage: strings.IsRequired,
invalidInputErrorMessage: strings.InvalidDateFormat,
};

View File

@ -0,0 +1,4 @@
export interface IOption {
key: string;
text: string;
}

View File

@ -10,6 +10,9 @@ import * as moment from 'moment';
import { SiteUser } from "@pnp/sp/src/siteusers";
import { IUserPermissions } from './IUserPermissions';
import parseRecurrentEvent from './parseRecurrentEvent';
import { IComboBoxOption } from '@fluentui/react';
import { Constants } from "../common/Constants";
import { Text } from "@microsoft/sp-core-library";
// Class Services
export default class spservices {
@ -450,7 +453,7 @@ export default class spservices {
* @returns {Promise< IEventData[]>}
* @memberof spservices
*/
public async getEvents(siteUrl: string, listId: string, eventStartDate: Date, eventEndDate: Date): Promise<IEventData[]> {
public async getEvents(siteUrl: string, listId: string, eventStartDate: Date, eventEndDate: Date, categories: IComboBoxOption[]): Promise<IEventData[]> {
let events: IEventData[] = [];
if (!siteUrl) {
@ -463,28 +466,13 @@ export default class spservices {
for (const cat of categoryDropdownOption) {
categoryColor.push({ category: cat.text, color: await this.colorGenerate() });
}
let camlQueryExpression = this.setUpQueryExpression(eventStartDate, eventEndDate, categories);
const web = new Web(siteUrl);
const results = await web.lists.getById(listId).usingCaching().renderListDataAsStream(
{
DatesInUtc: true,
ViewXml: `<View><ViewFields><FieldRef Name='RecurrenceData'/><FieldRef Name='Duration'/><FieldRef Name='Author'/><FieldRef Name='Category'/><FieldRef Name='Description'/><FieldRef Name='ParticipantsPicker'/><FieldRef Name='Geolocation'/><FieldRef Name='ID'/><FieldRef Name='EndDate'/><FieldRef Name='EventDate'/><FieldRef Name='ID'/><FieldRef Name='Location'/><FieldRef Name='Title'/><FieldRef Name='fAllDayEvent'/><FieldRef Name='EventType'/><FieldRef Name='UID' /><FieldRef Name='fRecurrence' /></ViewFields>
<Query>
<Where>
<And>
<Geq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventStartDate).format('YYYY-MM-DD')}</Value>
</Geq>
<Leq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format('YYYY-MM-DD')}</Value>
</Leq>
</And>
</Where>
</Query>
<RowLimit Paged=\"FALSE\">2000</RowLimit>
</View>`
ViewXml: camlQueryExpression
}
);
@ -571,6 +559,86 @@ export default class spservices {
}
}
/**
*
* @private
* @param {Date} eventStartDate
* @param {Date} eventEndDate
* @param {IOption[]} departments
* @returns {string} camlQuery
* @memberof spservices
*/
private setUpQueryExpression(
eventStartDate: Date,
eventEndDate: Date,
categories: IComboBoxOption[]
) {
let camlQuery = `
<View>
<ViewFields>
<FieldRef Name='RecurrenceData'/>
<FieldRef Name='Duration'/>
<FieldRef Name='Author'/>
<FieldRef Name='Category'/>
<FieldRef Name='Description'/>
<FieldRef Name='ParticipantsPicker'/>
<FieldRef Name='Geolocation'/>
<FieldRef Name='ID'/>
<FieldRef Name='EndDate'/>
<FieldRef Name='EventDate'/>
<FieldRef Name='Location'/>
<FieldRef Name='Title'/>
<FieldRef Name='fAllDayEvent'/>
<FieldRef Name='EventType'/>
<FieldRef Name='UID' />
<FieldRef Name='fRecurrence' />
</ViewFields>
<Query>
<Where>
<And>
<Geq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventStartDate).format("YYYY-MM-DD")}</Value>
</Geq>
{0}
<Leq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format("YYYY-MM-DD")}</Value>
</Leq>
{1}
{2}
</And>
</Where>
</Query>
<RowLimit Paged=\"FALSE\">2000</RowLimit>
</View>`;
let categoryCondition = `
<Eq>
<FieldRef Name='Category' />
<Value Type='Choice'>{0}</Value>
</Eq>`;
const deptsLength: number = categories.length;
let queryResult: string = "";
if (deptsLength > 0) {
if (deptsLength == 1) {
return Text.format(camlQuery, Constants.AndConditionStart, Text.format(categoryCondition, categories[0].key), Constants.AndConditionEnd);
} else {
let orCondition: string = `${Constants.OrConditionStart}{0}{1}${Constants.OrConditionEnd}`;
queryResult = Text.format(orCondition, Text.format(categoryCondition, categories[0].key), Text.format(categoryCondition, categories[1]));
for (let i = 2; i < categories.length; i++) {
const category = categories[i];
queryResult = Text.format(orCondition, Text.format(categoryCondition, category.key), queryResult);
}
}
return Text.format(camlQuery, Constants.AndConditionStart, queryResult, Constants.AndConditionEnd);
}
return Text.format(camlQuery, "", queryResult, "");
}
/**
*
* @private

View File

@ -11,8 +11,8 @@ import {
} from '@microsoft/sp-property-pane';
import * as strings from 'CalendarWebPartStrings';
import Calendar from './components/Calendar';
import { ICalendarProps } from './components/ICalendarProps';
import Calendar from './components/Calendar/Calendar';
import { ICalendarProps } from './components/Calendar/ICalendarProps';
import { PropertyFieldDateTimePicker, DateConvention, TimeConvention, IDateTimeFieldValue } from '@pnp/spfx-property-controls/lib/PropertyFieldDateTimePicker';
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";

View File

@ -1,7 +1,7 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme.vars";
@import "../../themes";
@import "../../../themes";
:export {
/* stylelint-disable property-case */

View File

@ -8,7 +8,7 @@ import * as strings from 'CalendarWebPartStrings';
import 'react-big-calendar/lib/css/react-big-calendar.css';
require('./calendar.css');
import { CommunicationColors, FluentCustomizations, FluentTheme } from '@uifabric/fluent-theme';
import Year from './Year';
import Year from '../Year/Year';
import { Calendar as MyCalendar, momentLocalizer } from 'react-big-calendar';
@ -36,22 +36,23 @@ import {
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
MessageBarType
} from 'office-ui-fabric-react';
import { IComboBoxOption, SelectableOptionMenuItemType } from '@fluentui/react';
import { EnvironmentType } from '@microsoft/sp-core-library';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from '@microsoft/sp-core-library';
import spservices from '../../../services/spservices';
import spservices from '../../../../services/spservices';
import { stringIsNullOrEmpty } from '@pnp/common';
import { Event } from '../../../controls/Event/event';
import { IPanelModelEnum } from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
import { IUserPermissions } from './../../../services/IUserPermissions';
import { Event } from '../../../../controls/Event/event';
import { IPanelModelEnum } from '../../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../../services/IEventData';
import { IUserPermissions } from './../../../../services/IUserPermissions';
import Category from '../Category/Category';
import { IOption } from '../../../../services/IOption';
//const localizer = BigCalendar.momentLocalizer(moment);
@ -64,11 +65,14 @@ const localizer = momentLocalizer(moment);
export default class Calendar extends React.Component<ICalendarProps, ICalendarState> {
private spService: spservices = null;
private userListPermissions: IUserPermissions = undefined;
public constructor(props) {
super(props);
this.state = {
showDialog: false,
categories: [],
selectedCategories: [],
eventData: [],
selectedEvent: undefined,
isloading: true,
@ -79,9 +83,29 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
this.onDismissPanel = this.onDismissPanel.bind(this);
this.onSelectEvent = this.onSelectEvent.bind(this);
this.onSelectSlot = this.onSelectSlot.bind(this);
this.onChangeCategories = this.onChangeCategories.bind(this);
this.spService = new spservices(this.props.context);
moment.locale(this.props.context.pageContext.cultureInfo.currentUICultureName);
}
/**
* @private
* @param {*} selectedCatogries
* @memberof Calendar
*/
private async onChangeCategories(selectedCategories: IComboBoxOption[]) {
try {
this.setState({
selectedCategories: selectedCategories
});
await this.loadEvents();
} catch (error) {
this.setState({
hasError: true,
errorMessage: error.message,
isloading: false,
});
}
}
private onDocumentCardClick(ev: React.SyntheticEvent<HTMLElement, Event>) {
@ -118,12 +142,20 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
*/
private async loadEvents() {
try {
// Teste Properties
if (!this.props.list || !this.props.siteUrl || !this.props.eventStartDate.value || !this.props.eventEndDate.value) return;
this.userListPermissions = await this.spService.getUserPermissions(this.props.siteUrl, this.props.list);
const eventsData: IEventData[] = await this.spService.getEvents(escape(this.props.siteUrl), escape(this.props.list), this.props.eventStartDate.value, this.props.eventEndDate.value);
let eventsData: IEventData[] = [];
if (this.state.selectedCategories.length > 0) {
eventsData = await this.spService.getEvents(
escape(this.props.siteUrl),
escape(this.props.list),
this.props.eventStartDate.value,
this.props.eventEndDate.value,
this.state.selectedCategories
);
}
this.setState({ eventData: eventsData, hasError: false, errorMessage: "" });
} catch (error) {
@ -134,7 +166,9 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
* @memberof Calendar
*/
public async componentDidMount() {
this.setState({ isloading: true });
const categories: IOption[] = await this.spService.getChoiceFieldOptions(this.props.siteUrl, this.props.list, 'Category');
this.setState({ isloading: true, categories: categories, selectedCategories: categories});
await this.loadEvents();
this.setState({ isloading: false });
}
@ -296,7 +330,6 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
};
}
/**
*
* @param {*} date
@ -342,6 +375,11 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
<div>
{this.state.isloading ? <Spinner size={SpinnerSize.large} label={strings.LoadingEventsLabel} /> :
<div className={styles.container}>
<Category
catogries={this.state.categories}
selectedCategories={this.state.selectedCategories}
onChangeCategories={this.onChangeCategories}
></Category>
<MyCalendar
dayPropGetter={this.dayPropGetter}
localizer={localizer}

View File

@ -0,0 +1,16 @@
import { IPanelModelEnum} from '../../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../../services/IEventData';
import { IComboBoxOption } from '@fluentui/react';
export interface ICalendarState {
showDialog: boolean;
categories: IComboBoxOption[];
selectedCategories: IComboBoxOption[];
eventData: IEventData[];
selectedEvent: IEventData;
panelMode?: IPanelModelEnum;
startDateSlot?: Date;
endDateSlot?:Date;
isloading: boolean;
hasError: boolean;
errorMessage: string;
}

View File

@ -0,0 +1,80 @@
import React from 'react';
import { ICategoryProps, ICategoryState } from "./ICategory";
import { ComboBox, IComboBoxOption, SelectableOptionMenuItemType } from '@fluentui/react';
export default class Category extends React.PureComponent<ICategoryProps, ICategoryState> {
private selectableOptions = this.props.catogries.filter(
option =>
(option.itemType === SelectableOptionMenuItemType.Normal || option.itemType === undefined) && !option.disabled,
);
public constructor(props) {
super(props);
this.state = {
selectedKeys: this.props.selectedCategories.length == 0 ?
[String('selectAll'), ...this.props.catogries.map(o => o.key as string)] :
[String('selectAll'), ...this.props.selectedCategories.map(o => o.key as string)]
};
}
private onChange = (event, option, index, value) => {
const selected = option?.selected;
const { selectedKeys } = this.state;
const currentSelectedOptionKeys = selectedKeys.filter(key => key !== 'selectAll');
const selectAllState = currentSelectedOptionKeys.length === this.selectableOptions.length;
if (option) {
if (option?.itemType === SelectableOptionMenuItemType.SelectAll) {
selectAllState
? this.setState({ selectedKeys: [] }, () => { this.updateSelectableCategories(); })
: this.setState({ selectedKeys: ['selectAll', ...this.selectableOptions.map(o => o.key as string)] }, () => { this.updateSelectableCategories(); });
}
else {
const updatedKeys = selected
? [...currentSelectedOptionKeys, option!.key as string]
: currentSelectedOptionKeys.filter(k => k !== option.key);
if (updatedKeys.length === this.selectableOptions.length) {
updatedKeys.push('selectAll');
}
this.setState({ selectedKeys: updatedKeys, }, () => { this.updateSelectableCategories(); });
}
}
}
private updateSelectableCategories() {
const currentSelectedCategories: IComboBoxOption[] = [];
if (this.state.selectedKeys.length >= 0) {
this.state.selectedKeys.forEach(key => {
const category: IComboBoxOption[] = this.selectableOptions.filter(opt => opt.key === key);
if (category.length > 0) {
currentSelectedCategories.push(category[0]);
}
});
}
this.props.onChangeCategories(currentSelectedCategories);
}
public render(): React.ReactElement {
return (
<div>
<ComboBox
label="Select Category"
multiSelect
options={[
{ key: 'selectAll', text: 'Select All', itemType: SelectableOptionMenuItemType.SelectAll },
...this.props.catogries
]}
selectedKey={this.state.selectedKeys}
onChange={this.onChange}
/>
</div>
);
}
}

View File

@ -0,0 +1,10 @@
import { IComboBoxOption } from '@fluentui/react';
export interface ICategoryProps {
catogries: IComboBoxOption[];
selectedCategories: IComboBoxOption[];
onChangeCategories: (onChangeCategories: IComboBoxOption[]) => void;
}
export interface ICategoryState {
selectedKeys: string[];
}

View File

@ -1,13 +0,0 @@
import { IPanelModelEnum} from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
export interface ICalendarState {
showDialog: boolean;
eventData: IEventData[];
selectedEvent: IEventData;
panelMode?: IPanelModelEnum;
startDateSlot?: Date;
endDateSlot?:Date;
isloading: boolean;
hasError: boolean;
errorMessage: string;
}

View File

@ -14,13 +14,14 @@
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"allowSyntheticDefaultImports":true,
"allowSyntheticDefaultImports": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
"webpack-env",
"@types/jest"
],
"lib": [
"es5",