This commit is contained in:
Mrigango 2024-09-27 12:27:46 +00:00 committed by GitHub
commit b21fe189e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
142 changed files with 75600 additions and 55174 deletions

View File

@ -0,0 +1 @@
v16.15.1

View File

@ -4,6 +4,26 @@
This sample is the source code for the Rhythm of Business Calendar app and is intended to demonstrate patterns and practices for building enterprise apps on the SharePoint platform.
This application requires below Graph Api Permissions-
# Send Approval notifications to approvers over teams in personal chat
1. Chat.Create - It is required for creating the chat and getting the chat id for sending an adaptive card to the approver.
2. ChatMessage.Send - It is required for sending the adaptive card (with @mention activity feed) to all the approvers whenever any event is created with approval rule applied for any refiner.
# Share event details to teams channel where the app is installed
1. ChannelMessage.Send - It is required for sharing the event details on click of "Share" button into the same teams channel in which the app is added.
Note: Sharing events details to teams channel feature will be disbled if the webpart is installed on a SharePoint page.
### versions
node v16.15.1
npm 8.13.2
spfx 1.15.0
TypeScript 4.5
<!-- TODO: link to the app once published
This sample is the source code for the Rhythm of Business Calendar app published in [AppSource](https://appsource.microsoft.com/en-us/marketplace/apps?product=sharepoint) and is intended to demonstrate patterns and practices for building enterprise apps on the SharePoint platform.
-->
@ -22,15 +42,15 @@ Edit refiner
## Compatibility
| :warning: Important |
|:---------------------------|
| Every SPFx version is only compatible with specific version(s) of Node.js. In order to be able to build this sample, please ensure that the version of Node on your workstation matches one of the versions listed in this section. This sample will not work on a different version of Node.|
|Refer to <https://aka.ms/spfx-matrix> for more information on SPFx compatibility. |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Every SPFx version is only compatible with specific version(s) of Node.js. In order to be able to build this sample, please ensure that the version of Node on your workstation matches one of the versions listed in this section. This sample will not work on a different version of Node. |
| Refer to <https://aka.ms/spfx-matrix> for more information on SPFx compatibility. |
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.15-green.svg)
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Does not work with SharePoint 2016 (Feature Pack 2)](<https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg> "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
@ -38,29 +58,30 @@ Edit refiner
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
- [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
- [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](https://aka.ms/m365/devprogram)
## Contributors
* [Dan Turley](https://github.com/d-turley)
- [Dan Turley](https://github.com/d-turley)
- Co-authored-by [Mrigango Deb](https://github.com/Mrigango)
## Version history
Version|Date|Comments
-------|----|--------
1.0|September 26, 2022|Initial release
| Version | Date | Comments |
| ------- | ------------------ | ------------------- |
| 1.0 | September 26, 2022 | Initial release |
| 5.0.1 | September 16, 2024 | Enhancement release |
## Minimal path to awesome
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-rhythm-of-business-calendar) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`react-rhythm-of-business-calendar`, located under `samples`)
* in the command line run:
* `npm install`
* `gulp serve --nobrowser`
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-rhythm-of-business-calendar) then unzip it)
- From your command line, change your current directory to the directory containing this sample (`react-rhythm-of-business-calendar`, located under `samples`)
- in the command line run:
- `npm install`
- `gulp serve --nobrowser`
<!--
TODO: add support for containers
@ -72,17 +93,17 @@ TODO: add support for containers
This sample is a complete app that demonstrates the "SPFx Solution Accelerator" framework, along with patterns and practices for building enterprise-class apps on SharePoint. Inspired by Domain Driven Design and Onion Architecture, this accelerator has evolved since SPFx v1.0, and we want to share it with the world!
At a high-level, the accelerator includes the following features:
* Prescribed [solution structure](./documentation/solution-structure.md) separates web parts, components, model, services, and schema (data) layers
* Robust [entity domain model](./documentation/entities.md) with relationships, validation, change tracking, and text search
* Robust [schema provisioning](./documentation/schema.md) and versioning; use SharePoint lists as a simple relational database
* [Services](./documentation/services.md) for interacting with SharePoint, timezones, domain isolation, and users and groups, plus patterns for building custom services for app-specific logic
* [Component library](./documentation/components.md) with customizable wizard, panel/dialog for quickly building view/edit screens, validation, and more
* [Live Update](./documentation/live-update.md) feature ensures users are always working with the latest data without manaually reloading the page
* Built on the latest SPFx with TypeScript, React, and Fluent UI, plus PnPjs, Moment.js, Lodash, and Jest
- Prescribed [solution structure](./documentation/solution-structure.md) separates web parts, components, model, services, and schema (data) layers
- Robust [entity domain model](./documentation/entities.md) with relationships, validation, change tracking, and text search
- Robust [schema provisioning](./documentation/schema.md) and versioning; use SharePoint lists as a simple relational database
- [Services](./documentation/services.md) for interacting with SharePoint, timezones, domain isolation, and users and groups, plus patterns for building custom services for app-specific logic
- [Component library](./documentation/components.md) with customizable wizard, panel/dialog for quickly building view/edit screens, validation, and more
- [Live Update](./documentation/live-update.md) feature ensures users are always working with the latest data without manaually reloading the page
- Built on the latest SPFx with TypeScript, React, and Fluent UI, plus PnPjs, Moment.js, Lodash, and Jest
A deep dive into the various features of the accelerator can be found in the [documentation](./documentation/README.md) folder.
<!--
RESERVED FOR REPO MAINTAINERS
@ -111,6 +132,6 @@ Finally, if you have an idea for improvement, [make a suggestion](https://github
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
<img src="https://m365-visitor-stats.azurewebsites.net/sp-dev-fx-webparts/samples/react-rhythm-of-business-calendar" />

View File

@ -11,10 +11,11 @@
]
}
},
"externals": { },
"externals": {},
"localizedResources": {
"CommonStrings": "lib/common/components/loc/{locale}.js",
"ComponentStrings": "lib/components/loc/{locale}.js",
"RhythmOfBusinessCalendarWebPartStrings": "lib/webparts/rhythmOfBusinessCalendar/loc/{locale}.js"
"RhythmOfBusinessCalendarWebPartStrings": "lib/webparts/rhythmOfBusinessCalendar/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -1,11 +1,25 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "LOCAL Rhythm of Business Calendar",
"name": "Rhythm of Business Calendar",
"id": "37df9a1c-b53e-46ad-9efb-2e4da77a724f",
"version": "1.0.0.0",
"version": "5.0.2.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "ChatMessage.Send"
},
{
"resource": "Microsoft Graph",
"scope": "Chat.Create"
},
{
"resource": "Microsoft Graph",
"scope": "ChannelMessage.Send"
}
],
"developer": {
"name": "",
"websiteUrl": "",
@ -15,6 +29,6 @@
}
},
"paths": {
"zippedPackage": "solution/RhythmOfBusinessCalendar-LOCAL.sppkg"
"zippedPackage": "solution/RhythmOfBusinessCalendar.sppkg"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,29 @@
{
"name": "rhythm-of-business-calendar",
"version": "1.0.0",
"version": "5.0.2",
"main": "lib/index.js",
"private": true,
"scripts": {
"refreshVSToken": "vsts-npm-auth -config .npmrc",
"build": "gulp bundle",
"clean": "gulp clean",
"deploy": "gulp deploy",
"package": "gulp package",
"test": "./node_modules/.bin/jest"
"test": "./node_modules/.bin/jest",
"preinstall": "npx npm-force-resolutions"
},
"resolutions": {
"@types/react": "~16.9.51"
"@types/react": "16.9.56",
"loader-utils": "2.0.4",
"json5": "2.2.2",
"nanoid": "3.1.31",
"fast-xml-parser": "4.4.1",
"@babel/traverse": "7.23.2",
"browserify-sign":"4.2.2",
"jszip":"3.8.0",
"braces":"3.0.3",
"fast-loops": "1.1.4",
"semver": "7.5.2"
},
"dependencies": {
"@fluentui/react": "^8.77.2",
@ -29,26 +41,32 @@
"@pnp/logging": "^2.13.0",
"@pnp/odata": "^2.13.0",
"@pnp/sp": "^2.13.0",
"@pnp/spfx-controls-react": "^3.13.0",
"compressed-json": "^1.0.16",
"eslint": "^7.32.0",
"he": "^1.2.0",
"hoist-non-react-statics": "^3.3.0",
"html2canvas": "^1.4.1",
"react-export-table-to-excel": "^1.0.6",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.35",
"office-ui-fabric-react": "7.185.7",
"pptxgenjs": "^3.12.0",
"react": "~16.13.1",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "~16.13.1",
"react-router-dom": "^6.3.0",
"sanitize-html": "^2.12.1",
"swiped-events": "^1.1.6"
"sanitize-html": "^2.7.1",
"swiped-events": "^1.1.6",
"typescript": "^4.5.2"
},
"devDependencies": {
"@microsoft/eslint-config-spfx": "^1.15.2",
"@microsoft/eslint-plugin-spfx": "^1.15.2",
"@microsoft/gulp-core-build": "3.17.19",
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
"@microsoft/sp-build-core-tasks": "^1.20.1",
"@microsoft/sp-build-web": "^1.20.1",
"@microsoft/sp-build-core-tasks": "^1.15.2",
"@microsoft/sp-build-web": "^1.15.2",
"@microsoft/sp-module-interfaces": "^1.15.2",
"@rushstack/eslint-config": "2.5.1",
"@testing-library/jest-dom": "^5.14.1",
@ -72,7 +90,7 @@
"gulp-zip": "^5.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"json5": "^2.2.2",
"json5": "^2.2.0",
"raf": "^3.4.1",
"react-test-renderer": "~16.13.1",
"stateful-process-command-proxy": "^1.0.1",
@ -81,4 +99,5 @@
"engines": {
"node": ">=16.15.1"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

View File

@ -1,9 +1,32 @@
import { Moment, unitOfTime } from "moment-timezone";
import { useTimeZoneService } from "./services";
import moment from "moment";
export class MomentRange {
public start: Moment;
public end: Moment;
public static overlaps = (range1: MomentRange, range2: MomentRange, units: unitOfTime.StartOf = 'day'): boolean =>
!range1.start.isAfter(range2.end) && !range1.end.isBefore(range2.start, units)
public static overlaps = (range1: MomentRange, range2: MomentRange, units: unitOfTime.StartOf = 'minutes'): boolean => {
const _range1 = moment(range1.start);
const _range2 = moment(range2.start);
const timeZone_range1 = _range1.tz();
const timeZone_range2 = _range2.tz();
if (timeZone_range1 !== timeZone_range2) {
return !range1.start.isAfter(range2.end, units) && !range1.end.isBefore(range2.start, units);
}
else{
return !range1.start.isAfter(range2.end, units) && !range1.end.isBefore(range2.start, units);
}
}
}

View File

@ -46,7 +46,7 @@ export const CalendarPicker: FC<IProps> = ({
return (
<span ref={buttonRef}>
<ActionButton iconProps={iconProps} disabled={disabled} onClick={toggleCalendar}>
<ActionButton className="btnDateLabel" iconProps={iconProps} disabled={disabled} onClick={toggleCalendar}>
{buttonLabel}
</ActionButton>
{showCalendar &&

View File

@ -1,7 +1,7 @@
import React, { CSSProperties, FC, ReactNode } from 'react';
import { TooltipHost, ITooltipHostProps, Text } from '@fluentui/react';
import { InfoIcon } from '@fluentui/react-icons-mdl2';
import styles from "./styles/LiveTextField.module.scss";
const infoIconStyle: CSSProperties = {
fontSize: 12,
marginLeft: 4
@ -12,15 +12,19 @@ interface IProps extends ITooltipHostProps {
hideIcon?: boolean;
tooltipHostProps?: ITooltipHostProps;
children: ReactNode;
isCssClassName?: boolean;
}
export const InfoTooltip: FC<IProps> = ({
text,
hideIcon = false,
tooltipHostProps,
children
children,
isCssClassName = false
}: IProps) =>
<TooltipHost {...tooltipHostProps} content={text}>
<span className={isCssClassName ? styles.infoLabelStyle : ''}>
{children}
{text && !hideIcon && <Text><InfoIcon style={infoIconStyle} tabIndex={0} /></Text>}
</span>
</TooltipHost>

View File

@ -1,7 +1,7 @@
import moment, { Moment } from 'moment-timezone';
import React, { useCallback } from 'react';
import { DatePicker, IDatePickerProps, Label, Stack } from '@fluentui/react';
import { ValidationRule, PropsOfType } from 'common';
import { ValidationRule, PropsOfType, now } from 'common';
import { ListItemEntity } from 'common/sharepoint';
import LiveUpdate from './LiveUpdate';
import { getCurrentValue, LiveType, setValue } from './LiveUtils';
@ -33,6 +33,27 @@ const LiveDatePicker = <E extends ListItemEntity<any>, P extends PropsOfType<E,
} = props;
const value = getCurrentValue(entity, propertyName) as T;
//const Datenow = moment(now()).tz(value.tz()).format();
const date1String = value && value.toString();
const date2String = value && value.toDate().toString();
// Create Moment objects with Moment Timezone
const date1 = date1String && moment.tz(date1String, 'ddd MMM DD YYYY HH:mm:ss [GMT]Z');
const date2 = date2String && moment.tz(date2String, 'ddd MMM DD YYYY HH:mm:ss [GMT]Z');
let totalTime = 0;
// Calculate the time difference
if(date1 && date2){
const duration = moment.duration(date1.diff(date2));
// Get the difference in hours, minutes, and seconds
const hours = duration && duration.hours();
const minutes = duration && duration.minutes();
const seconds = duration && duration.seconds();
totalTime= (hours * 3600 + minutes*60 + seconds)
}
const updateValue = useCallback((val: LiveType<E, P>) => updateField(e => setValue(e, propertyName, val)), [updateField, propertyName]);
const renderValue = useCallback((val: LiveType<E, P>) => <span>{(val as DataType)?.isValid() ? (val as DataType).format('dddd, MMMM DD, YYYY') : ''}</span>, []);
const formatDate = useCallback((val: Date) => formatMoment(moment(val)), [formatMoment]);
@ -52,7 +73,7 @@ const LiveDatePicker = <E extends ListItemEntity<any>, P extends PropsOfType<E,
ariaLabel={ariaLabel}
isRequired={!label && required}
formatDate={formatDate}
value={value?.isValid() && value?.toDate()}
value={value?.isValid() && totalTime > 0 ? value && value.add(totalTime,'seconds').toDate() : value && value.add(totalTime,'seconds').toDate() }
onSelectDate={onChange}
/>
{!label && renderLiveUpdateMark()}

View File

@ -0,0 +1,120 @@
import React, { useCallback } from "react";
import { ITextFieldProps, Label } from "@fluentui/react";
import { ValidationRule, PropsOfType } from "common";
import { ListItemEntity } from "common/sharepoint";
import LiveUpdate from "./LiveUpdate";
import { getCurrentValue, LiveType, setValue } from "./LiveUtils";
import { Validation } from "./Validation";
import { RichText } from "@pnp/spfx-controls-react/lib/RichText";
type DataType = string | number;
interface IConverter<T> {
parse: (val: string) => T;
toString: (val: T) => string;
}
class NonConverter implements IConverter<string> {
public parse(val: string) {
return val;
}
public toString(val: string) {
return val;
}
}
interface IProps<
E extends ListItemEntity<any>,
P extends PropsOfType<E, DataType>
> extends ITextFieldProps {
entity: E;
propertyName: P;
updateField: (update: (data: E) => void, callback?: () => any) => void;
converter?: IConverter<LiveType<E, P>>;
rules?: ValidationRule<E>[];
showValidationFeedback?: boolean;
liveUpdateMarkClassName?: string;
tooltip?: string;
nextFocusComponent?: (assignFocus: boolean) => void;
}
const LiveMultiTextField = <
E extends ListItemEntity<any>,
P extends PropsOfType<E, DataType>
>(
props: IProps<E, P>
) => {
const {
entity,
propertyName,
converter = new NonConverter() as unknown as IConverter<LiveType<E, P>>,
rules,
showValidationFeedback,
label,
liveUpdateMarkClassName,
updateField,
nextFocusComponent,
} = props;
const value = converter.toString(getCurrentValue(entity, propertyName));
const updateValue = useCallback(
(val: LiveType<E, P>) =>
updateField((e) => setValue(e, propertyName, val)),
[updateField, propertyName]
);
const renderValue = useCallback(
(val: LiveType<E, P>) => (
<>{(converter ? converter.toString(val) : val) || "-"}</>
),
[converter]
);
const onChangeNew = useCallback(
(ev, val) => {
updateField((e) => {
setValue(
e,
propertyName,
converter
? converter.parse(val)
: (val as unknown as LiveType<E, P>)
);
});
},
[updateField, propertyName, converter]
);
const onChange = (val: string) => {
onChangeNew(entity, val);
if (val.endsWith("\t</p>")) nextFocusComponent(true);
return val;
};
return (
<Validation
entity={entity}
rules={rules}
active={showValidationFeedback}
>
<LiveUpdate
entity={entity}
propertyName={propertyName}
updateValue={updateValue}
renderValue={renderValue}
>
{(renderLiveUpdateMark) => (
<>
<Label aria-label={label}>{label}</Label>
<RichText value={value} onChange={onChange} />
{!label &&
renderLiveUpdateMark({
className: liveUpdateMarkClassName,
})}
</>
)}
</LiveUpdate>
</Validation>
);
};
export default LiveMultiTextField;

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { CSSProperties, useCallback } from 'react';
import { ITextFieldProps, TextField, Stack } from '@fluentui/react';
import { ValidationRule, PropsOfType } from 'common';
import { ListItemEntity } from 'common/sharepoint';
@ -6,7 +6,7 @@ import { InfoTooltip } from './InfoTooltip';
import LiveUpdate from './LiveUpdate';
import { getCurrentValue, LiveType, setValue } from './LiveUtils';
import { Validation } from './Validation';
import styles from "./styles/LiveTextField.module.scss";
type DataType = string | number;
interface IConverter<T> {
@ -27,12 +27,14 @@ interface IProps<E extends ListItemEntity<any>, P extends PropsOfType<E, DataTyp
showValidationFeedback?: boolean;
liveUpdateMarkClassName?: string;
tooltip?: string;
isCssClassName?: boolean;
updateField: (update: (data: E) => void, callback?: () => any) => void;
}
const LiveTextField = <E extends ListItemEntity<any>, P extends PropsOfType<E, DataType>>(props: IProps<E, P>) => {
const {
entity,
isCssClassName = false,
propertyName,
converter = new NonConverter() as unknown as IConverter<LiveType<E, P>>,
rules,
@ -59,7 +61,7 @@ const LiveTextField = <E extends ListItemEntity<any>, P extends PropsOfType<E, D
ariaLabel={ariaLabel}
onRenderLabel={(textFieldProps, defaultRender) => {
return label && <Stack horizontal>
<InfoTooltip text={tooltip}>{defaultRender(textFieldProps)}</InfoTooltip>
<InfoTooltip text={tooltip} isCssClassName={isCssClassName}>{defaultRender(textFieldProps)}</InfoTooltip>
{renderLiveUpdateMark({ className: liveUpdateMarkClassName })}
</Stack>;
}}

View File

@ -1,19 +1,22 @@
import { last } from 'lodash';
import React, { useCallback } from 'react';
import { ILabelStyles, Label, Stack } from '@fluentui/react';
import { PropsOfType, ValidationRule, User } from 'common';
import { ListItemEntity } from 'common/sharepoint';
import { InfoTooltip } from './InfoTooltip';
import LiveUpdate from './LiveUpdate';
import { getCurrentValue, LiveType, setValue } from './LiveUtils';
import { Validation } from './Validation';
import UserPicker, { IUserPickerProps } from './UserPicker';
import { last } from "lodash";
import React, { useCallback } from "react";
import { ILabelStyles, Label, Stack } from "@fluentui/react";
import { PropsOfType, ValidationRule, User } from "common";
import { ListItemEntity } from "common/sharepoint";
import { InfoTooltip } from "./InfoTooltip";
import LiveUpdate from "./LiveUpdate";
import { getCurrentValue, LiveType, setValue } from "./LiveUtils";
import { Validation } from "./Validation";
import UserPicker, { IUserPickerProps } from "./UserPicker";
const labelStyles: ILabelStyles = {
root: { display: 'inline-block' }
root: { display: "inline-block" },
};
interface IProps<E extends ListItemEntity<any>, P extends PropsOfType<E, User> | PropsOfType<E, User[]>> extends Omit<IUserPickerProps, 'users' | 'onChanged'> {
interface IProps<
E extends ListItemEntity<any>,
P extends PropsOfType<E, User> | PropsOfType<E, User[]>
> extends Omit<IUserPickerProps, "users" | "onChanged"> {
entity: E;
propertyName: P;
rules?: ValidationRule<E>[];
@ -22,10 +25,16 @@ interface IProps<E extends ListItemEntity<any>, P extends PropsOfType<E, User> |
tooltip?: string;
required?: boolean;
onUsersChanging?: (users: User[]) => User[];
setComponentRef?: boolean;
updateField: (update: (data: E) => void, callback?: () => any) => void;
}
const LiveUserPicker = <E extends ListItemEntity<any>, P extends PropsOfType<E, User> | PropsOfType<E, User[]>>(props: IProps<E, P>) => {
const LiveUserPicker = <
E extends ListItemEntity<any>,
P extends PropsOfType<E, User> | PropsOfType<E, User[]>
>(
props: IProps<E, P>
) => {
const {
entity,
propertyName,
@ -34,28 +43,76 @@ const LiveUserPicker = <E extends ListItemEntity<any>, P extends PropsOfType<E,
label,
tooltip,
required,
onUsersChanging = users => users,
updateField
onUsersChanging = (users) => users,
updateField,
setComponentRef,
} = props;
const value = getCurrentValue(entity, propertyName) as (User | User[]);
const updateValue = useCallback((val: LiveType<E, P>) => updateField(e => setValue(e, propertyName, val)), [updateField, propertyName]);
const renderValue = useCallback((val: LiveType<E, P>) => Array.isArray(val) ? (val as User[]).map((v, idx) => <span key={idx}>{idx > 0 ? '; ' : ''}{v.title}</span>) : (val as User)?.title || '', []);
const onChanged = useCallback((users: User[]) => {
const value = getCurrentValue(entity, propertyName) as User | User[];
const updateValue = useCallback(
(val: LiveType<E, P>) =>
updateField((e) => setValue(e, propertyName, val)),
[updateField, propertyName]
);
const renderValue = useCallback(
(val: LiveType<E, P>) =>
Array.isArray(val)
? (val as User[]).map((v, idx) => (
<span key={idx}>
{idx > 0 ? "; " : ""}
{v.title}
</span>
))
: (val as User)?.title || "",
[]
);
const onChanged = useCallback(
(users: User[]) => {
users = onUsersChanging(users);
updateField(e => setValue(e, propertyName, (Array.isArray(value) ? users : last(users)) as LiveType<E, P>));
}, [onUsersChanging, updateField, propertyName, value]);
updateField((e) =>
setValue(
e,
propertyName,
(Array.isArray(value) ? users : last(users)) as LiveType<
E,
P
>
)
);
},
[onUsersChanging, updateField, propertyName, value]
);
return (
<Validation entity={entity} rules={rules} active={showValidationFeedback}>
<LiveUpdate entity={entity} propertyName={propertyName} updateValue={updateValue} renderValue={renderValue}>
{(renderLiveUpdateMark) => <>
{label && <Stack horizontal>
<InfoTooltip text={tooltip}><Label required={required} styles={labelStyles}>{label}</Label></InfoTooltip>
<Validation
entity={entity}
rules={rules}
active={showValidationFeedback}
>
<LiveUpdate
entity={entity}
propertyName={propertyName}
updateValue={updateValue}
renderValue={renderValue}
>
{(renderLiveUpdateMark) => (
<>
{label && (
<Stack horizontal>
<InfoTooltip text={tooltip}>
<Label
required={required}
styles={labelStyles}
>
{label}
</Label>
</InfoTooltip>
{renderLiveUpdateMark()}
</Stack>}
</Stack>
)}
<UserPicker
{...props}
setComponentRef={setComponentRef}
label={undefined}
ariaLabel={label}
required={required}
@ -63,7 +120,8 @@ const LiveUserPicker = <E extends ListItemEntity<any>, P extends PropsOfType<E,
onChanged={onChanged}
/>
{!label && renderLiveUpdateMark()}
</>}
</>
)}
</LiveUpdate>
</Validation>
);

View File

@ -1,5 +1,6 @@
import { IManyToManyRelationship, IManyToOneRelationship, IOneToManyRelationship, ManyToManyRelationship, ManyToOneRelationship, OneToManyRelationship } from "common"
import { ListItemEntity } from "common/sharepoint";
import sanitizeHTML from 'sanitize-html';
const isOneToManyRelationship = <T>(obj: any): obj is IOneToManyRelationship<T> =>
obj instanceof OneToManyRelationship
@ -101,3 +102,17 @@ export const setValue = <E extends ListItemEntity<any>, P extends keyof E>(entit
entity[propertyName] = val as E[P];
}
};
export const renderSanitizedHTML = (value: string) => {
return sanitizeHTML(value, {
allowedTags: ['div', 'span', 'strong', 'b', 'p', 'a', 'title', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'u',
'strike', 'ol', 'ul', 'li', 'font', 'br', 'hr', 's', 'em', 'img', "em", "small",
"table", "tbody", "td", "tfoot", "th", "thead", "tr"],
selfClosing: ['img', 'br', 'hr'],
allowedAttributes: {
a: ['href', 'target', 'data-interception'],
img: ['src'],
'*': ['style']
}
});
};

View File

@ -22,9 +22,17 @@ const HoursOptions: IDropdownOption[] = [
const MinutesOptions: IDropdownOption[] = [
{ key: 0, text: '00' },
{ key: 5, text: '05' },
{ key: 10, text: '10' },
{ key: 15, text: '15' },
{ key: 20, text: '20' },
{ key: 25, text: '25' },
{ key: 30, text: '30' },
{ key: 35, text: '35' },
{ key: 40, text: '40' },
{ key: 45, text: '45' },
{ key: 50, text: '50' },
{ key: 55, text: '55' },
];
export interface ITimePickerProps {
@ -50,7 +58,7 @@ export const TimePicker: FC<ITimePickerProps> = ({
const time = useMemo(() => {
return {
hour: value.hours() % 12,
minute: Math.floor(value.minutes() / 15) * 15, // round down to the closest 15-minute increment
minute: Math.floor(value.minutes() / 5) * 5, // round down to the closest 15-minute increment
ampm: value.hours() >= 12
};
}, [value]);

View File

@ -1,21 +1,33 @@
import { isEmpty } from "lodash";
import React, { FC, useCallback, useMemo } from "react";
import React, { FC, useCallback, useMemo, useRef, useEffect } from "react";
import { PrincipalType } from "@pnp/sp";
import { IPeoplePickerProps, ListPeoplePicker, NormalPeoplePicker, CompactPeoplePicker, IPersonaProps, Label, css, useTheme, PeoplePickerItem, IPeoplePickerItemSelectedProps, IPeoplePickerItemSelectedStyles } from '@fluentui/react';
import { IDirectoryService, useDirectoryService } from 'common/services';
import {
IPeoplePickerProps,
ListPeoplePicker,
NormalPeoplePicker,
CompactPeoplePicker,
IPersonaProps,
Label,
css,
useTheme,
PeoplePickerItem,
IPeoplePickerItemSelectedProps,
IPeoplePickerItemSelectedStyles,
} from "@fluentui/react";
import { IDirectoryService, useDirectoryService } from "common/services";
import { SharePointGroup } from "common/sharepoint";
import { User } from '../User';
import { User } from "../User";
import { InfoTooltip } from "./InfoTooltip";
import * as cstrings from 'CommonStrings';
import styles from './styles/UserPicker.module.scss';
import * as cstrings from "CommonStrings";
import styles from "./styles/UserPicker.module.scss";
const maximumSuggestions = 10;
export enum UserPickerDisplayOption {
Normal,
List,
Compact
Compact,
}
export type OnChangedCallback = (users: User[]) => void;
@ -32,6 +44,7 @@ export interface IUserPickerProps {
onChanged: OnChangedCallback;
restrictPrincipalType?: PrincipalType;
restrictToGroupMembers?: SharePointGroup;
setComponentRef?: boolean;
}
interface IUserPersonaProps extends IPersonaProps {
@ -43,16 +56,16 @@ const userToUserPersona = (user: User): IUserPersonaProps => {
imageUrl: user.picture,
text: user.title,
secondaryText: user.email,
user: user
user: user,
};
};
const containsUser = (list: User[], user: User) => {
return list.some(item => item.email === user.email);
return list.some((item) => item.email === user.email);
};
const removeDuplicateUsers = (suggestedUsers: User[], currentUsers: User[]) => {
return suggestedUsers.filter(user => !containsUser(currentUsers, user));
return suggestedUsers.filter((user) => !containsUser(currentUsers, user));
};
const extractEmailAddress = (input: string): string => {
@ -65,48 +78,70 @@ const extractEmailAddress = (input: string): string => {
}
};
const extractEmailAddresses = (input: string): string[] => {
return input.split(';').map(extractEmailAddress).filter(Boolean).map(e => e.toLocaleLowerCase());
return input
.split(";")
.map(extractEmailAddress)
.filter(Boolean)
.map((e) => e.toLocaleLowerCase());
};
const isListOfEmailAddresses = (input: string): boolean => {
return input.indexOf(';') !== -1 && input.length > 10;
return input.indexOf(";") !== -1 && input.length > 10;
};
const resolveSuggestions = async (searchText: string, currentUserPersonas: IUserPersonaProps[], directoryService: IDirectoryService, onChangedFn: OnChangedCallback, restrictToGroupMembers?: SharePointGroup, restrictPrincipalType?: PrincipalType): Promise<IUserPersonaProps[]> => {
const resolveSuggestions = async (
searchText: string,
currentUserPersonas: IUserPersonaProps[],
directoryService: IDirectoryService,
onChangedFn: OnChangedCallback,
restrictToGroupMembers?: SharePointGroup,
restrictPrincipalType?: PrincipalType
): Promise<IUserPersonaProps[]> => {
if (!searchText) return [];
searchText = searchText.toLocaleLowerCase();
const currentUsers = currentUserPersonas.map(userPersona => userPersona.user);
const currentUsers = currentUserPersonas.map(
(userPersona) => userPersona.user
);
if (isListOfEmailAddresses(searchText)) {
const extractedEmails = extractEmailAddresses(searchText);
let resolvedUsers: User[];
if (restrictToGroupMembers)
resolvedUsers = restrictToGroupMembers.members.filter(member => extractedEmails.some(email => member.email === email));
else
resolvedUsers = await directoryService.resolve(extractedEmails);
resolvedUsers = restrictToGroupMembers.members.filter((member) =>
extractedEmails.some((email) => member.email === email)
);
else resolvedUsers = await directoryService.resolve(extractedEmails);
const nextUsers = [
...currentUsers,
...removeDuplicateUsers(resolvedUsers, currentUsers)
...removeDuplicateUsers(resolvedUsers, currentUsers),
];
onChangedFn(nextUsers);
return [];
}
else {
} else {
let suggestedUsers: User[];
if (restrictToGroupMembers)
suggestedUsers = restrictToGroupMembers.members.filter(member => member.title?.toLocaleLowerCase().includes(searchText) || member.email?.toLocaleLowerCase().includes(searchText));
suggestedUsers = restrictToGroupMembers.members.filter(
(member) =>
member.title?.toLocaleLowerCase().includes(searchText) ||
member.email?.toLocaleLowerCase().includes(searchText)
);
else
suggestedUsers = await directoryService.search(searchText, restrictPrincipalType);
suggestedUsers = await directoryService.search(
searchText,
restrictPrincipalType
);
suggestedUsers = suggestedUsers.slice(0, maximumSuggestions);
return removeDuplicateUsers(suggestedUsers, currentUsers).map(userToUserPersona);
return removeDuplicateUsers(suggestedUsers, currentUsers).map(
userToUserPersona
);
}
};
@ -121,39 +156,69 @@ const UserPicker: FC<IUserPickerProps> = ({
users,
onChanged,
restrictToGroupMembers,
restrictPrincipalType
restrictPrincipalType,
setComponentRef,
}) => {
const { palette: { neutralLight } } = useTheme();
const {
palette: { neutralLight },
} = useTheme();
const directory = useDirectoryService();
const userPersonas = users.map(userToUserPersona);
const role = !isEmpty(userPersonas) ? "list" : "none";
const onChange = (items: IPersonaProps[]) => {
if (!disabled)
onChanged((items as IUserPersonaProps[]).map(userPersona => userPersona.user));
onChanged(
(items as IUserPersonaProps[]).map(
(userPersona) => userPersona.user
)
);
};
const onResolveSuggestions = (filter: string, selectedItems: IPersonaProps[]) =>
resolveSuggestions(filter, selectedItems as IUserPersonaProps[], directory, onChanged, restrictToGroupMembers, restrictPrincipalType);
const onResolveSuggestions = (
filter: string,
selectedItems: IPersonaProps[]
) =>
resolveSuggestions(
filter,
selectedItems as IUserPersonaProps[],
directory,
onChanged,
restrictToGroupMembers,
restrictPrincipalType
);
const fixHighContrastPeoplePickerItemStyles = useMemo(() => {
return { root: { backgroundColor: neutralLight } } as IPeoplePickerItemSelectedStyles;
return {
root: { backgroundColor: neutralLight },
} as IPeoplePickerItemSelectedStyles;
}, [neutralLight]);
const onRenderItem = useCallback(
(props: IPeoplePickerItemSelectedProps) => <PeoplePickerItem {...props} styles={fixHighContrastPeoplePickerItemStyles} />,
(props: IPeoplePickerItemSelectedProps) => (
<PeoplePickerItem
{...props}
styles={fixHighContrastPeoplePickerItemStyles}
/>
),
[fixHighContrastPeoplePickerItemStyles]
);
const dropDownRef = useRef<any>();
useEffect(() => {
if (setComponentRef) dropDownRef.current?.focus();
}, [setComponentRef]);
const renderPicker = () => {
const peoplePickerProps: IPeoplePickerProps = {
selectedItems: userPersonas,
onResolveSuggestions,
onChange,
disabled,
inputProps: { 'aria-label': ariaLabel || label },
inputProps: { "aria-label": ariaLabel || label },
removeButtonAriaLabel: cstrings.UserPicker.RemoveAriaLabel,
onRenderItem
onRenderItem,
componentRef: dropDownRef,
};
switch (display) {
@ -167,12 +232,18 @@ const UserPicker: FC<IUserPickerProps> = ({
};
return (
<div className={css(styles.userPicker, className)} aria-label={ariaLabel || label} role={role}>
{label &&
<div
className={css(styles.userPicker, className)}
aria-label={ariaLabel || label}
role={role}
>
{label && (
<InfoTooltip text={tooltip}>
<Label className={styles.label} required={required}>{label}</Label>
<Label className={styles.label} required={required}>
{label}
</Label>
</InfoTooltip>
}
)}
{renderPicker()}
</div>
);

View File

@ -34,4 +34,5 @@ export { UserList } from './UserList';
export { default as UserPicker, UserPickerDisplayOption } from './UserPicker';
export { Validation } from './Validation';
export { WebPartTitle } from './WebPartTitle';
export { default as LiveMultiTextField } from './LiveMultiTextField';
export { Wizard, IWizardPageProps, IWizardStepProps, IButtonRenderProps, PageRenderer, StepRenderer } from './Wizard';

View File

@ -0,0 +1,5 @@
@import 'common.module';
.infoLabelStyle > label:first-child{
display: inline !important;
}

View File

@ -27,6 +27,7 @@ export interface IDirectoryService extends IService {
findGroupByTitle(title: string, web?: IWeb): Promise<SharePointGroup>;
persistGroup(group: SharePointGroup, web?: IWeb): Promise<void>;
changeGroupOwner(group: SharePointGroup, owner: SharePointGroup | User): Promise<void>;
readonly userHasEditPermisison?: boolean;
}
export type DirectoryServiceProp = {

View File

@ -13,7 +13,11 @@ import { SPPermission } from "@microsoft/sp-page-context";
import { RoleType, SharePointGroup } from "../../sharepoint";
import { ErrorHandler } from "../../ErrorHandler";
import { User } from "../../User";
import { mapGetOrAdd, sanitizeSharePointGroupName, cloneWeb } from "../../Utils";
import {
mapGetOrAdd,
sanitizeSharePointGroupName,
cloneWeb,
} from "../../Utils";
import { ServiceContext } from "../IService";
import { SpfxContext } from "../SpfxContext";
import { IDirectoryService } from "./DirectoryServiceDescriptor";
@ -25,7 +29,7 @@ const adminPermissionsCheck = [
SPPermission.layoutsPage,
SPPermission.manageLists,
SPPermission.managePermissions,
SPPermission.manageWeb
SPPermission.manageWeb,
];
export class OnlineDirectoryService implements IDirectoryService {
@ -34,13 +38,17 @@ export class OnlineDirectoryService implements IDirectoryService {
private readonly _spHttpClient: SPHttpClient;
private readonly _currentUser: User;
private readonly _currentUserPermissions: SPPermission;
private readonly _userHasEditPermission: boolean;
private readonly _resolveCache = new Map<string, Promise<User[]>>();
private readonly _searchCache = new Map<string, Promise<User[]>>();
private readonly _ensureUserCache = new Map<string, Promise<User>>();
private readonly _roleDefinitionIdCache = new Map<RoleType, Promise<number>>();
private readonly _roleDefinitionIdCache = new Map<
RoleType,
Promise<number>
>();
constructor({
[SpfxContext]: { pageContext, spHttpClient }
[SpfxContext]: { pageContext, spHttpClient },
}: ServiceContext) {
const { site, web, user } = pageContext;
this._siteId = site.id;
@ -48,17 +56,25 @@ export class OnlineDirectoryService implements IDirectoryService {
this._spHttpClient = spHttpClient;
this._currentUser = User.fromSPUser(user);
this._currentUserPermissions = web.permissions;
this._userHasEditPermission = web.permissions.hasPermission(
SPPermission.editListItems
);
}
public async initialize(): Promise<void> {
}
public async initialize(): Promise<void> {}
public get currentUser(): User {
return this._currentUser;
}
public get userHasEditPermisison(): boolean {
return this._userHasEditPermission;
}
public get currentUserIsSiteAdmin(): boolean {
return this._currentUserPermissions.hasAllPermissions(...adminPermissionsCheck);
return this._currentUserPermissions.hasAllPermissions(
...adminPermissionsCheck
);
}
public get currentUserEffectivePermissions(): IBasePermissions {
@ -70,36 +86,58 @@ export class OnlineDirectoryService implements IDirectoryService {
inputs = inputs || [];
const batch = web.createBatch();
const principalGroupPromises = Promise.all(inputs.map(input => this._resolveCore(input, batch)));
const principalGroupPromises = Promise.all(
inputs.map((input) => this._resolveCore(input, batch))
);
await batch.execute();
return flatten(await principalGroupPromises);
}
private readonly _resolveCore = async (input: string, batch?: SPBatch): Promise<User[]> => {
private readonly _resolveCore = async (
input: string,
batch?: SPBatch
): Promise<User[]> => {
if (input === null || input.length === 0) {
return [];
}
return mapGetOrAdd(this._resolveCache, input, async () => {
const batchedUtility = batch ? sp.utility.inBatch(batch) : sp.utility;
const results = await batchedUtility.expandGroupsToPrincipals([input]);
const batchedUtility = batch
? sp.utility.inBatch(batch)
: sp.utility;
const results = await batchedUtility.expandGroupsToPrincipals([
input,
]);
return results.map(User.fromPrincipalInfo);
});
}
};
public search(input: string, principalType: PrincipalType = PrincipalType.All): Promise<User[]> {
public search(
input: string,
principalType: PrincipalType = PrincipalType.All
): Promise<User[]> {
return mapGetOrAdd(this._searchCache, input, async () => {
const results = await sp.utility.searchPrincipals(input, principalType, PrincipalSource.All, "", 10);
const results = await sp.utility.searchPrincipals(
input,
principalType,
PrincipalSource.All,
"",
10
);
return results.map(User.fromPrincipalInfo);
});
}
public ensureUsers(users: User[], batch?: SPBatch, web?: IWeb): Promise<User[]> {
public ensureUsers(
users: User[],
batch?: SPBatch,
web?: IWeb
): Promise<User[]> {
web = cloneWeb(web);
const batchedWeb = batch ? web.inBatch(batch) : web;
const ensureUserPromises = users.map(async user => {
const ensureUserPromises = users.map(async (user) => {
const ensuredUser = await this._ensureUserCore(user, batchedWeb);
user.updateId(ensuredUser.id);
return ensuredUser;
@ -119,23 +157,34 @@ export class OnlineDirectoryService implements IDirectoryService {
});
}
public async ensureLogin(users: readonly User[], web?: IWeb): Promise<User[]> {
public async ensureLogin(
users: readonly User[],
web?: IWeb
): Promise<User[]> {
web = cloneWeb(web);
const batch = web.createBatch();
const ensureLoginPromises = Promise.all(users.map(user => this._ensureLoginCore(user, batch)));
const ensureLoginPromises = Promise.all(
users.map((user) => this._ensureLoginCore(user, batch))
);
await batch.execute();
return ensureLoginPromises;
}
private _ensureLoginCore = async (user: User, batch?: SPBatch): Promise<User> => {
private _ensureLoginCore = async (
user: User,
batch?: SPBatch
): Promise<User> => {
if (user.login) {
return user;
} else {
const resolvedUsers = await this._resolveCore(user.email, batch);
if (resolvedUsers.length > 1) throw Error(`Login for ${user.title} (${user.email}) cannot be resolved unambiguously`);
if (resolvedUsers.length > 1)
throw Error(
`Login for ${user.title} (${user.email}) cannot be resolved unambiguously`
);
user.updateLogin(resolvedUsers[0].login);
}
}
};
public async roleDefinitionId(type: RoleType, web?: IWeb): Promise<number> {
web = cloneWeb(web);
@ -143,14 +192,20 @@ export class OnlineDirectoryService implements IDirectoryService {
if (type === RoleType.None) return null;
return mapGetOrAdd(this._roleDefinitionIdCache, type, async () => {
const definition = await sp.web.roleDefinitions.getByType(type).get();
const definition = await sp.web.roleDefinitions
.getByType(type)
.get();
return definition.Id;
});
}
public async siteAdmins(): Promise<User[]> {
const siteUsers = await sp.web.siteUsers();
return siteUsers.filter(r => r.IsSiteAdmin && r.PrincipalType === PrincipalType.User).map(User.fromSiteUserInfo);
return siteUsers
.filter(
(r) => r.IsSiteAdmin && r.PrincipalType === PrincipalType.User
)
.map(User.fromSiteUserInfo);
}
public async siteOwnersGroup(web?: IWeb): Promise<SharePointGroup> {
@ -176,55 +231,78 @@ export class OnlineDirectoryService implements IDirectoryService {
return this._loadSiteGroup(web.siteGroups.getById(id), web);
}
public async findGroupByTitle(title: string, web?: IWeb): Promise<SharePointGroup> {
public async findGroupByTitle(
title: string,
web?: IWeb
): Promise<SharePointGroup> {
try {
web = cloneWeb(web);
const sanitizedTitle = sanitizeSharePointGroupName(title);
return await this._loadSiteGroup(web.siteGroups.getByName(sanitizedTitle), web);
return await this._loadSiteGroup(
web.siteGroups.getByName(sanitizedTitle),
web
);
} catch (e) {
return null; // group does not exist
}
}
private async _loadSiteGroup(siteGroup: ISiteGroup, web: IWeb): Promise<SharePointGroup> {
private async _loadSiteGroup(
siteGroup: ISiteGroup,
web: IWeb
): Promise<SharePointGroup> {
const batch = web.createBatch();
const results = Promise.all([
siteGroup.inBatch(batch)(),
siteGroup.users.inBatch(batch)()
siteGroup.users.inBatch(batch)(),
]);
await batch.execute();
const [groupResult, userResults] = await results;
const users = userResults.map(User.fromSiteUserInfo);
return new SharePointGroup(groupResult.Id, groupResult.LoginName, users);
return new SharePointGroup(
groupResult.Id,
groupResult.LoginName,
users
);
}
public async persistGroup(group: SharePointGroup, web?: IWeb): Promise<void> {
public async persistGroup(
group: SharePointGroup,
web?: IWeb
): Promise<void> {
web = cloneWeb(web);
if (group.hasChanges() && group.isDeleted && !group.isNew) {
await web.siteGroups.removeById(group.id);
}
else if (group.hasChanges() && !group.isDeleted) {
} else if (group.hasChanges() && !group.isDeleted) {
if (group.hasMetadataChanges()) {
const sanitizedTitle = sanitizeSharePointGroupName(group.title);
const groupProperties = {
Title: sanitizedTitle,
Description: group.description,
AllowRequestToJoinLeave: group.allowRequestToJoinLeave,
AutoAcceptRequestToJoinLeave: group.autoAcceptRequestToJoinLeave,
RequestToJoinLeaveEmailSetting: group.requestToJoinLeaveEmailSetting,
AllowMembersEditMembership: group.allowMembersEditMembership,
OnlyAllowMembersViewMembership: group.onlyAllowMembersViewMembership
AutoAcceptRequestToJoinLeave:
group.autoAcceptRequestToJoinLeave,
RequestToJoinLeaveEmailSetting:
group.requestToJoinLeaveEmailSetting,
AllowMembersEditMembership:
group.allowMembersEditMembership,
OnlyAllowMembersViewMembership:
group.onlyAllowMembersViewMembership,
};
if (group.isNew) {
const saveResult = await web.siteGroups.add(groupProperties);
const saveResult = await web.siteGroups.add(
groupProperties
);
group.setId(saveResult.data.Id);
} else {
await web.siteGroups.getById(group.id).update(groupProperties);
await web.siteGroups
.getById(group.id)
.update(groupProperties);
}
}
@ -235,7 +313,9 @@ export class OnlineDirectoryService implements IDirectoryService {
const eh = new ErrorHandler();
const usersBatch = web.createBatch();
const batchedGroupUsers = web.siteGroups.getById(group.id).users.inBatch(usersBatch);
const batchedGroupUsers = web.siteGroups
.getById(group.id)
.users.inBatch(usersBatch);
membersDifference.added.forEach(({ login }) =>
batchedGroupUsers.add(login).catch(eh.catch)
);
@ -250,14 +330,16 @@ export class OnlineDirectoryService implements IDirectoryService {
group.immortalize();
}
public async changeGroupOwner(group: SharePointGroup, owner: SharePointGroup | User): Promise<void> {
const rootId = '740c6a0b-85e2-48a0-a494-e0f1759d4aa7';
public async changeGroupOwner(
group: SharePointGroup,
owner: SharePointGroup | User
): Promise<void> {
const rootId = "740c6a0b-85e2-48a0-a494-e0f1759d4aa7";
const processQuery = `${this._webAbsoluteUrl}/_vti_bin/client.svc/ProcessQuery`;
const ownerType = owner instanceof SharePointGroup ? 'g' : 'u';
const ownerType = owner instanceof SharePointGroup ? "g" : "u";
const options: ISPHttpClientOptions = {
body:
`<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
body: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
<Actions>
<SetProperty Id="1" ObjectPathId="2" Name="Owner">
<Parameter ObjectPathId="3" />
@ -265,12 +347,20 @@ export class OnlineDirectoryService implements IDirectoryService {
<Method Name="Update" Id="4" ObjectPathId="2" />
</Actions>
<ObjectPaths>
<Identity Id="2" Name="${rootId}:site:${this._siteId.toString()}:g:${group.id}" />
<Identity Id="3" Name="${rootId}:site:${this._siteId.toString()}:${ownerType}:${owner.id}" />
<Identity Id="2" Name="${rootId}:site:${this._siteId.toString()}:g:${
group.id
}" />
<Identity Id="3" Name="${rootId}:site:${this._siteId.toString()}:${ownerType}:${
owner.id
}" />
</ObjectPaths>
</Request>`
</Request>`,
};
await this._spHttpClient.post(processQuery, SPHttpClient.configurations.v1, options);
await this._spHttpClient.post(
processQuery,
SPHttpClient.configurations.v1,
options
);
}
}

View File

@ -1,21 +1,29 @@
import moment from 'moment-timezone';
import { ICachingOptions } from '@pnp/odata';
import { extractWebUrl, sp } from '@pnp/sp';
import { ICachingOptions } from "@pnp/odata";
import { extractWebUrl, sp } from "@pnp/sp";
import "@pnp/sp/regional-settings";
import { IWeb } from '@pnp/sp/webs/types';
import { arrayToMap, cloneWeb, now, } from '../../Utils';
import { DeveloperService, DeveloperServiceProp, IDeveloperService } from '../developer';
import { ServiceContext } from '../IService';
import { SpfxContext } from '../SpfxContext';
import { ITimeZone, ITimeZoneService } from './TimeZoneServiceDescriptor';
import { IWeb } from "@pnp/sp/webs/types";
import moment from "moment-timezone";
import { arrayToMap, cloneWeb, now } from "../../Utils";
import {
DeveloperService,
DeveloperServiceProp,
IDeveloperService,
} from "../developer";
import { ServiceContext } from "../IService";
import { SpfxContext } from "../SpfxContext";
import { ITimeZone, ITimeZoneService } from "./TimeZoneServiceDescriptor";
interface TimeZoneMapping {
readonly name: string;
readonly momentId: string;
readonly sharepointId: number;
}
const timezoneMappings = require('./timezone-mappings.json') as TimeZoneMapping[];
const timezoneMappingsBySharePointId = arrayToMap(timezoneMappings, tz => tz.sharepointId);
const timezoneMappings =
require("./timezone-mappings.json") as TimeZoneMapping[];
const timezoneMappingsBySharePointId = arrayToMap(
timezoneMappings,
(tz) => tz.sharepointId
);
class TimeZoneResult {
public Id: number;
@ -34,8 +42,12 @@ class TimeZone implements ITimeZone {
private readonly _mapping: TimeZoneMapping;
public get hasMomentMapping(): boolean { return !!this._mapping; }
public get momentId(): string { return this._mapping.momentId; }
public get hasMomentMapping(): boolean {
return !!this._mapping;
}
public get momentId(): string {
return this._mapping.momentId;
}
constructor(
public readonly id: number,
@ -62,6 +74,10 @@ export class OnlineTimeZoneService implements ITimeZoneService {
return this._siteTimeZoneCache.get(sp.web.toUrl());
}
public get isDifferenceInTimezone(): boolean {
return this.siteTimeZone.momentId !== moment.tz.guess();
}
public get localTimeZone(): ITimeZone {
return this._localTimeZone;
}
@ -72,7 +88,7 @@ export class OnlineTimeZoneService implements ITimeZoneService {
constructor({
[DeveloperService]: dev,
[SpfxContext]: context
[SpfxContext]: context,
}: ServiceContext<DeveloperServiceProp>) {
this._dev = dev;
this._currentWebUrl = context.pageContext.web.absoluteUrl;
@ -80,17 +96,23 @@ export class OnlineTimeZoneService implements ITimeZoneService {
}
public async initialize(): Promise<void> {
const [
timeZoneResults,
siteTimeZone
] = await Promise.all([
sp.web.regionalSettings.timeZones.usingCaching(this._cacheOptions(sp.web, 'timezones'))(),
this._getTimeZone(sp.web)
const [timeZoneResults, siteTimeZone] = await Promise.all([
sp.web.regionalSettings.timeZones.usingCaching(
this._cacheOptions(sp.web, "timezones")
)(),
this._getTimeZone(sp.web),
]);
this._timeZones = timeZoneResults.map(TimeZone.fromTimeZoneResult).filter(tz => tz.hasMomentMapping);
this._timeZonesBySharePointId = arrayToMap(this._timeZones, tz => tz.id);
this._localTimeZone = this._timeZones.find(tz => tz.momentId === moment.tz.guess());
this._timeZones = timeZoneResults
.map(TimeZone.fromTimeZoneResult)
.filter((tz) => tz.hasMomentMapping);
this._timeZonesBySharePointId = arrayToMap(
this._timeZones,
(tz) => tz.id
);
this._localTimeZone = this._timeZones.find(
(tz) => tz.momentId === moment.tz.guess()
);
this._siteTimeZoneCache.set(sp.web.toUrl(), siteTimeZone);
@ -110,12 +132,16 @@ export class OnlineTimeZoneService implements ITimeZoneService {
}
private async _getTimeZone(web: IWeb): Promise<TimeZone> {
const timeZoneResult = await web.regionalSettings.timeZone.usingCaching(this._cacheOptions(web, 'timezone'))();
const timeZoneResult = await web.regionalSettings.timeZone.usingCaching(
this._cacheOptions(web, "timezone")
)();
const timeZone = TimeZone.fromTimeZoneResult(timeZoneResult);
const { hasMomentMapping, id, description } = timeZone;
if (!hasMomentMapping) {
console.warn(`Site time zone (${id} - ${description}) cannot be mapped to an IANA time zone for moment library.`);
console.warn(
`Site time zone (${id} - ${description}) cannot be mapped to an IANA time zone for moment library.`
);
}
return timeZone;
@ -123,25 +149,26 @@ export class OnlineTimeZoneService implements ITimeZoneService {
private readonly _cacheOptions = (web: IWeb, key: string) => {
return {
expiration: now().add(1, 'day').toDate(),
storeName: 'local',
key: `${extractWebUrl(web.toUrl()) || this._currentWebUrl}-${key}`
expiration: now().add(1, "day").toDate(),
storeName: "local",
key: `${extractWebUrl(web.toUrl()) || this._currentWebUrl}-${key}`,
} as ICachingOptions;
}
};
private readonly _devScripts = {
timezones: {
list: () => {
console.log(`Listing known timezones`);
const tzToString = (tz: ITimeZone) => `'${tz.description}' (SPO ID: ${tz.id}, Moment ID: ${tz.momentId})`;
const tzToString = (tz: ITimeZone) =>
`'${tz.description}' (SPO ID: ${tz.id}, Moment ID: ${tz.momentId})`;
this._timeZones.forEach((tz, idx) => {
console.log(`${idx} ${tzToString(tz)}`);
});
console.log(`Site time zone: ${tzToString(this.siteTimeZone)}`);
}
}
},
},
};
}

View File

@ -17,6 +17,7 @@ export interface ITimeZoneService extends IService {
readonly timeZones: ITimeZone[];
readonly siteTimeZone: ITimeZone;
readonly localTimeZone: ITimeZone;
readonly isDifferenceInTimezone: boolean;
timeZoneFromId(id: number): ITimeZone;
timeZoneForWeb(web?: IWeb): Promise<ITimeZone>;
}
@ -25,10 +26,15 @@ export type TimeZoneServiceProp = {
[TimeZoneService]: ITimeZoneService;
};
export const useTimeZoneService = () => useServices<TimeZoneServiceProp>()[TimeZoneService];
export const useTimeZoneService = () =>
useServices<TimeZoneServiceProp>()[TimeZoneService];
export const TimeZoneServiceDescriptor: IServiceDescriptor<typeof TimeZoneService, ITimeZoneService, TimeZoneServiceProp> = {
export const TimeZoneServiceDescriptor: IServiceDescriptor<
typeof TimeZoneService,
ITimeZoneService,
TimeZoneServiceProp
> = {
symbol: TimeZoneService,
dependencies: [],
online: OnlineTimeZoneService
online: OnlineTimeZoneService,
};

View File

@ -1,14 +1,34 @@
import { first } from "lodash";
import moment, { Moment } from "moment-timezone";
import { ITimeZone } from '../services';
import { ITimeZone } from "../services";
import { Entity } from "../Entity";
import { User } from "../User";
import { parseFloatOrDefault, parseIntOrDefault, PropsOfType } from '../Utils';
import { ILookupResult, ITaxonomyResult, IUserInfoResult, IThumbnailResult } from "./query_";
import { parseFloatOrDefault, parseIntOrDefault, PropsOfType } from "../Utils";
import {
ILookupResult,
ITaxonomyResult,
IUserInfoResult,
IThumbnailResult,
} from "./query_";
import { TaxonomyTermEntity } from "./TaxonomyTermEntity";
import { UpdateHyperlink, UpdateMultiChoice, UpdateMultiLookup, UpdateTaxonomy } from "./update";
import {
UpdateHyperlink,
UpdateMultiChoice,
UpdateMultiLookup,
UpdateTaxonomy,
} from "./update";
import { ListItemRating } from "./ListItemRating";
import { ITitleFieldDefinition, ITextFieldDefinition, INumberFieldDefinition, IBooleanFieldDefinition, ITaxonomyFieldDefinition, IDateTimeFieldDefinition, IHyperlinkFieldDefinition, IUserFieldDefinition, AllowedIntegerFieldNames } from "./schema";
import {
ITitleFieldDefinition,
ITextFieldDefinition,
INumberFieldDefinition,
IBooleanFieldDefinition,
ITaxonomyFieldDefinition,
IDateTimeFieldDefinition,
IHyperlinkFieldDefinition,
IUserFieldDefinition,
AllowedIntegerFieldNames,
} from "./schema";
import { Guid } from "@microsoft/sp-core-library";
const BooleanDescriminator = Symbol("Boolean Descriminator");
@ -31,27 +51,41 @@ const GuidDescriminator = Symbol("Guid Descriminator");
const IntegerDescriminator = Symbol("Integer Descriminator");
const RecurrenceDescriminator = Symbol("Recurrence Descriminator");
const sharepointDateTimeFormat = 'M/D/YYYY h:mm A';
const sharepointDateTimeFormat = "M/D/YYYY h:mm A";
export type Query_Boolean = string & { [BooleanDescriminator]: never; };
export type Query_Choice = string & { [ChoiceDescriminator]: never; };
export type Query_ChoiceMulti = string[] & { [ChoiceMultiDescriminator]: never; };
export type Query_Currency = string & { [CurrencyDescriminator]: never; };
export type Query_DateTime = string & { [DateTimeDescriminator]: never; };
export type Query_Lookup = (ILookupResult & { [LookupDescriminator]: never; })[];
export type Query_LookupMulti = (ILookupResult & { [LookupMultiDescriminator]: never; })[];
export type Query_Number = string & { [NumberDescriminator]: never; };
export type Query_Text = string & { [TextDescriminator]: never; };
export type Query_TextMultiLine = string & { [TextMultiDescriminator]: never; };
export type Query_User = (IUserInfoResult & { [UserDescriminator]: never; })[];
export type Query_UserMulti = (IUserInfoResult & { [UserMultiDescriminator]: never; })[];
export type Query_Hyperlink = string & { [HyperlinkDescriminator]: never; };
export type Query_Thumbnail = IThumbnailResult & { [ThumbnailDescriminator]: never; };
export type Query_Taxonomy = ITaxonomyResult & { [TaxonomyDescriminator]: never; };
export type Query_TaxonomyMulti = (ITaxonomyResult & { [TaxonomyMultiDescriminator]: never; })[];
export type Query_Guid = (string & { [GuidDescriminator]: never; })[];
export type Query_Integer = (string & { [IntegerDescriminator]: never; })[];
export type Query_Recurrence = (string & { [RecurrenceDescriminator]: never; })[];
export type Query_Boolean = string & { [BooleanDescriminator]: never };
export type Query_Choice = string & { [ChoiceDescriminator]: never };
export type Query_ChoiceMulti = string[] & {
[ChoiceMultiDescriminator]: never;
};
export type Query_Currency = string & { [CurrencyDescriminator]: never };
export type Query_DateTime = string & { [DateTimeDescriminator]: never };
export type Query_Lookup = (ILookupResult & { [LookupDescriminator]: never })[];
export type Query_LookupMulti = (ILookupResult & {
[LookupMultiDescriminator]: never;
})[];
export type Query_Number = string & { [NumberDescriminator]: never };
export type Query_Text = string & { [TextDescriminator]: never };
export type Query_TextMultiLine = string & { [TextMultiDescriminator]: never };
export type Query_User = (IUserInfoResult & { [UserDescriminator]: never })[];
export type Query_UserMulti = (IUserInfoResult & {
[UserMultiDescriminator]: never;
})[];
export type Query_Hyperlink = string & { [HyperlinkDescriminator]: never };
export type Query_Thumbnail = IThumbnailResult & {
[ThumbnailDescriminator]: never;
};
export type Query_Taxonomy = ITaxonomyResult & {
[TaxonomyDescriminator]: never;
};
export type Query_TaxonomyMulti = (ITaxonomyResult & {
[TaxonomyMultiDescriminator]: never;
})[];
export type Query_Guid = (string & { [GuidDescriminator]: never })[];
export type Query_Integer = (string & { [IntegerDescriminator]: never })[];
export type Query_Recurrence = (string & {
[RecurrenceDescriminator]: never;
})[];
export type Update_Boolean = boolean;
export type Update_Choice = string;
@ -73,7 +107,13 @@ export type Update_Guid = string;
export type Update_Integer = number;
export type Update_Recurrence = boolean;
const toUserCore = ({ id, title, email, sip, picture }: IUserInfoResult): User => {
const toUserCore = ({
id,
title,
email,
sip,
picture,
}: IUserInfoResult): User => {
return new User(parseInt(id), title, email, sip, picture);
};
@ -90,62 +130,132 @@ export const fromUser = (user: User): Update_UserId => {
};
export const fromUsers = (users: User[]): Update_UserIdMulti => {
return new UpdateMultiLookup(users.map(u => u.id));
return new UpdateMultiLookup(users.map((u) => u.id));
};
export const fromDateTime = <T>(row: T, fieldName: PropsOfType<T, Query_DateTime>, { momentId }: ITimeZone): Moment => {
export const fromDateTime = <T>(
row: T,
fieldName: PropsOfType<T, Query_DateTime>,
{ momentId }: ITimeZone
): Moment => {
const value: string = (row as any)[`${String(fieldName)}.`];
return value ? moment.tz(value, [moment.ISO_8601, sharepointDateTimeFormat], momentId) : null;
return value
? moment.tz(
value,
[moment.ISO_8601, sharepointDateTimeFormat],
momentId
)
: null;
};
export const toDateTime = (dateTime: Moment, { momentId }: ITimeZone): Update_DateTime => {
export const fromDate = <T>(
row: T,
fieldName: PropsOfType<T, Query_DateTime>,
{ momentId }: ITimeZone
): Moment => {
const value: string = (row as any)[`${String(fieldName)}.`];
if (value && value.indexOf("T") > -1) {
const dateValue: string = value.split("T")[0];
return value
? moment.tz(
dateValue,
[moment.ISO_8601, sharepointDateTimeFormat],
momentId
)
: null;
} else
return value
? moment.tz(
value,
[moment.ISO_8601, sharepointDateTimeFormat],
momentId
)
: null;
};
export const toDateTime = (
dateTime: Moment,
{ momentId }: ITimeZone
): Update_DateTime => {
return dateTime ? dateTime.tz(momentId, true).toISOString() : null;
};
export const toDateOnly = (dateTime: Moment): Update_DateTime => {
return dateTime ? dateTime.format('MM-DD-YYYY') : null;
return dateTime ? dateTime.format("MM-DD-YYYY") : null;
};
export const fromYesNo = <T>(row: T, fieldName: PropsOfType<T, Query_Boolean>, defaultValue: boolean = false): boolean => {
export const fromYesNo = <T>(
row: T,
fieldName: PropsOfType<T, Query_Boolean>,
defaultValue: boolean = false
): boolean => {
const value: string = (row as any)[`${String(fieldName)}.value`];
switch (value) {
case "0": return false;
case "1": return true;
default: return defaultValue;
case "0":
return false;
case "1":
return true;
default:
return defaultValue;
}
};
export const fromInteger = <T>(row: T, fieldName: PropsOfType<T, Query_Integer> & AllowedIntegerFieldNames): number => {
export const fromInteger = <T>(
row: T,
fieldName: PropsOfType<T, Query_Integer> & AllowedIntegerFieldNames
): number => {
const value: string = (row as any)[fieldName];
return parseIntOrDefault(value, undefined, 10);
};
export const fromInt = <T>(row: T, fieldName: PropsOfType<T, Query_Number>, defaultValue: number = Number.NaN, radix: number = 10): number => {
export const fromInt = <T>(
row: T,
fieldName: PropsOfType<T, Query_Number>,
defaultValue: number = Number.NaN,
radix: number = 10
): number => {
const value: string = (row as any)[`${String(fieldName)}.`];
return parseIntOrDefault(value, defaultValue, radix);
};
export const fromFloat = <T>(row: T, fieldName: PropsOfType<T, Query_Number>, defaultValue: number = Number.NaN): number => {
export const fromFloat = <T>(
row: T,
fieldName: PropsOfType<T, Query_Number>,
defaultValue: number = Number.NaN
): number => {
const value: string = (row as any)[`${String(fieldName)}.`];
return parseFloatOrDefault(value, defaultValue);
};
export const fromCurrency = <T>(row: T, fieldName: PropsOfType<T, Query_Currency>, defaultValue: number = Number.NaN): number => {
export const fromCurrency = <T>(
row: T,
fieldName: PropsOfType<T, Query_Currency>,
defaultValue: number = Number.NaN
): number => {
const value: string = (row as any)[`${String(fieldName)}.`];
return parseFloatOrDefault(value, defaultValue);
};
export const fromGuid = <T>(row: T, fieldName: PropsOfType<T, Query_Guid>): Guid => {
export const fromGuid = <T>(
row: T,
fieldName: PropsOfType<T, Query_Guid>
): Guid => {
const value: string = (row as any)[fieldName];
return Guid.tryParse(value);
};
export const fromRecurrence = <T>(row: T, fieldName: PropsOfType<T, Query_Recurrence> & "fRecurrence"): boolean => {
export const fromRecurrence = <T>(
row: T,
fieldName: PropsOfType<T, Query_Recurrence> & "fRecurrence"
): boolean => {
const value: string = (row as any)[fieldName];
switch (value) {
case "0": return false;
case "1": return true;
default: return false;
case "0":
return false;
case "1":
return true;
default:
return false;
}
};
@ -153,61 +263,118 @@ export const tofRecurrence = (recurrence: boolean): Update_Recurrence => {
return recurrence;
};
export const toLookupMulti = <T extends Entity<any>>(entities: ReadonlyArray<T>): Update_LookupIdMulti => {
return new UpdateMultiLookup(entities.map(e => e.id));
export const toLookupMulti = <T extends Entity<any>>(
entities: ReadonlyArray<T>
): Update_LookupIdMulti => {
return new UpdateMultiLookup(entities.map((e) => e.id));
};
export const toChoiceMulti = <T extends Entity<any>>(
entities: ReadonlyArray<string>
): Update_ChoiceMulti => {
return new UpdateMultiChoice(entities.map((e) => e));
};
export const lookupHasValue = (value: Query_Lookup | Query_LookupMulti) => {
return value && value.length > 0 && value[0].lookupId > 0 && !!value[0].lookupValue;
return (
value &&
value.length > 0 &&
value[0].lookupId > 0 &&
!!value[0].lookupValue
);
};
export const fromLookup = <T>(value: Query_Lookup, lookup: ReadonlyMap<number, T>) => {
export const fromLookup = <T>(
value: Query_Lookup,
lookup: ReadonlyMap<number, T>
) => {
return lookupHasValue(value) ? lookup.get(first(value).lookupId) : null;
};
export const fromLookupMulti = <T>(values: Query_LookupMulti, lookup: ReadonlyMap<number, T>) => {
return lookupHasValue(values) ? values.map(value => lookup.get(value.lookupId)) : [];
export const fromLookupMulti = <T>(
values: Query_LookupMulti,
lookup: ReadonlyMap<number, T>
) => {
return lookupHasValue(values)
? values.map((value) => lookup.get(value.lookupId))
: [];
};
export const fromLookupAsync = async <T>(value: Query_Lookup, lookup: (id: number) => T | Promise<T>) => {
export const fromLookupAsync = async <T>(
value: Query_Lookup,
lookup: (id: number) => T | Promise<T>
) => {
return lookupHasValue(value) ? await lookup(value[0].lookupId) : null;
};
export const fromLookupMultiAsync = async <T>(values: Query_LookupMulti, lookup: (id: number) => T | Promise<T>) => {
return lookupHasValue(values) ? await Promise.all(values.map(value => lookup(value.lookupId))) : [];
export const fromLookupMultiAsync = async <T>(
values: Query_LookupMulti,
lookup: (id: number) => T | Promise<T>
) => {
return lookupHasValue(values)
? await Promise.all(values.map((value) => lookup(value.lookupId)))
: [];
};
export const toTaxonomy = <T extends TaxonomyTermEntity<any, T>>(term: T): Update_Taxonomy => {
export const toTaxonomy = <T extends TaxonomyTermEntity<any, T>>(
term: T
): Update_Taxonomy => {
return term ? new UpdateTaxonomy(term.label, term.termId.toString()) : null;
};
export const toTaxonomyMulti = <T extends TaxonomyTermEntity<any, T>>(terms: readonly T[]): Update_TaxonomyMulti => {
return (terms || []).map(term => `-1;#${term.label}|${term.termId.toString()}`).join(';#');
export const toTaxonomyMulti = <T extends TaxonomyTermEntity<any, T>>(
terms: readonly T[]
): Update_TaxonomyMulti => {
return (terms || [])
.map((term) => `-1;#${term.label}|${term.termId.toString()}`)
.join(";#");
};
export const fromTaxonomy = <T extends TaxonomyTermEntity<any, T>>(value: Query_Taxonomy, lookup: ReadonlyMap<string, T>) => {
export const fromTaxonomy = <T extends TaxonomyTermEntity<any, T>>(
value: Query_Taxonomy,
lookup: ReadonlyMap<string, T>
) => {
return lookup.get(value?.TermID || value?.TermGuid);
};
export const fromTaxonomyMulti = <T extends TaxonomyTermEntity<any, T>>(values: Query_TaxonomyMulti, lookup: ReadonlyMap<string, T>) => {
return (values || []).map(value => lookup.get(value.TermID)).filter(Boolean);
export const fromTaxonomyMulti = <T extends TaxonomyTermEntity<any, T>>(
values: Query_TaxonomyMulti,
lookup: ReadonlyMap<string, T>
) => {
return (values || [])
.map((value) => lookup.get(value.TermID))
.filter(Boolean);
};
export const fromTaxonomyAsync = async <T extends TaxonomyTermEntity<any, T>>(value: Query_Taxonomy, lookup: (guid: string) => T | Promise<T>) => {
export const fromTaxonomyAsync = async <T extends TaxonomyTermEntity<any, T>>(
value: Query_Taxonomy,
lookup: (guid: string) => T | Promise<T>
) => {
return lookup(value?.TermID);
};
export const fromTaxonomyMultiAsync = async <T extends TaxonomyTermEntity<any, T>>(values: Query_TaxonomyMulti, lookup: (guid: string) => T | Promise<T>) => {
return Promise.all((values || []).map(value => lookup(value.TermID)));
export const fromTaxonomyMultiAsync = async <
T extends TaxonomyTermEntity<any, T>
>(
values: Query_TaxonomyMulti,
lookup: (guid: string) => T | Promise<T>
) => {
return Promise.all((values || []).map((value) => lookup(value.TermID)));
};
export const fromThumbnail = (value: Query_Thumbnail): string => {
return value?.serverRelativeUrl;
};
export const toRating = (entity: ListItemRating, row: { RatedBy: Query_UserMulti, Ratings: Query_Text }): void => {
export const toRating = (
entity: ListItemRating,
row: { RatedBy: Query_UserMulti; Ratings: Query_Text }
): void => {
entity.ratedBy = toUsers(row.RatedBy);
entity.ratings = (row.Ratings || '').split(',').filter(Boolean).map(r => parseInt(r, 10));
entity.ratings = (row.Ratings || "")
.split(",")
.filter(Boolean)
.map((r) => parseInt(r, 10));
};
export const Form = {
@ -218,33 +385,69 @@ export const Form = {
return { FieldName: field.name, FieldValue: value };
},
Number: (field: INumberFieldDefinition, value: number) => {
return { FieldName: field.name, FieldValue: value?.toString() || '' };
return { FieldName: field.name, FieldValue: value?.toString() || "" };
},
Boolean: (field: IBooleanFieldDefinition, value: boolean) => {
return { FieldName: field.name, FieldValue: value ? '1' : '2' };
return { FieldName: field.name, FieldValue: value ? "1" : "2" };
},
User: (field: IUserFieldDefinition, value: User) => {
return { FieldName: field.name, FieldValue: value ? JSON.stringify([{ Key: value.login }]) : '' };
return {
FieldName: field.name,
FieldValue: value ? JSON.stringify([{ Key: value.login }]) : "",
};
},
UserMulti: (field: IUserFieldDefinition, value: User[]) => {
return { FieldName: field.name, FieldValue: value.length > 0 ? "[" + value.map(user => `{ "Key": "${user.login}" }`).join(',') + "]" : '' };
return {
FieldName: field.name,
FieldValue:
value.length > 0
? "[" +
value
.map((user) => `{ "Key": "${user.login}" }`)
.join(",") +
"]"
: "",
};
},
Date: (field: IDateTimeFieldDefinition, value: Moment) => {
return { FieldName: field.name, FieldValue: value ? value.format('MM/DD/YYYY') : '' };
return {
FieldName: field.name,
FieldValue: value ? value.format("MM/DD/YYYY") : "",
};
},
DateTime: (field: IDateTimeFieldDefinition, value: Moment) => {
return { FieldName: field.name, FieldValue: value ? value.format('MM/DD/YYYY HH:MM A') : '' };
return {
FieldName: field.name,
FieldValue: value ? value.format("MM/DD/YYYY HH:MM A") : "",
};
},
Hyperlink: (field: IHyperlinkFieldDefinition, value: string) => {
return { FieldName: field.name, FieldValue: value?.toString() || '' };
return { FieldName: field.name, FieldValue: value?.toString() || "" };
},
SingleMMD: <T extends TaxonomyTermEntity<any, T>>(field: ITaxonomyFieldDefinition, value: T) => {
return { FieldName: field.name, FieldValue: value ? `${value.label}|${value.termId.toString()};` : '' };
SingleMMD: <T extends TaxonomyTermEntity<any, T>>(
field: ITaxonomyFieldDefinition,
value: T
) => {
return {
FieldName: field.name,
FieldValue: value
? `${value.label}|${value.termId.toString()};`
: "",
};
},
MultiMMD: <T extends TaxonomyTermEntity<any, T>>(field: ITaxonomyFieldDefinition, value: T[]) => {
return { FieldName: field.name, FieldValue: value?.map(term => `${term.label}|${term.termId.toString()}`).join(';') || '' };
MultiMMD: <T extends TaxonomyTermEntity<any, T>>(
field: ITaxonomyFieldDefinition,
value: T[]
) => {
return {
FieldName: field.name,
FieldValue:
value
?.map((term) => `${term.label}|${term.termId.toString()}`)
.join(";") || "",
};
},
FileLeafRef: (value: string) => {
return { FieldName: 'FileLeafRef', FieldValue: value };
}
return { FieldName: "FileLeafRef", FieldValue: value };
},
};

View File

@ -384,11 +384,21 @@ export class ElementProvisioner {
}
// we do not allow creating or changing built-in fields
// if (AutomaticListFields.allLists.includes(name) ||
// AutomaticListFields[fieldDefinition[ParentList].template].includes(name)) {
// return;
// }
if (AutomaticListFields.allLists.includes(name) ||
AutomaticListFields[fieldDefinition[ParentList].template].includes(name)) {
(fieldDefinition[ParentList] &&
AutomaticListFields[
fieldDefinition[ParentList].template
].includes(name))
) {
return;
}
const batchedFields = fields.inBatch(batch);
const displayName = fieldDefinition.displayName || name;

View File

@ -0,0 +1,285 @@
// import React from 'react';
// import { FocusZone, ICommandBarItemProps, IDropdownOption, Text } from "@fluentui/react";
// import { Entity, ErrorHandler } from 'common';
// import { EntityPanelBase, IEntityPanelProps, IDataPanelBaseState, ResponsiveGrid, GridRow, GridCol, IDataPanelBase, LiveTextField, LiveText} from "common/components";
// import { ChannelsConfigurations, Refiner, RefinerValue } from "model";
// import { withServices, ServicesProp, EventsServiceProp, EventsService } from 'services';
// import { ListItemTechnicals } from '../shared';
// import { PersistConcurrencyFailureMessage, ChannelsPanel as strings } from "ComponentStrings";
// import styles from '../approvals/ApproversPanel.module.scss';
// export interface IChannelsPanel extends IDataPanelBase<ChannelsConfigurations> {
// }
// interface IOwnProps {
// sendDataToParent?: (data: boolean) => void;
// isTeamsMessageRequired?:boolean;
// }
// type IProps = IOwnProps & IEntityPanelProps<ChannelsConfigurations> & ServicesProp<EventsServiceProp>;
// interface IOwnState {
// refinerValueOptionsByRefiner?: Map<Refiner, IDropdownOption[]>;
// refiners?: readonly Refiner[];
// dataToSend?: boolean;
// }
// type IState = IOwnState & IDataPanelBaseState<ChannelsConfigurations>;
// class ChannelsPanel extends EntityPanelBase<ChannelsConfigurations, IProps, IState> implements IChannelsPanel {
// handleButtonClick = () => {
// // Call the function passed from the parent and pass the data
// this.props.sendDataToParent(this.state.dataToSend);
// };
// protected get title() {
// return '';
// }
// protected resetState(): IState {
// this._buildRefinerValueOptions();
// return {
// ...super.resetState(),
// refinerValueOptionsByRefiner: new Map(),
// refiners: [],
// dataToSend: this.props.isTeamsMessageRequired ? this.props.isTeamsMessageRequired : false
// };
// }
// public componentShouldRender() {
// super.componentShouldRender();
// this._buildRefinerValueOptions();
// }
// private async _buildRefinerValueOptions() {
// const { [EventsService]: { refinersAsync } } = this.props.services;
// await refinersAsync.promise;
// const refiners = [...refinersAsync.data];
// refiners.sort(Refiner.OrderAscComparer);
// const refinerValueOptionsByRefiner = new Map<Refiner, IDropdownOption[]>();
// for (const refiner of refiners) {
// const options: IDropdownOption[] = refiner.values.filter(Entity.NotDeletedFilter).map((value: RefinerValue) => {
// const { key, displayName: text } = value;
// return { key, text, data: value } as IDropdownOption;
// });
// refinerValueOptionsByRefiner.set(refiner, options);
// }
// this.setState({ refinerValueOptionsByRefiner, refiners });
// }
// protected async persistChangesCore() {
// const { [EventsService]: events } = this.props.services;
// let _teamsName = "";
// let _actualChannelName = "";
// try{
// _teamsName = await events.getTeamsNameById(this.entity.teamsId);
// _actualChannelName = await events.getActualChannelNameById(this.entity.teamsId,this.entity.channelId);
// this.setState({
// dataToSend:false
// });
// } catch (ex) {
// _teamsName = "";
// _actualChannelName = "";
// this.setState({
// dataToSend:true
// });
// }
// try {
// this.entity.teamsName = _teamsName;
// this.entity.actualChannelName = _actualChannelName;
// events.track(this.entity);
// await events.persist();
// } catch (e) {
// if (ErrorHandler.is_412_PRECONDITION_FAILED(e)) {
// const message = await ErrorHandler.message(e);
// console.warn(message, e);
// return Promise.reject(PersistConcurrencyFailureMessage);
// } else {
// throw e;
// }
// }
// }
// private channelDescription(): JSX.Element {
// return <>
// <Text tabIndex={0}>The steps to get the Teams Id and Channel Id are given below : </Text>
// <Text block tabIndex={0}>
// <ul>
// <li>Click on the three dots near the channel name within Microsoft Teams.</li>
// <li>Select 'Get a link to the channel' and copy the link.</li>
// <li>Select the <b>'groupId'</b> value from the copied link and paste it for the teams id field.</li>
// <li>Next copy the text starting after the <b>'channel/'</b> from the copied link and select till the <b>'.tacv2'</b>.</li>
// <li>Now paste the link within the channel id field.</li>
// </ul>
// </Text>
// </>;
// }
// protected renderDisplayContent(): JSX.Element {
// const entity = this.entity;
// const liveProps = {
// entity
// };
// return (
// <FocusZone>
// <ResponsiveGrid className={styles.content}>
// <GridRow>
// <GridCol>
// <LiveText label={strings.Field_ChannelName_DisplayMode.Label} {...liveProps} propertyName="channelName" />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol>
// <LiveText label={strings.Field_TeamsId_DisplayMode.Label} {...liveProps} propertyName="teamsId" />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol>
// <LiveText label={strings.Field_ChannelId_DisplayMode.Label} {...liveProps} propertyName="channelId" />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol>
// <LiveText label={strings.Field_TeamsName_DisplayMode.Label} {...liveProps} propertyName="teamsName" />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol>
// <LiveText label={strings.Field_ActualChannelName_DisplayMode.Label} {...liveProps} propertyName="actualChannelName" />
// </GridCol>
// </GridRow>
// <br />
// <GridRow>
// <GridCol sm={12}>
// {this.channelDescription()}
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol sm={12}>
// <ListItemTechnicals entity={this.entity} />
// </GridCol>
// </GridRow>
// </ResponsiveGrid>
// </FocusZone>
// );
// }
// protected renderEditContent(): JSX.Element {
// const { showValidationFeedback } = this.state;
// const entity = this.entity;
// const liveProps = {
// entity,
// showValidationFeedback,
// updateField: this.updateField,
// hideIcon:true
// };
// return (
// <ResponsiveGrid className={styles.content}>
// <GridRow>
// <GridCol sm={12}>
// <LiveTextField
// {...liveProps}
// label={strings.Field_ChannelName_EditMode.Label}
// propertyName="channelName"
// autoFocus={entity.isNew}
// required
// maxLength={255}
// rules={ChannelsConfigurations.ChannelNameValidations}
// />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol sm={12}>
// <LiveTextField
// {...liveProps}
// label={strings.Field_TeamsId_EditMode.Label}
// propertyName="teamsId"
// autoFocus={entity.isNew}
// required
// maxLength={255}
// rules={ChannelsConfigurations.TeamsIdValidations}
// />
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol sm={12}>
// <LiveTextField
// {...liveProps}
// label={strings.Field_ChannelId__EditMode.Label}
// propertyName="channelId"
// autoFocus={entity.isNew}
// required
// maxLength={255}
// rules={ChannelsConfigurations.ChannelIdValidations}
// />
// </GridCol>
// </GridRow>
// <br />
// <GridRow>
// <GridCol sm={12}>
// {this.channelDescription()}
// </GridCol>
// </GridRow>
// <GridRow>
// <GridCol sm={12}>
// <ListItemTechnicals entity={this.entity} />
// </GridCol>
// </GridRow>
// </ResponsiveGrid>
// );
// }
// protected buildDisplayHeaderCommands(): ICommandBarItemProps[] {
// const onEdit = () => { this.edit(); };
// return [{
// key: 'edit',
// text: strings.Command_Edit.Text,
// iconProps: { iconName: 'Edit' },
// onClick: onEdit
// }];
// }
// protected buildEditHeaderCommands(): ICommandBarItemProps[] {
// const { submitting } = this.state;
// const { isDeleted } = this.entity;
// const onSubmit = () => this.submit(() => {
// this.handleButtonClick();
// this.dismiss();
// });
// const onConfirmDiscard = () => this.confirmDiscard();
// const onDelete = () => this.confirmDelete();
// return [{
// key: 'save',
// text: strings.Command_Save.Text,
// iconProps: { iconName: 'Save' },
// disabled: submitting,
// onClick: onSubmit
// }, {
// key: 'discard',
// text: strings.Command_Discard.Text,
// iconProps: { iconName: 'Cancel' },
// onClick: onConfirmDiscard
// }, {
// key: 'delete',
// text: strings.Command_Delete.Text,
// iconProps: { iconName: 'Delete' },
// disabled: isDeleted,
// onClick: onDelete
// }];
// }
// }
// export default withServices(ChannelsPanel);

View File

@ -0,0 +1,266 @@
// import { isEqual } from "lodash";
// import React, { Component, createRef, MutableRefObject, ReactNode, RefObject } from "react";
// import { CheckboxVisibility, CommandBar, ConstrainMode, DetailsList, DetailsListLayoutMode, IColumn, IContextualMenuItem, MessageBar, MessageBarType, Panel, PanelType, Selection, SelectionMode, Text } from "@fluentui/react";
// import { Entity, humanizeFixedList, IAsyncData, multifilter } from "common";
// import { AsyncDataComponent } from "common/components";
// import { Approvers, ChannelsConfigurations, Refiner } from "model";
// import { EventsService, EventsServiceProp, ServicesProp, TeamsJs, withServices } from "services";
// import ChannelsPanel, { IChannelsPanel } from "./ChannelsPanel";
// import { ConfigureChannelsPanel as strings } from "ComponentStrings";
// export interface IConfigureChannelsPanel {
// open: () => void;
// close: () => void;
// }
// interface IOwnProps {
// componentRef?: RefObject<IConfigureChannelsPanel>;
// }
// type IProps = IOwnProps & ServicesProp<EventsServiceProp>;
// interface IState {
// hidden: boolean;
// channelsConfigurationsAsync: IAsyncData<readonly ChannelsConfigurations[]>;
// refinersAsync: IAsyncData<readonly Refiner[]>;
// isTeamsMessageRequired: boolean;
// }
// class ConfigureChannelsPanel extends Component<IProps, IState> implements IConfigureChannelsPanel {
// private readonly _channelsPanel = createRef<IChannelsPanel>();
// private readonly _selection: Selection<ChannelsConfigurations>;
// constructor(props: IProps) {
// super(props);
// const {
// [EventsService]: { channelsConfigurationsAsync, refinersAsync }
// } = this.props.services;
// this.state = {
// hidden: true,
// channelsConfigurationsAsync,
// refinersAsync,
// isTeamsMessageRequired: false
// };
// this._selection = new Selection<ChannelsConfigurations>({
// onSelectionChanged: () => this.setState({}),
// items: []
// });
// }
// handleDataFromChild = (data:boolean) => {
// this.setState({ isTeamsMessageRequired: data });
// };
// public componentDidMount() {
// (this.props.componentRef as MutableRefObject<IConfigureChannelsPanel>).current = this;
// }
// public componentWillUnmount(): void {
// (this.props.componentRef as MutableRefObject<IConfigureChannelsPanel>).current = null;
// }
// public readonly open = () =>
// this.setState({ hidden: false })
// public readonly close = () =>
// this.setState({ hidden: true })
// private readonly _viewApprovers = async () => {
// try {
// // const approvers = this._selection.getSelection()[0];
// // await this._approversPanel.current.display(approvers);
// } finally { this.forceUpdate(); }
// }
// private readonly _viewChannelsConfiguration = async () => {
// try {
// const channelsConfiguration = this._selection.getSelection()[0];
// await this._channelsPanel.current.display(channelsConfiguration);
// } finally { this.forceUpdate(); }
// }
// private readonly _newChannelsConfiguration = async () => {
// try {
// await this._channelsPanel.current.edit(new ChannelsConfigurations());
// } finally { this.forceUpdate(); }
// }
// // Added this funtion to edit the channel configuration
// private readonly _editChannelsConfiguration = async () => {
// try {
// const channelsConfiguration = this._selection.getSelection()[0];
// await this._channelsPanel.current.edit(channelsConfiguration);
// } finally { this.forceUpdate(); }
// }
// private readonly _getApproversKey = ({ key }: Approvers) => key;
// private readonly _generateCommands = (selectedCount: number) => {
// const addChannelsConfig: IContextualMenuItem = {
// key: "add",
// name: "New",
// iconProps: { iconName: "Add" },
// onClick: () => { this._newChannelsConfiguration(); }
// };
// const viewChannelsConfig: IContextualMenuItem = {
// key: "view",
// name: "View",
// iconProps: { iconName: "View" },
// disabled: selectedCount === 0,
// onClick: () => { this._viewChannelsConfiguration(); }
// };
// const editChannelsConfig: IContextualMenuItem = {
// key: "edit",
// name: "Edit",
// iconProps: { iconName: "Edit" },
// disabled: selectedCount === 0,
// onClick: () => { this._editChannelsConfiguration(); }
// };
// return {
// near: [addChannelsConfig, viewChannelsConfig, editChannelsConfig]
// };
// }
// private *_generateColumns(refiners?: readonly Refiner[]): Generator<IColumn> {
// // yield {
// // key: 'title',
// // name: strings.Column_Title,
// // isRowHeader: true,
// // isResizable: true,
// // isMultiline: true,
// // fieldName: 'title'
// // } as IColumn;
// yield {
// key: 'channelName',
// name: strings.Column_ChannelName,
// isRowHeader: true,
// isResizable: true,
// isMultiline: true,
// flexGrow: 1,
// minWidth: 100,
// fieldName: 'channelName'
// } as IColumn;
// yield {
// key: 'teamsId',
// name: strings.Column_TeamsId,
// isRowHeader: true,
// isResizable: true,
// isMultiline: true,
// flexGrow: 1,
// minWidth: 100,
// fieldName: 'teamsId'
// } as IColumn;
// yield {
// key: 'channelId',
// name: strings.Column_ChannelId,
// isRowHeader: true,
// isResizable: true,
// isMultiline: true,
// flexGrow: 1,
// minWidth: 100,
// fieldName: 'channelId'
// } as IColumn;
// yield {
// key: 'teamsName',
// name: strings.Column_TeamsName,
// isRowHeader: true,
// isResizable: true,
// isMultiline: true,
// fieldName: 'teamsName'
// } as IColumn;
// yield {
// key: 'actualChannelName',
// name: strings.Column_ActualChannelName,
// isRowHeader: true,
// isResizable: true,
// isMultiline: true,
// fieldName: 'actualChannelName'
// } as IColumn;
// }
// private _filteredAndSortdChannels: ChannelsConfigurations[] = [];
// private _getFilteredAndSortedChannels(channelsConfigurations: readonly ChannelsConfigurations[]): ChannelsConfigurations[] {
// const filteredAndSortdChannels = channelsConfigurations.filter(Entity.NotDeletedFilter).sort(Entity.DisplayNameAscComparer);
// if (!isEqual(this._filteredAndSortdChannels, filteredAndSortdChannels)) {
// this._filteredAndSortdChannels = filteredAndSortdChannels;
// this._selection.setItems(filteredAndSortdChannels);
// }
// const hasTeamChannelBlankName = filteredAndSortdChannels.some(channel => !channel.teamsName.trim() || !channel.actualChannelName.trim());
// if(hasTeamChannelBlankName && !this.state.isTeamsMessageRequired) {
// this.setState({ isTeamsMessageRequired: true });
// }
// return this._filteredAndSortdChannels;
// }
// public render(): ReactNode {
// const { [TeamsJs]: teams } = this.props.services;
// const { hidden, channelsConfigurationsAsync, refinersAsync } = this.state;
// const commands = this._generateCommands(this._selection.getSelectedCount());
// return (
// <AsyncDataComponent dataAsync={channelsConfigurationsAsync}>{channelsConfigurations =>
// <AsyncDataComponent dataAsync={refinersAsync}>{refiners => <>
// <Panel
// type={PanelType.large}
// isOpen={!hidden}
// isBlocking={false}
// isLightDismiss
// onDismiss={this.close}
// headerText={strings.HeaderText}
// closeButtonAriaLabel={strings.Command_Close.AriaLabel}
// >
// <CommandBar items={commands.near} />
// <DetailsList
// items={this._getFilteredAndSortedChannels(channelsConfigurations)}
// getKey={this._getApproversKey}
// columns={[...this._generateColumns(refiners)]}
// selection={this._selection}
// selectionMode={SelectionMode.single}
// layoutMode={DetailsListLayoutMode.fixedColumns}
// constrainMode={ConstrainMode.horizontalConstrained}
// checkboxVisibility={CheckboxVisibility.always}
// onItemInvoked={this._viewApprovers}
// />
// {this._filteredAndSortdChannels.length === 0 &&
// <Text block styles={{ root: { marginLeft: 60, marginBottom: 20 } }}>{strings.NoChannelsDefined}</Text>
// }
// {
// }
// { this.state.isTeamsMessageRequired && <MessageBar messageBarType={MessageBarType.warning}>
// {strings.Message_Teams}
// </MessageBar>
// }
// </Panel>
// <ChannelsPanel
// hasCloseButton
// componentRef={this._channelsPanel}
// asyncWatchers={[channelsConfigurationsAsync]}
// sendDataToParent={this.handleDataFromChild}
// isTeamsMessageRequired={this.state.isTeamsMessageRequired}
// />
// </>}</AsyncDataComponent>
// }</AsyncDataComponent>
// );
// }
// }
// export default withServices(ConfigureChannelsPanel);

View File

@ -0,0 +1,2 @@
// export { default as ChannelsPanel, IChannelsPanel } from './ChannelsPanel';
// export { default as ConfigureChannelsPanel, IConfigureChannelsPanel } from './ConfigureChannelsPanel';

View File

@ -0,0 +1,27 @@
@import 'common.module';
.productivityStudioLogo {
@include ms-font-mi;
font-size: 12px;
padding: 16px 16px 2px 8px;
display: flex;
justify-content: flex-end;
align-items: center;
// text-align: right;
span{
margin-right: 4px;
display:inline-block;
}
a{
text-decoration: none;
img{
position:relative;
top:1px;
}
}
a:visited {
color: $ms-color-themePrimary;
outline: 1px solid black;
}
}

View File

@ -0,0 +1,27 @@
import * as React from "react";
import { Stack, css } from "@fluentui/react";
import { ProductivityStudioLogo as strings } from "ComponentStrings";
const ProductivityStudioLogoImg = require('assets/onboarding/ProductivityStudioLogo.png');
import styles from './ProductivityStudioLogo.module.scss';
export interface IProductivityStudioLogoProps {
className?: string;
}
export const ProductivityStudioLogo: React.SFC<IProductivityStudioLogoProps> = (props: IProductivityStudioLogoProps) => {
return (
<p className={css(styles.productivityStudioLogo, props.className)}>
{<span> Created by the </span>}
<a tabIndex={0} href="mailto:ProdStudioHarvesting@microsoft.com">
{strings.Command_ProductivityLogoLink}
</a>
<a tabIndex={0} href="mailto:ProdStudioHarvesting@microsoft.com">
<img src={ProductivityStudioLogoImg} alt="Productivity Studio Logo" width={16} />
</a>
</p>
);
};

View File

@ -32,7 +32,14 @@ class ApprovalDialog extends EntityDialogBase<Event, IProps, IState> implements
try {
this.entity.moderator = currentUser;
this.entity.moderationTimestamp = now();
const itemUrl = events.createEventDeepLink(this.entity);
const chatID: { chat_Id: string, rxUser: any }[] = [];
chatID.push({chat_Id: this.entity.teamsGroupChatId.replace("#", "@"), rxUser: []});
if(this.entity.moderationStatus === EventModerationStatus.Approved ){
await events.sendNotification_EventApproved(chatID, this.entity, itemUrl);
} else {
await events.sendNotification_EventRejected(chatID , this.entity, itemUrl);
}
await events.persist();
} catch (e) {
if (ErrorHandler.is_412_PRECONDITION_FAILED(e)) {

View File

@ -45,6 +45,23 @@ $eventBorderRadius: 6px;
text-overflow: ellipsis;
}
.text_overflow {
white-space: nowrap;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal;
width: auto;
}
.icon {
display: flex;
align-items: center;
}
.recur {
text-align: end;
}

View File

@ -20,9 +20,12 @@ interface IProps {
endsIn: boolean;
timeStringOverride?: string;
size?: EventBarSize;
selectedTemplateKeys?: string[];
type?: string;
}
export const EventBar: FC<IProps> = ({ event, startsIn, endsIn, timeStringOverride, size = EventBarSize.Compact }) => {
export const EventBar: FC<IProps> = ({ event, startsIn, endsIn, timeStringOverride, size = EventBarSize.Compact, type, selectedTemplateKeys }) => {
const { palette: { themePrimary } } = useTheme();
const { active: { useApprovals } } = useConfigurationService();
@ -47,21 +50,42 @@ export const EventBar: FC<IProps> = ({ event, startsIn, endsIn, timeStringOverri
const startTimeString = timeStringOverride ||
(size === EventBarSize.Compact
? (!isAllDay && start?.format('LT'))
? (!isAllDay ? `${start?.format('LT')} - ${end?.format('LT')}`: strings.AllDay)
: isAllDay ? strings.AllDay : `${start?.format('LT')} - ${end?.format('LT')}`
);
const showLockIcon = isConfidential;
const showRepeatIcon = isRecurring;
const growTitle = !isConfidential && !isRecurring;
return (
<Stack className={eventClassName} style={style} tokens={useConst({ childrenGap: 2 })}>
<Stack horizontal verticalAlign="center" title={title} tokens={useConst({ childrenGap: 6 })}>
{tag && <span>[{tag}]</span>}
{selectedTemplateKeys && selectedTemplateKeys.includes('tag') && size === EventBarSize.Compact && tag && <span>[{tag}]</span>}
{/* {((size == EventBarSize.Compact && type=== "Quarter") || (size !== EventBarSize.Compact)) && tag && <span>[{tag}]</span>} */}
{(size !== EventBarSize.Compact) && tag && <span>[{tag}]</span>}
<StackItem className={styles.text}>
{size === EventBarSize.Compact && startTimeString && `${startTimeString}, `}
{title}
{selectedTemplateKeys && selectedTemplateKeys.includes('starttime') && size === EventBarSize.Compact && startTimeString && `${startTimeString} `}
</StackItem>
{isConfidential && <LockIcon />}
<StackItem grow className={styles.recur}>
{isRecurring && <RepeatAllIcon />}
</Stack>
<Stack horizontal verticalAlign="center" title={title} tokens={useConst({ childrenGap: 4 })}>
<StackItem grow={growTitle} className={styles.text}>
<span className={styles.text_overflow}> {title} </span>
</StackItem>
{isConfidential && (<StackItem className={styles.icon}>
<LockIcon />
</StackItem>)}
{isRecurring && (
<StackItem className={styles.icon}>
<RepeatAllIcon />
</StackItem>)}
</Stack>
<Stack horizontal verticalAlign="center" title={title} tokens={useConst({ childrenGap: 4 })}>
<StackItem className={styles.text}>
{selectedTemplateKeys && selectedTemplateKeys.includes('location') && size === EventBarSize.Compact &&
<> <POIIcon />
<span className={styles.text}>{location || '-'}</span>
</> }
</StackItem>
</Stack>
{size === EventBarSize.Large && <>

View File

@ -13,6 +13,7 @@ export interface IEventDetailsCallout {
interface IProps {
componentRef: RefObject<IEventDetailsCallout>;
commands: IEventCommands;
// channels: readonly ChannelsConfigurations[]; enable for share event
}
export const EventDetailsCallout: FC<IProps> = ({ componentRef, commands }) => {
@ -50,6 +51,7 @@ export const EventDetailsCallout: FC<IProps> = ({ componentRef, commands }) => {
viewCommand,
addToOutlookCommand,
getLinkCommand
// shareCommand //uncomment to share functionality
] = useEventCommandActionButtons(commands, event);
return (isOpen &&
@ -67,6 +69,7 @@ export const EventDetailsCallout: FC<IProps> = ({ componentRef, commands }) => {
{viewCommand}
{addToOutlookCommand}
{getLinkCommand}
{/* {shareCommand} */}
</Stack>
</FocusZone>
</FocusTrapCallout>

View File

@ -2,6 +2,7 @@ import { FC, ReactElement } from "react";
import { Entity, MomentRange, User } from "common";
import { Approvers, Event, EventOccurrence, Refiner, RefinerValue } from "model";
import { useConfigurationService, useDirectoryService } from "services";
import { useTimeZoneService } from "services";
interface IProps {
events: readonly Event[];
@ -9,17 +10,46 @@ interface IProps {
refiners: readonly Refiner[];
selectedRefinerValues: Set<RefinerValue>;
approvers: readonly Approvers[];
searchText: string;
viewType?: string;
exactMatch: boolean;
selectedItem: any;
siteTimeZone?: string;
children: (cccurrences: readonly EventOccurrence[]) => ReactElement;
}
export const EventFilter: FC<IProps> = ({ events, dateRange, refiners, selectedRefinerValues, approvers, children }) => {
export const EventFilter: FC<IProps> = ({ events, dateRange, refiners, selectedRefinerValues, approvers, searchText, viewType, exactMatch, selectedItem, siteTimeZone, children}) => {
const { currentUser, currentUserIsSiteAdmin } = useDirectoryService();
const { active: { useApprovals, useRefiners } } = useConfigurationService();
const currentUserApprovers = approvers.filter(a => a.userIsAnApprover(currentUser));
const { isDifferenceInTimezone } = useTimeZoneService();
const filteredEventOccurrences = events
.filter(event => !event.isSeriesException)
.filter(event => {
//if (viewType !== 'list') {
return !event.isSeriesException;
// }
// return true;
})
.filter(Entity.NotDeletedFilter)
// .filter(event => {
// if(searchText !== ""){
// if(event.displayName.toLowerCase().includes(searchText.toLowerCase()) || event.description.toLowerCase().includes(searchText.toLowerCase()) || event.location.toLowerCase().includes(searchText.toLowerCase())){
// return event.displayName !== undefined;
// }
// if (event.contacts.length > 0){
// for(var i=0; i<event.contacts.length; i++){
// if((event.contacts[i].title.toLowerCase().includes(searchText.toLowerCase())) || (event.contacts[i].email.toLowerCase().includes(searchText.toLowerCase()))){
// return event.displayName !== undefined;
// }
// }
// }
// }
// else{
// return event.displayName !== undefined;
// }
// })
.filter(event => {
if (event.isApproved) {
return true;
@ -38,7 +68,24 @@ export const EventFilter: FC<IProps> = ({ events, dateRange, refiners, selectedR
return false;
}
})
.flatMap(event => event.expandOccurrences(dateRange))
.flatMap(event => event.expandOccurrences(isDifferenceInTimezone, dateRange, viewType, siteTimeZone))
.filter(occurrence => {
if(searchText !== ""){
if(occurrence.event.displayName.toLowerCase().includes(searchText.toLowerCase()) || occurrence.event.description.toLowerCase().includes(searchText.toLowerCase()) || occurrence.event.location.toLowerCase().includes(searchText.toLowerCase())){
return occurrence.event.displayName !== undefined;
}
if (occurrence.event.contacts.length > 0){
for(var i=0; i<occurrence.event.contacts.length; i++){
if((occurrence.event.contacts[i].title.toLowerCase().includes(searchText.toLowerCase())) || (occurrence.event.contacts[i].email.toLowerCase().includes(searchText.toLowerCase()))){
return occurrence.event.displayName !== undefined;
}
}
}
}
else{
return occurrence.event.displayName !== undefined;
}
})
.filter(occurrence => {
const valuesByRefiner = occurrence.event.valuesByRefiner();
return !useRefiners || refiners.every(refiner => {

View File

@ -1,5 +1,13 @@
@import '../common.module';
@mixin eventDayView-font() {
color: black;
@include ms-fontSize-12;
line-height: 16px;
overflow: hidden;
white-space: nowrap;
}
.root {
position: relative;
@ -23,4 +31,11 @@
text-decoration: line-through;
}
}
.textDayView {
@include eventDayView-font();
text-overflow: ellipsis;
display: block;
}
}

View File

@ -9,7 +9,7 @@ import { useConfigurationService } from 'services';
import { RefinerValuePill } from '../refiners';
import { EventOverview as strings } from 'ComponentStrings';
import { Humanize as _strings } from "ComponentStrings";
import styles from './EventOverview.module.scss';
interface IProps {
@ -71,7 +71,7 @@ export const EventOverview: FC<IProps> = ({ event, className }) => {
{isRecurring &&
<Stack horizontal verticalAlign='center' tokens={iconTextStackTokens}>
<Text><RepeatAllIcon /></Text>
<Text data-is-focusable>{event.getSeriesMaster().start.format('LT')} - {event.getSeriesMaster().end.format('LT')}, {humanizeRecurrencePattern(start, recurrence)}</Text>
<Text data-is-focusable>{ isAllDay ? `${_strings.AllDay}, ${humanizeRecurrencePattern(event.getSeriesMaster().start, recurrence)}` : `${event.getSeriesMaster().start.format('LT')} - ${event.getSeriesMaster().end.format('LT')}, ${humanizeRecurrencePattern(event.getSeriesMaster().start, recurrence)}`}</Text>
</Stack>
}
{location &&

View File

@ -1,6 +1,8 @@
import { IEvent } from "model";
export type EventCommand = (event: IEvent) => void;
export type EventCommand = (event: IEvent, timeZoneDiff?: any) => void;
//export type ChannelsEventCommand = (event: IEvent, channelId?: string, groupId?: string, timeZoneDiff?: any) => Promise<string>;
export interface IEventCommands {
view: EventCommand;
@ -8,5 +10,7 @@ export interface IEventCommands {
reject: EventCommand;
addToOutlook: EventCommand;
addSeriesToOutlook: EventCommand;
// sharedEventLink: ChannelsEventCommand;
getLink: EventCommand;
configEnableOutlook : boolean;
}

View File

@ -0,0 +1,19 @@
@import '../common.module';
.save_btn{
top:2px;
margin-top: 60px;
margin-bottom: 20px;
}
.cancel_btn{
top:0px;
margin-top: 60px;
margin-bottom: 20px;
}
.container_message{
height: 32px;
opacity: 0;
overflow: hidden
}

View File

@ -1,14 +1,30 @@
import React, { useMemo } from "react";
import { ActionButton } from "@fluentui/react";
import React, { useMemo, useState } from "react";
import { ActionButton, PrimaryButton, DefaultButton, MessageBar, MessageBarType, Dropdown, IDropdownOption, IDropdownStyles } from "@fluentui/react";
import { IEvent } from "model";
import { IEventCommands } from "../events/IEventCommands";
import { EventCommands as strings } from 'ComponentStrings';
import { Dialog, DialogType, DialogFooter } from '@fluentui/react/lib/Dialog';
import styles from "./useEventCommandActionButtons.module.scss";
import {
useConfigurationService
} from "services";
export const useEventCommandActionButtons = (commands: IEventCommands, event: IEvent | undefined) => {
const { view, addToOutlook, addSeriesToOutlook, getLink } = commands;
const { view, addToOutlook, addSeriesToOutlook, getLink, configEnableOutlook} = commands;
const { isApproved, isSeriesMaster, isRecurring } = event || {};
const canAddToOutlook = isApproved;
const [linkShared,setLinkShared]= useState(false);
const [isShared,setIsShared]= useState(false);
//const [channelId, setChannelId] = useState("");
const [groupId, setGroupId] = useState("");
const [isSuccess,setIsSuccess]= useState(false);
const [isError,setIsError]= useState(false);
const [errorMessage,setErrorMessage]= useState("");
const [selectedItem, setSelectedItem] = React.useState<IDropdownOption>();
const [isSaveBtnEnable,setIsSaveBtnEnable]= useState(false);
const { active: config } = useConfigurationService();
const enableAddToOutlook = config && config.useAddToOutlook;
const viewCommand = useMemo(() =>
<ActionButton iconProps={{ iconName: "View" }} onClick={() => view(event)}>
@ -52,7 +68,7 @@ export const useEventCommandActionButtons = (commands: IEventCommands, event: IE
);
const addToOutlookCommand = useMemo(() =>
canAddToOutlook && (
enableAddToOutlook && canAddToOutlook && (
isRecurring
? (isSeriesMaster
? addToOutlookSeriesCommand
@ -60,7 +76,7 @@ export const useEventCommandActionButtons = (commands: IEventCommands, event: IE
)
: addToOutlookSingleCommand
),
[canAddToOutlook, isRecurring, isSeriesMaster, addToOutlookRecurringCommand, addToOutlookSingleCommand]
[canAddToOutlook, isRecurring, isSeriesMaster, addToOutlookRecurringCommand, addToOutlookSingleCommand, enableAddToOutlook]
);
const getLinkCommand = useMemo(() =>
@ -70,9 +86,125 @@ export const useEventCommandActionButtons = (commands: IEventCommands, event: IE
[event, getLink]
);
// const dialogContentProps = {
// type: DialogType.normal,
// title: 'Select a channel',
// closeButtonAriaLabel: 'Close',
// maxWidth: '100%',
// width: 'auto',
// showCloseButton: true
// };
// const onChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
// setSelectedItem(item);
// setChannelId(item.key.toString());
// setGroupId(item.id);
// setIsSaveBtnEnable(true);
// };
// const on_Dismiss = (): void => {
// setLinkShared(false);
// setIsShared(false);
// setErrorMessage("");
// setSelectedItem(null);
// setChannelId("");
// setGroupId("");
// setIsSuccess(false);
// setIsError(false);
// setIsSaveBtnEnable(false);
// };
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 300 } };
//enable to share event
// const shareToTeams = useMemo(() =>
// <Dialog
// hidden={!linkShared}
// onDismiss={on_Dismiss}
// dialogContentProps={dialogContentProps}
// maxWidth={540}
// >
// {!isSuccess && !isError ? (<div className={styles.container_message}></div>):null}
// {isSuccess ?
// <MessageBar messageBarType={MessageBarType.success}> Event details posted successfully! </MessageBar>:<div></div>}
// {isError ?
// <MessageBar messageBarType={MessageBarType.error}> Error: {errorMessage ? errorMessage:"Please check access"} </MessageBar>:<div></div>}
// <Dropdown
// label="Select one"
// selectedKey={selectedItem ? selectedItem.key : undefined}
// onChange={onChange}
// placeholder="Select an option"
// options={channels.map(x => ({ id: x.teamsId, text: x.channelName, key:x.channelId }))}
// styles={dropdownStyles}
// />
// <DialogFooter>
// <PrimaryButton disabled={!isSaveBtnEnable} className={styles.save_btn} iconProps={{ iconName: isShared ? "CheckMark":"Share" }} onClick={async () => {
// setIsShared(true);
// setIsSaveBtnEnable(false);
// const isSharedMessage= await sharedEventLink(event, channelId, groupId);
// if(isSharedMessage === "Success"){
// setIsSuccess(true);
// setIsError(false);
// setIsShared(true);
// }
// else if(isSharedMessage === "InsufficientPrivileges"){
// setIsError(true);
// setIsSuccess(false);
// setErrorMessage("Please verify access on channel");
// }
// else if(isSharedMessage === "teamId needs to be a valid GUID."){
// setIsError(true);
// setIsSuccess(false);
// setErrorMessage("Teams Id/Channel Id is invalid");
// }
// else{
// setIsError(true);
// setIsSuccess(false);
// setErrorMessage("Internal Error: " + isSharedMessage);
// }
// setTimeout(() => {
// //setSelectedItem(null);
// setIsShared(false);
// setIsSuccess(false);
// setIsError(false);
// setErrorMessage("");
// }, 4000);
// }} text="Send" />
// <DefaultButton className={styles.cancel_btn} onClick={on_Dismiss} text="Don't send" />
// </DialogFooter>
// </Dialog>,
// [event, sharedEventLink,linkShared,channelId, groupId,isShared,isSuccess,isError,errorMessage,selectedItem,isSaveBtnEnable]
// );
//enable to share event channel
// const shareCommand = useMemo(() =>
// <>
// <ActionButton disabled={false/*!window.location.href.includes("teamshostedapp.aspx")*/} iconProps={{ iconName: "Share" }} onClick={() =>
// {
// setLinkShared(true);
// }}>
// <div style={{ width: "40px" }}>
// {strings.Command_Share.Text}
// </div>
// </ActionButton>
// {shareToTeams}
// </>
// ,
// [event, sharedEventLink,linkShared,channelId, groupId,isShared,isSuccess,isError,errorMessage,selectedItem,isSaveBtnEnable]
// );
return [
viewCommand,
addToOutlookCommand,
getLinkCommand
//shareCommand //uncomment to share functionality
] as const;
};

View File

@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react";
import { IComponent, parseIntOrDefault } from "common";
import { Event } from "model";
import { useEventsService, useTeamsJS } from "services";
import { useTimeZoneService } from "services";
const eventIdParam = 'eventid';
const recurrenceDateParam = 'recurrencedate';
@ -15,6 +16,7 @@ export const useExecuteEventDeepLink = (displayEvent: (event: Event) => Promise<
const [eventId, setEventId] = useState<number>();
const [recurrenceDate, setRecurrenceDate] = useState<Moment>();
const [event, setEvent] = useState<Event>();
const { isDifferenceInTimezone } = useTimeZoneService();
useEffect(() => {
const { search } = location;
@ -63,8 +65,8 @@ export const useExecuteEventDeepLink = (displayEvent: (event: Event) => Promise<
useEffect(() => {
if (event) {
console.debug('deep linking to event with ID:', eventId, 'and recurrence date', recurrenceDate?.format());
const eventToDisplay = (recurrenceDate && event.findOrCreateExceptionForDate(recurrenceDate)) || event;
//const { isDifferenceInTimezone } = useTimeZoneService();
const eventToDisplay = (recurrenceDate && event.findOrCreateExceptionForDate(recurrenceDate, isDifferenceInTimezone)) || event;
displayEvent(eventToDisplay).finally(eraseEventFromQueryString);
}
}, [event, eventId, recurrenceDate]);

View File

@ -2,12 +2,13 @@ import { useCallback, useRef } from "react";
import { useForceUpdate } from "@fluentui/react-hooks";
import { useConfigurationService, useDirectoryService } from "services";
import { IConfigureApproversPanel } from "../approvals";
//import { IConfigureChannelsPanel } from "../ChannelsConfiguration";
import { ISettingsPanel } from "../settings";
export const useSettings = () => {
const forceUpdate = useForceUpdate();
const { active: config } = useConfigurationService();
const { currentUserIsSiteAdmin } = useDirectoryService();
const { currentUserIsSiteAdmin, userHasEditPermisison } = useDirectoryService();
const userCanManageSettings = currentUserIsSiteAdmin;
@ -20,11 +21,14 @@ export const useSettings = () => {
}, [config, settingsPanel, forceUpdate]);
const configureApproversPanel = useRef<IConfigureApproversPanel>();
//const configureChannelsPanel = useRef<IConfigureChannelsPanel>(); //enable to share event
return [
userCanManageSettings,
settingsPanel,
configureApproversPanel,
//configureChannelsPanel, //enable to share event
userHasEditPermisison,
editSettings
] as const;
};

View File

@ -29,6 +29,7 @@ declare module 'ComponentStrings' {
Week: string;
Month: string;
Quarter: string;
List: string;
}
interface IViewRouteStrings {
@ -125,6 +126,8 @@ declare module 'ComponentStrings' {
Command_AddToOutlook_Recurring_Series: IButtonStrings;
Command_AddToOutlook_Recurring_Instance: IButtonStrings;
Command_GetLink: IButtonStrings;
Command_Share: IButtonStrings;
Command_Shared: IButtonStrings;
}
interface IEventPanelStrings {
@ -195,6 +198,13 @@ declare module 'ComponentStrings' {
NoReasonGiven: string;
EventDetailsHeading: string;
},
ApprovedEmail: {
Subject: string;
Intro: string;
EventLinkText: string;
CommentGiven: string;
EventDetailsHeading: string;
},
EventDetails: {
EventName: string;
Location: string;
@ -234,6 +244,33 @@ declare module 'ComponentStrings' {
Command_Discard: IButtonStrings;
Command_Delete: IButtonStrings;
}
//enable to share event
// interface IChannelsConfigurationPanelStrings {
// //Field_Title_DisplayMode: ITextFieldStrings;
// // Field_Title_EditMode: ITextFieldStrings;
// Field_ChannelName_DisplayMode: ITextFieldStrings; //Newly added
// Field_ChannelName_EditMode: ITextFieldStrings; // Newly added
// Field_TeamsId_DisplayMode: ITextFieldStrings;
// Field_TeamsId_EditMode: ITextFieldStrings;
// Field_ChannelId_DisplayMode: ITextFieldStrings;
// Field_ChannelId__EditMode: ITextFieldStrings;
// Field_TeamsName_DisplayMode: ITextFieldStrings;
// Field_ActualChannelName_DisplayMode: ITextFieldStrings;
// Field_Users: IFieldStrings;
// ApprovalExplanation: string;
// AnyValue: string;
// AnyRefinerValue: string;
// ValueForRefiner: string;
// ValueListConjunction: string;
// Command_Edit: IButtonStrings;
// Command_Save: IButtonStrings;
// Command_Discard: IButtonStrings;
// Command_Delete: IButtonStrings;
// GetTeams_Info: string;
// GetChannel_Info: string;
// GetName_Info: string;
// }
interface IConfigureApproversPanelStrings {
HeaderText: string;
@ -249,7 +286,25 @@ declare module 'ComponentStrings' {
Command_Edit: IButtonStrings;
Command_View: IButtonStrings;
}
// interface IConfigureChannelsPanelStrings {
// HeaderText: string;
// //Column_Title: string;
// Column_ChannelName: string; //Newly added
// Column_TeamsId: string; //Newly added
// Column_ChannelId: string; //Newly added
// Column_TeamsName: string;
// Column_ActualChannelName: string; //Newly added
// Column_Users: string;
// AnyValue: string;
// ValueListConjunction: string;
// Message_Teams: string;
// // AdminApproversMessage_SharePoint: string;
// NoChannelsDefined: string;
// Command_Close: IButtonStrings;
// Command_Add: IButtonStrings;
// Command_Edit: IButtonStrings;
// Command_View: IButtonStrings;
// }
interface IMyApprovalsPanelStrings {
HeaderText: string;
NoEventsToApprove: string;
@ -293,12 +348,24 @@ declare module 'ComponentStrings' {
Field_AllowConfidentialEvents: IToggleFieldStrings;
Field_Refiners: IFieldStrings;
Command_ConfigureApprovers: IButtonStrings;
// Command_ConfigureChannels: IButtonStrings;
Command_AddRefiner: IButtonStrings;
Command_EditRefiner: IButtonStrings;
Command_ReorderRefiner: IButtonStrings;
Command_Edit: IButtonStrings;
Command_Save: IButtonStrings;
Command_Back: IButtonStrings;
Field_ShowFiscalYear: IFieldStrings;
Field_UseApprovalsEmailNotification:IToggleFieldStrings;
Field_UseApprovalsTeamsNotification:IToggleFieldStrings;
// Heading_ChannelsSettings: string;
Heading_ApprovalSettings: string;
Heading_GeneralSettings: string;
Heading_TemplateSettings: string
TeamsChannel_MessageInfo: string;
Field_UseAddToOutlook: IToggleFieldStrings;
Field_ListViewColumn: IFieldStrings;
Field_TemplateView: IFieldStrings;
}
interface ICopyLinkDialogStrings {
@ -358,11 +425,18 @@ declare module 'ComponentStrings' {
ApprovalDialog: IApprovalDialogStrings;
ApproversPanel: IApproversPanelStrings;
ConfigureApproversPanel: IConfigureApproversPanelStrings;
// ConfigureChannelsPanel: IConfigureChannelsPanelStrings;
MyApprovalsPanel: IMyApprovalsPanelStrings;
RefinerPanel: IRefinerPanelStrings;
SettingsPanel: ISettingsPanelStrings;
CopyLinkDialog: ICopyLinkDialogStrings;
Validation: IValidationStrings;
// ChannelsPanel: IChannelsConfigurationPanelStrings;
ProductivityStudioLogo : IProductivityStudioLogoStrings;
}
interface IProductivityStudioLogoStrings {
Command_ProductivityLogoLink: string;
}
const strings: IComponentStrings;

View File

@ -27,7 +27,8 @@ define([], function () {
Day: "Day",
Week: "Week",
Month: "Month",
Quarter: "Quarter"
Quarter: "Quarter",
List: "List View"
},
ViewRoute: {
Command_NewEvent: { Text: "New event" },
@ -136,6 +137,8 @@ define([], function () {
Command_AddToOutlook_Recurring_Series: { Text: "Entire series" },
Command_AddToOutlook_Recurring_Instance: { Text: "Just this occurence" },
Command_GetLink: { Text: "Get link" },
Command_Share: { Text: "Share" },
Command_Shared: { Text: "Shared" }
},
EventPanel: {
NewEvent: "New event",
@ -182,6 +185,7 @@ define([], function () {
Command_AddToOutlook_Recurring_Series: { Text: "Entire series" },
Command_AddToOutlook_Recurring_Instance: { Text: "Just this occurence" },
Command_GetLink: { Text: "Get link" },
Command_Share: { Text: "Share" },
Command_Approval: { Text: "Approval" },
Command_Approval_Approve: { Text: "Approve" },
Command_Approval_Reject: { Text: "Decline" },
@ -192,18 +196,25 @@ define([], function () {
},
ApprovalEmails: {
RequestEmail: {
Subject: "Your approval is requested",
Subject: "Event Approval Requested",
Intro: "An event requiring your approval has been submitted to the {0} by {1}.",
EventLinkText: "Please approve or decline this event.",
EventDetailsHeading: "Event details:"
},
RejectedEmail: {
Subject: "Your event was declined",
Subject: "Event Declined",
Intro: "An event you submitted to the {0} has been declined by {1}",
EventLinkText: "View event in the calendar",
NoReasonGiven: "(none given)",
EventDetailsHeading: "Event details:"
},
ApprovedEmail: {
Subject: "Event Approved",
Intro: "An event you submitted to the {0} has been approved by {1}",
EventLinkText: "View event in the calendar",
CommentGiven: "(none given)",
EventDetailsHeading: "Event details:"
},
EventDetails: {
EventName: "Event",
Location: "Location",
@ -241,6 +252,29 @@ define([], function () {
Command_Discard: { Text: "Discard changes" },
Command_Delete: { Text: "Delete" }
},
// ChannelsPanel: {
// Field_ChannelName_DisplayMode: {Label: "Name"},
// Field_ChannelName_EditMode: {Label: "Please provide a descriptive name (append teams and channel name for identification)"},
// Field_TeamsId_DisplayMode: { Label: "Teams Id" },
// Field_TeamsId_EditMode: { Label: "Please provide the id of the teams" },
// Field_ChannelId_DisplayMode: { Label: "Channel Id" },
// Field_ChannelId__EditMode: { Label: "Please provide the id of the channel" },
// Field_TeamsName_DisplayMode: { Label: "Teams Name" },
// Field_ActualChannelName_DisplayMode: { Label: "Channel Name" },
// Field_Users: { Label: "Users" },
// ApprovalExplanation: "These users can approve events with:",
// AnyValue: "Any value",
// AnyRefinerValue: "Any {0}",
// ValueForRefiner: "{0} for {1}",
// ValueListConjunction: "or",
// Command_Edit: { Text: "Edit" },
// Command_Save: { Text: "Save" },
// Command_Discard: { Text: "Discard changes" },
// Command_Delete: { Text: "Delete" },
// GetTeams_Info: "1.Click on the three dots near the channel name within the Microsoft Teams. '\n' 2. Select 'Get a link to the channel' and copy the link. '\n' 3. Select the 'groupId' value from the copied link and paste it for the teams id field",
// GetChannel_Info: "1. Click on the three dots near the channel name within the Microsoft Teams. '\n' 2. Select 'Get a link to the channel' and copy the link.'\n' 3. Copy the text staring after the 'channel/' from the copy link and select till the.tacv2. '\n' 4. Now paste the link within the channelId field",
// GetName_Info: "Please provide a name to identify the channel from the dropdown selection which appears if click on share button from the event"
// },
ConfigureApproversPanel: {
HeaderText: "Configure Approvers",
Column_Title: "Name",
@ -255,6 +289,21 @@ define([], function () {
Command_Edit: { Text: "Edit" },
Command_View: { Text: "View" },
},
// ConfigureChannelsPanel: {
// HeaderText: "Configure Teams Channel",
// Column_ChannelName: "Name",
// Column_TeamsId: "Teams Id",
// Column_ChannelId: "Channel Id",
// Column_TeamsName: "Teams Name",
// Column_ActualChannelName: "Channel Name",
// ValueListConjunction: "or",
// Message_Teams: "The configured teams/channel id should be provided correctly to successfully share the events. Also the teams name and channel name will be auto populated based on the provided ids if necessary permissions are granted from the Admin centre of SharePoint",
// NoChannelsDefined: "You have not configured any channels.",
// Command_Close: { Text: "Close", Tooltip: "Close", AriaLabel: "close" },
// Command_Add: { Text: "New" },
// Command_Edit: { Text: "Edit" },
// Command_View: { Text: "View" },
// },
MyApprovalsPanel: {
HeaderText: "Events needing your approval",
NoEventsToApprove: "You're all caught up. There are no events pending approval.",
@ -287,21 +336,34 @@ define([], function () {
},
SettingsPanel: {
Heading: "Settings",
Field_FiscalYear: { Label: "First month of your fiscal year", Tooltip: "Determines the fiscal quarter for the calendar view" },
Field_FiscalYear: { Label: "First month of fiscal year", Tooltip: "Determines the fiscal quarter for the calendar view" },
Field_DefaultView: { Label: "Initial view", Tooltip: "Default view that should appear to all users when the app starts" },
Field_UseRefiners: { Label: "Use refiners", OnText: "Yes", OffText: "No", Tooltip: "Turn refiners on or off" },
Field_RefinerRailInitialDisplay: { Label: "Refiners rail initial display", OnText: "Expanded", OffText: "Collapsed", Tooltip: "Whether the refiner rail will initially display expanded or collapsed when the app starts" },
Field_QuarterViewGroupByRefiner: { Label: "Quarter view - group by", Tooltip: "Determines how events are grouped in the quarter view" },
Field_UseApprovals: { Label: "Use approvals", OnText: "Yes", OffText: "No", Tooltip: "Turn on or off approval workflow for events" },
// Field_UseChannels: { Label: "Use Channels to Share", OnText: "Yes", OffText: "No", Tooltip: "Turn on or off channel to share events" },
Field_AllowConfidentialEvents: { Label: "Allow confidential events", OnText: "Yes", OffText: "No", Tooltip: "Turn on or off the ability for users to create events that are only visible to specific people or groups" },
Field_Refiners: { Label: "Refiners" },
Command_ConfigureApprovers: { Text: "Configure Approvers", Tooltip: "Create an approval matrix to define who will approve which events" },
Command_ConfigureChannels: { Text: "Configure Channel", Tooltip: "Create a channel matrix to define which teams and channel can be configure" },
Heading_ChannelsSettings: "Teams Channel Settings",
Heading_ApprovalSettings: "Approval Settings",
Heading_GeneralSettings: "General Settings",
Heading_TemplateSettings: "Template View Settings (for Month and Quarter)",
Command_AddRefiner: { Text: "Add refiner" },
Command_EditRefiner: { Text: "Edit refiner", Tooltip: "Edit refiner", AriaLabel: "edit refiner" },
Command_ReorderRefiner: { AriaLabel: "reorder this refiner" },
Command_Edit: { Text: "Edit" },
Command_Save: { Text: "Save" },
Command_Back: { Text: "Back" }
Command_Back: { Text: "Back" },
Field_ShowFiscalYear: { Label: "Show fiscal year as", Tooltip: "Determines the fiscal year for the calendar view" },
Field_UseApprovalsTeamsNotification: { Label: "Teams notification", OnText: "Yes", OffText: "No", Tooltip: "Turn on or off approval team notification for events" },
Field_UseApprovalsEmailNotification: { Label: "Email notification", OnText: "Yes", OffText: "No", Tooltip: "Turn on or off approval email notification for events" },
TeamsChannel_MessageInfo : "This screen can be used to configure the teams channel into which the user can share the event details by selecting the channel from the dropdown which appears on click of the event",
Field_UseAddToOutlook: { Label: "Enable Add to Outlook", OnText: "Yes", OffText: "No", Tooltip: "Enable the add to outlook feature for downloading the ics file and add as an outlook invite" },
Field_ListViewColumn: { Label: "Select List column view", Tooltip: "Select columns to display for list view" },
Field_TemplateView: { Label: "Select template view", Tooltip: "Select template to display for month view" }
},
CopyLinkDialog: {
Title: "Copy a link to \"{0}\"",
@ -335,6 +397,10 @@ define([], function () {
NotValid: "Refiner selections are not valid",
Required: "This is required"
}
},
ProductivityStudioLogo: {
Command_ProductivityLogoLink: "Microsoft Productivity Studio"
}
};
});

View File

@ -1,5 +1,15 @@
@import '../panels.module';
.templateView {
// position: absolute;
background-color: "[theme: themePrimary, default: #005a9e]";
color: #ffffff;
// width: 184px;
// padding: 5px;
// border-radius: 5px;
// font-size: 14px;
}
.refiners {
.refiner {
height: 32px;
@ -21,3 +31,4 @@
}
}
}

View File

@ -1,18 +1,27 @@
import { months } from 'moment-timezone';
import React, { RefObject } from 'react';
import { DefaultButton, ICommandBarItemProps, IDropdownOption, Label, TooltipHost } from "@fluentui/react";
import { months} from 'moment-timezone';
import React, { CSSProperties, RefObject } from 'react';
import { ComboBox, DefaultButton, IComboBox, IComboBoxOption, IComboBoxStyles, ICommandBarItemProps, IDropdownOption, Label, SelectableOptionMenuItemType, TooltipHost } from "@fluentui/react";
import { arrayToMap, Entity, ErrorHandler } from 'common';
import { EntityPanelBase, IEntityPanelProps, IDataPanelBaseState, ResponsiveGrid, GridRow, GridCol, IDataPanelBase, LiveToggle, LiveDropdown } from "common/components";
import { ReadonlyRefinerMap, Refiner } from 'model';
import { EntityPanelBase, IEntityPanelProps, IDataPanelBaseState, ResponsiveGrid, GridRow, GridCol, IDataPanelBase, LiveToggle, LiveDropdown, LiveComboBox, LiveMultiselectDropdown } from "common/components";
import { ListViewKeys, ReadonlyRefinerMap, Refiner } from 'model';
import { withServices, ServicesProp, ConfigurationServiceProp, ConfigurationService, EventsServiceProp, EventsService } from 'services';
import { Configuration } from 'schema';
import { IConfigureApproversPanel } from '../approvals';
//import { IConfigureChannelsPanel } from '../ChannelsConfiguration';
import { ViewDescriptors, ViewDescriptorsById } from '../views';
import { RefinerEditor } from './RefinerEditor';
import { ViewYearFYKeys } from "model";
import { InfoIcon } from '@fluentui/react-icons-mdl2';
import { PersistConcurrencyFailureMessage, SettingsPanel as strings } from "ComponentStrings";
import { TemplateViewKeys } from 'model/TemplateViewKeys';
import styles from './SettingsPanel.module.scss';
const templateViewImg = require('assets/onboarding/ETemplateView.png');
const infoIconStyle: CSSProperties = {
fontSize: 12,
marginLeft: 4
};
const refinerToDropdownOption = (refiner: Refiner) => {
const { id: key, displayName: text } = refiner;
@ -34,6 +43,13 @@ const fiscalYearDropdownOptions: IDropdownOption[] = monthNames.map((name, idx)
};
});
const fiscalYearShowDropdownOptions: IDropdownOption[] = Object.values(ViewYearFYKeys).map((name, idx) => {
return {
key: name,
text: name
};
});
export interface ISettingsPanel extends IDataPanelBase<Configuration> {
}
@ -42,6 +58,9 @@ interface IOwnProps {
onNewRefiner: () => void;
onEditRefiner: (refiner: Refiner) => void;
configureApproversPanel: RefObject<IConfigureApproversPanel>;
selectedTemplateKeys?: string[];
// configureChannelsPanel: RefObject<IConfigureChannelsPanel>;
}
type IProps = IOwnProps & IEntityPanelProps<Configuration> & ServicesProp<ConfigurationServiceProp & EventsServiceProp>;
@ -49,9 +68,47 @@ interface IOwnState {
groupByRefinerOptions: IDropdownOption[];
refiners: Refiner[];
refinersById: ReadonlyRefinerMap;
selectedKeys: string[];
selectedTemplateKeys: string[];
}
type IState = IOwnState & IDataPanelBaseState<Configuration>;
const comboBoxStyles: Partial<IComboBoxStyles> = { root: { maxWidth: 300 } };
const options: IComboBoxOption[] = [
// { key: 'selectAll', text: 'Select All', itemType: SelectableOptionMenuItemType.SelectAll },
{ key: 'isAllDay', text: 'Is All Day' },
{ key: 'isApproved', text: 'Is Approved' },
{ key: 'isRejected', text: 'Is Rejected' },
{ key: 'isConfidential', text: 'Is Confidential' },
{ key: 'isRecurring', text: 'Is Recurring' },
{ key: 'contacts', text: 'Event Contacts' },
{ key: 'description', text: 'Event description' },
{ key: 'eventEndTime', text: 'End Time' },
{ key: 'location', text: 'Location' },
{ key: 'recurrence', text: 'Recurrence Type' },
{ key: 'refinerValues', text: 'Refiner Values' },
{ key: 'eventStartDate', text: 'Start Time' },
{ key: 'tag', text: 'Tag' },
{ key: 'title', text: 'Title' },
{ key: 'created', text: 'Created' },
{ key: 'createdBy', text: 'Created By' },
{ key: 'modified', text: 'Modified' },
{ key: 'modifiedBy', text: 'Modified By' },
];
const viewValue: IComboBoxOption[] = [
// { key: 'eventTitle', text: 'Event Title' },
{ key: 'tag', text: 'Tag' },
{ key: 'location', text: 'Location' },
{ key: 'starttime', text: 'Start Time - End Time' },
];
const selectableOptions = options.filter(
option =>
(option.itemType === SelectableOptionMenuItemType.Normal || option.itemType === undefined) && !option.disabled,
);
class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> implements ISettingsPanel {
protected get title() {
return strings.Heading;
@ -64,10 +121,40 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
...super.resetState(),
groupByRefinerOptions: [],
refiners: [],
refinersById: new Map()
refinersById: new Map(),
selectedKeys: ['displayName'],
selectedTemplateKeys:['eventTitle']
};
}
// public onChange = (
// event: React.FormEvent<HTMLDivElement>,
// option?: IComboBoxOption,
// index?: number,
// value?: string,
// ) => {
// const { selectedKeys } = this.state;
// const selected = option?.selected;
// const currentSelectedOptionKeys = selectedKeys.filter(key => key !== 'selectAll');
// const selectAllState = currentSelectedOptionKeys.length === selectableOptions.length;
// if (option) {
// if (option?.itemType === SelectableOptionMenuItemType.SelectAll) {
// selectAllState
// ? this.setState({ selectedKeys: [] })
// : this.setState({ selectedKeys: ['selectAll', ...selectableOptions.map(o => o.key as string)] });
// } else {
// const updatedKeys = selected
// ? [...currentSelectedOptionKeys, option.key as string]
// : currentSelectedOptionKeys.filter(k => k !== option.key);
// if (updatedKeys.length === selectableOptions.length) {
// updatedKeys.push('selectAll');
// }
// this.setState({ selectedKeys: updatedKeys });
// }
// }
// };
public componentShouldRender() {
super.componentShouldRender();
this._buildGroupByRefinerOptions();
@ -119,6 +206,8 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
private _openConfigureApprovers = () =>
this.props.configureApproversPanel.current?.open();
// private _openConfigureChannels = () =>
// this.props.configureChannelsPanel.current?.open(); //enable to share event
protected renderEditContent(): JSX.Element {
const { onNewRefiner, onEditRefiner } = this.props;
@ -131,10 +220,12 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
updateField: this.updateFieldAndSubmit
};
return (
<ResponsiveGrid className={styles.content}>
<div> <p><b> {strings.Heading_GeneralSettings} </b></p></div>
<GridRow>
<GridCol sm={12} lg={4}>
<GridCol sm={12} lg={3}>
<LiveDropdown
label={strings.Field_DefaultView.Label}
tooltip={strings.Field_DefaultView.Tooltip}
@ -145,7 +236,7 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
renderValue={v => ViewDescriptorsById.get(v).title}
/>
</GridCol>
<GridCol sm={12} lg={5}>
<GridCol sm={12} lg={4}>
<LiveDropdown
label={strings.Field_FiscalYear.Label}
tooltip={strings.Field_FiscalYear.Tooltip}
@ -156,9 +247,20 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
renderValue={v => monthNames[v]}
/>
</GridCol>
<GridCol sm={12} lg={5}>
<LiveDropdown
label={strings.Field_ShowFiscalYear.Label}
tooltip={strings.Field_ShowFiscalYear.Tooltip}
{...liveProps}
options={fiscalYearShowDropdownOptions}
propertyName='fiscalYearStartYear'
getKeyFromValue={v => v}
renderValue={v => ViewYearFYKeys[v]}
/>
</GridCol>
</GridRow>
<GridRow>
<GridCol sm={6} lg={4}>
<GridCol sm={6} lg={3}>
<LiveToggle
{...liveProps}
label={strings.Field_UseRefiners.Label}
@ -168,7 +270,7 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
propertyName='useRefiners'
/>
</GridCol>
<GridCol sm={6} lg={4}>
<GridCol sm={6} lg={5}>
<LiveToggle
{...liveProps}
label={strings.Field_RefinerRailInitialDisplay.Label}
@ -195,6 +297,50 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
/>
</GridCol>
</GridRow>
<GridRow>
<GridCol sm={6} lg={4}>
<LiveToggle
{...liveProps}
label={strings.Field_AllowConfidentialEvents.Label}
onText={strings.Field_AllowConfidentialEvents.OnText}
offText={strings.Field_AllowConfidentialEvents.OffText}
tooltip={strings.Field_AllowConfidentialEvents.Tooltip}
propertyName='allowConfidentialEvents'
/>
</GridCol>
<GridCol sm={6} lg={4}>
<LiveToggle
{...liveProps}
label={strings.Field_UseAddToOutlook.Label}
onText={strings.Field_UseAddToOutlook.OnText}
offText={strings.Field_UseAddToOutlook.OffText}
tooltip={strings.Field_UseAddToOutlook.Tooltip}
propertyName='useAddToOutlook'
/>
</GridCol>
<GridCol sm={6} lg={4}>
<LiveMultiselectDropdown
label={strings.Field_ListViewColumn.Label}
tooltip={strings.Field_ListViewColumn.Tooltip}
{...liveProps}
options={options}
multiSelect
propertyName='listViewColumn'
getKeyFromValue={val => val}
// placeholder={anyValueString}
// onRenderTitle={() => <>{humanizedString(selectedValues)}</>}
renderValue= {(vals) => (
ListViewKeys
)}
/>
</GridCol>
</GridRow>
<div style={{ borderTop: '2px solid #ccc', margin: '10px 0' }}></div>
<div> <p><b> {strings.Heading_ApprovalSettings} </b></p></div>
<GridRow>
<GridCol sm={6} lg={4}>
<LiveToggle
@ -216,17 +362,81 @@ class SettingsPanel extends EntityPanelBase<Configuration, IProps, IState> imple
</GridCol>
</GridRow>
<GridRow>
<GridCol>
<GridCol sm={6} lg={6}>
<LiveToggle
{...liveProps}
label={strings.Field_AllowConfidentialEvents.Label}
onText={strings.Field_AllowConfidentialEvents.OnText}
offText={strings.Field_AllowConfidentialEvents.OffText}
tooltip={strings.Field_AllowConfidentialEvents.Tooltip}
propertyName='allowConfidentialEvents'
label={strings.Field_UseApprovalsTeamsNotification.Label}
onText={strings.Field_UseApprovalsTeamsNotification.OnText}
offText={strings.Field_UseApprovalsTeamsNotification.OffText}
tooltip={strings.Field_UseApprovalsTeamsNotification.Tooltip}
propertyName="useApprovalsTeamsNotification"
disabled={!useApprovals}
defaultChecked={!useApprovals}
/>
</GridCol>
<GridCol sm={6} lg={6}>
<LiveToggle
{...liveProps}
label={strings.Field_UseApprovalsEmailNotification.Label}
onText={strings.Field_UseApprovalsEmailNotification.OnText}
offText={strings.Field_UseApprovalsEmailNotification.OffText}
tooltip={strings.Field_UseApprovalsEmailNotification.Tooltip}
propertyName="useApprovalsEmailNotification"
disabled={!useApprovals}
/>
</GridCol>
</GridRow>
<div style={{ borderTop: '2px solid #ccc', margin: '10px 0' }}></div>
<div> <p><b> {strings.Heading_TemplateSettings} </b></p></div>
<GridRow>
<GridCol sm={12} lg={12}>
<div style={{ display: 'flex', alignItems: 'center' }}> {/* Container for dropdown and image */}
<LiveMultiselectDropdown
label={strings.Field_TemplateView.Label}
tooltip={strings.Field_TemplateView.Tooltip}
{...liveProps}
options={viewValue}
multiSelect
propertyName='templateView'
getKeyFromValue={val => val}
// placeholder={anyValueString}
// onRenderTitle={() => <>{humanizedString(selectedValues)}</>}
renderValue= {(vals) => (
TemplateViewKeys
)}
style={{ width: '188px' }}
/>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '22px', marginLeft:'32px' }}>
<span style={{ marginRight: '15px', position: 'absolute', marginBottom:'100px'}}>Preview</span>
<div className={styles.templateView} style={{ position: 'absolute', width:'184px', padding: '5px', borderRadius: '5px', fontSize: '14px' }}>
{(this.props.selectedTemplateKeys.includes('tag') && this.props.selectedTemplateKeys.includes('starttime')) ? <div>[TAG] [Start Time - End Time]</div> : this.props.selectedTemplateKeys.includes('tag') ? <div>[TAG]</div> : this.props.selectedTemplateKeys.includes('starttime') ? <div>[Start Time - End Time]</div> : null}
<div>[Event Name]</div>
{this.props.selectedTemplateKeys.includes('location') && <div>[Location]</div>}
</div>
{/* <img src={templateViewImg} alt="Template Preview" style={{ height: '60px', width: '220px' }} /> */}
</div>
</div>
</GridCol>
</GridRow>
{/* enable this code for the share option */}
{/* <div style={{ borderTop: '2px solid #ccc', margin: '10px 0' }}></div>
<div> <p><b> {strings.Heading_ChannelsSettings} </b>
<TooltipHost content={strings.TeamsChannel_MessageInfo}>
{<InfoIcon style={infoIconStyle} tabIndex={0} />}
</TooltipHost></p></div>
<GridRow className=''>
<GridCol sm={6} lg={8}>
<TooltipHost content={strings.Command_ConfigureChannels.Tooltip}>
<DefaultButton onClick={this._openConfigureChannels} >
{strings.Command_ConfigureChannels.Text}
</DefaultButton>
</TooltipHost>
</GridCol>
</GridRow> */}
<div style={{ borderTop: '2px solid #ccc', margin: '36px 0 -6px' }}></div>
{useRefiners &&
<GridRow>
<GridCol>

View File

@ -3,7 +3,8 @@ import { Moment } from "moment-timezone";
import { IIconProps } from "@fluentui/react";
import { useCallback, useState } from "react";
import { Configuration } from "schema";
import { useConfigurationService } from "services";
import { useConfigurationService, useTimeZoneService } from "services";
import moment from "moment";
export interface IDateRotatorController {
previousIconProps: IIconProps;
@ -14,9 +15,13 @@ export interface IDateRotatorController {
}
export const useDataRotatorController = (controller: IDateRotatorController) => {
const [anchorDate, setAnchorDate] = useState(now());
//const [anchorDate, setAnchorDate] = useState(now());
const { siteTimeZone } = useTimeZoneService();
const [anchorDate, setAnchorDate] = useState(moment().tz(siteTimeZone.momentId));
const { active: config } = useConfigurationService();
//const { siteTimeZone } = useTimeZoneService();
//console.log("siteTimeZone",siteTimeZone);
const onRotatePreviousDate = useCallback(() => {
setAnchorDate(controller.previousDate(anchorDate, config));

View File

@ -10,5 +10,6 @@ export interface IViewDescriptor {
title: string;
dateRotatorController: IDateRotatorController;
dateRange: (anchorDate: Moment, config: Configuration) => MomentRange;
renderer: (props: IViewProps) => JSX.Element;
renderer: ((props: IViewProps) => JSX.Element) | React.ComponentClass<IViewProps>;
}

View File

@ -10,4 +10,10 @@ export interface IViewProps {
selectedRefinerValues: Set<RefinerValue>;
eventCommands: IEventCommands;
viewCommands: IViewCommands;
siteTimeZone?: string;
selectedKeys?: string[];
selectedTemplateKeys?: string[];
onStateChange?: (stateVariable: any) => void;
// channels: readonly ChannelsConfigurations[];
}

View File

@ -9,3 +9,95 @@
padding-right: 20px;
}
}
// .hellotest {
// border:1px solid red;
// }
.shadow{
min-height: 100%;
position: absolute;
top: 0;
left: 0;
min-width: 100%;
z-index: 999;
background-color: rgba(255,255,255,0.6);
}
.search {
float: right;
width: 22%;
min-width: 180px;
}
// .filterDropdown{
// float: right;
// width: 22%;
// // margin-right: 5.5px;
// margin-right: 8px;
// margin-top: -28px;
// margin-bottom: 14px;
// }
.blankSearch{
float: left;
width: 52%;
}
.searchRow{
width: 100%;
}
.searchRow:after{
content: "";
display: table;
clear: both;
}
// .exactMatchRow{
// width: 100%;
// margin-top: -26px !important;
// margin-bottom: 26px;
// }
.blankMatch{
float: left;
width: 90%;
}
.exactMatchCol{
float: right;
width: 10%;
}
.exactMatchBtn:hover {
background-color: rgb(123, 115, 115);
//color: black ;
}
.exactMatchBtn{
height: 16px;
width: 15px !important;
background-color: white ;
}
.normalMatchBtn{
height: 16px;
width: 15px !important;
background-color: #0078d4 !important;
}
.showDropdown{
display: block;
}
.hideDropdown{
display: none;
}
.searchRow {
// display: flex;
align-items: center;
}
.filterDropdown {
margin-right: 10px; /* Adjust as necessary to create space between dropdown and search box */
}
.searchContainer {
display: flex;
align-items: center;
}
.datePickerList {
margin-right: 10px;
width: 160px;
}

View File

@ -4,12 +4,14 @@ import { DayViewDescriptor } from "./day/DayView";
import { WeekViewDescriptor } from "./week/WeekView";
import { MonthViewDescriptor } from "./month/MonthView";
import { QuarterViewDescriptor } from "./quarter/QuarterView";
import { ListViewDescriptor } from "./list/ListView"
export const ViewDescriptors: IViewDescriptor[] = [
DayViewDescriptor,
WeekViewDescriptor,
MonthViewDescriptor,
QuarterViewDescriptor
QuarterViewDescriptor,
ListViewDescriptor
];
export const ViewDescriptorsById = arrayToMap(ViewDescriptors, v => v.id);

View File

@ -23,6 +23,7 @@ const eventCommandsStackItemStyles: IStackItemStyles = {
interface IEventCardProps {
occurrence: EventOccurrence;
commands: IEventCommands,
//channels: readonly ChannelsConfigurations[];
}
const EventCard: FC<IEventCardProps> = ({ occurrence, commands }) => {
@ -54,7 +55,7 @@ const EventCard: FC<IEventCardProps> = ({ occurrence, commands }) => {
const DayView: FC<IViewProps> = ({
cccurrences,
eventCommands,
eventCommands
}) => {
if (cccurrences.length === 0) {
return <Text variant='large'>{strings.DayView.NoEventsMessage}</Text>
@ -68,6 +69,7 @@ const DayView: FC<IViewProps> = ({
key={`${occurrence.event.id}-${occurrence.start.format('L')}`}
occurrence={occurrence}
commands={eventCommands}
// channels ={channels}
/>
)}
</FocusZone>

View File

@ -0,0 +1,126 @@
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
import { useDownloadExcel } from 'react-export-table-to-excel';
import Moment from 'moment-timezone';
import moment from 'moment';
import { Refiner, humanizeRecurrencePattern } from 'model';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react';
import { Humanize as _strings } from 'ComponentStrings';
import { EventOccurrence } from 'model';
import { renderSanitizedHTML } from "common/components/LiveUtils";
interface IExportToExcelProps {
items: any[];
_refiners:readonly Refiner[]
}
const ExportToExcel: React.FC<any> = forwardRef(( props: IExportToExcelProps, ref ) => {
const { _refiners } = props;
const items = [...props.items].sort(EventOccurrence.StartAscComparer)
const tableRef = useRef<HTMLTableElement>(null);
const { onDownload } = useDownloadExcel({
currentTableRef: tableRef.current,
filename: 'Event Details',
sheet: 'Events',
});
useImperativeHandle(ref, () => ({
handleExportExcel
}));
const handleExportExcel = () => {
if (onDownload) {
onDownload();
}
};
const getUniqueEtags = (items: any[]) => {
const etags = items.flatMap(item => item.refinerValues.state.map((rv: any) => rv.etag));
return Array.from(new Set(etags));
};
const renderRefinerValues = (item: any, refiner:any) => {
const matchingDisplayNames = item.refinerValues.state
.filter((refinerItem: any) =>
refiner.values.state.some((valueItem: any) => valueItem.id === refinerItem.id))
.map((matchingItem: any) => matchingItem.displayName)
.join('; ');
return <span>{matchingDisplayNames}</span>;
};
const uniqueEtags = getUniqueEtags(items);
const formatDate = (date: any) => {
return moment(date).format('MM-DD-YYYY HH:mm');
};
return (
<div>
<table ref={tableRef} style={{ borderCollapse: 'collapse', border: '1px solid black', display: 'none' }}>
<thead>
<tr>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Name</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Start Time</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>End Time</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Description</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Is Recurring</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>All Day Event</th>
{_refiners.map((refiner, index) => (
<th key={index} style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>{refiner.displayName}</th>
))}
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Location</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Tag</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Is Rejected</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Event Contacts</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Is Confidential</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Is Approved</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Title</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Recurrence</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Created</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Created By</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Modified</th>
<th style={{ border: '1px solid black', padding: '8px', backgroundColor: '#4CC9E4' }}>Modified By</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => (
<tr key={index}>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.title}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{formatDate(item.start)}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{formatDate(item.end)}</td>
<td style={{ border: '1px solid black', padding: '8px' }} dangerouslySetInnerHTML={{ __html: renderSanitizedHTML(item.description) }}></td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.isRecurring ? 'Yes' : 'No'}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.isAllDay ? 'Yes' : 'No'}</td>
{_refiners.map((refiner, index) => (
<td key={index} style={{ border: '1px solid black', padding: '8px' }}>{renderRefinerValues(item, refiner)}</td>
))}
<td style={{ border: '1px solid black', padding: '8px' }}>{item.location}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.tag}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.isRejected ? 'Yes' : 'No'}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>
{item.contacts.map((contact:any, index:any) => (
<span key={index}>
{contact.title ? contact.title : contact.email}
{index < item.contacts.length - 1 ? '; ' : ''}
</span>
))}
</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.isConfidential ? 'Yes' : 'No'}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.isApproved ? 'Yes' : 'No'}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.title}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>
{item.isRecurring?(item.isAllDay ?
`${_strings.AllDay}, ${humanizeRecurrencePattern(item.getSeriesMaster().start, item.recurrence)}`
: `${item.getSeriesMaster().start.format('LT')} - ${item.getSeriesMaster().end.format('LT')}, ${humanizeRecurrencePattern(item.getSeriesMaster().start, item.recurrence)}`):null}
</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.created.format('MMM D, YYYY')}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.createdBy ? (item.createdBy.title ? item.createdBy.title : (item.createdBy.email ? item.createdBy.email : '')) : ''}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.modified.format('MMM D, YYYY')}</td>
<td style={{ border: '1px solid black', padding: '8px' }}>{item.modifiedBy ? (item.modifiedBy.title ? item.modifiedBy.title : (item.modifiedBy.email ? item.modifiedBy.email : '')) : ''}</td>
</tr>
))}
</tbody>
</table>
</div>
);
});
export default ExportToExcel;

View File

@ -0,0 +1,654 @@
import * as React from 'react';
import { FC, createRef, useCallback, useRef } from 'react';
import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode, IColumn, mergeStyleSets, Icon } from '@fluentui/react';
import { EventOccurrence, ViewKeys } from 'model';
import { IViewDescriptor } from '../IViewDescriptor';
import { IViewProps } from '../IViewProps';
import { ViewNames} from 'ComponentStrings';
import { useTimeZoneService } from 'services';
import moment from "moment";
import {humanizeRecurrencePattern } from 'model';
import { Humanize as _strings } from "ComponentStrings";
import ExportToExcel from './ExportToExcel';
import { renderSanitizedHTML } from "common/components/LiveUtils";
const classNames = mergeStyleSets({
fileIconHeaderIcon: {
padding: 0,
fontSize: '10px',
},
fileIconCell: {
textAlign: 'center',
selectors: {
'&:before': {
content: '.',
display: 'inline-block',
verticalAlign: 'middle',
height: '100%',
width: '0px',
visibility: 'hidden',
},
},
},
fileIconImg: {
verticalAlign: 'middle',
maxHeight: '16px',
maxWidth: '16px',
},
controlWrapper: {
display: 'flex',
flexWrap: 'wrap',
},
exampleToggle: {
display: 'inline-block',
marginBottom: '10px',
marginRight: '30px',
},
selectionDetails: {
marginBottom: '20px',
},
});
export interface IDetailsListState {
columns: IColumn[];
items: any[];
//selectionDetails: string;
isModalSelection: boolean;
isCompactMode: boolean;
announcedMessage?: string;
defaultcolumns?: IColumn[];
}
export class ListView extends React.Component<IViewProps, IDetailsListState> {
// private _selection: Selection;
private _allItems: any[];
private sortedEventOccurrences = [...this.props.cccurrences].sort(EventOccurrence.StartAscComparer);
constructor(props:any) {
super(props);
// this._selection = new Selection({
// onSelectionChanged: () => {
// this.setState({
// selectionDetails: this._getSelectionDetails(),
// });
// },
// getKey: this._getKey,
// });
//const sortedEventOccurrences = [...this.props.cccurrences].sort(EventOccurrence.StartAscComparer);
const defaultcolumns: IColumn[] = [
{
key: 'column1',
name: 'Name',
fieldName: 'displayName',
minWidth: 200,
maxWidth: 240,
isRowHeader: true,
isResizable: true,
isSorted: true,
isSortedDescending: false,
sortAscendingAriaLabel: 'Sorted A to Z',
sortDescendingAriaLabel: 'Sorted Z to A',
onColumnClick: this._onColumnClick,
data: 'string',
isPadded: true,
},
];
this.state = {
items: [],
columns:defaultcolumns,
// selectionDetails: this._getSelectionDetails(),
isModalSelection: false,
isCompactMode: false,
announcedMessage: undefined,
defaultcolumns:defaultcolumns
};
}
private handleTestButton = () =>{
console.log(this.state.items)
}
public render() {
const sortedEventOccurrences = [...this.props.cccurrences].sort(EventOccurrence.StartAscComparer);
const { columns, isCompactMode, items, isModalSelection, announcedMessage } = this.state;
// const newColumns = [...this.state.defaultcolumns];
// this.addColumnInList(newColumns);
// const [
// viewCommand,
// addToOutlookCommand,
// getLinkCommand
// ] = useEventCommandActionButtons(this.props.eventCommands, this.state.items);
// const detailsCallout = useRef<IEventDetailsCallout>();
// const onActivate = useCallback((cccurrence: EventOccurrence, target: HTMLElement) => {
// detailsCallout.current?.open(cccurrence, target);
// }, []);
const data = this.state.items.map(item => ({
title: item.title
}));
return (
<div style={{ overflowX: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop:'-11px' }}>
<div>
<ExportToExcel items={this.state.items} _refiners={this.props.refiners}/>
</div>
<div style={{margin: '10px 0', fontWeight:'640' }}> Count: {this.state.items.length} </div>
</div>
{items.length > 0 ? (
<div style={{marginTop:'-12px'}}>
<DetailsList
items={this.state.items}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this._getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.fixedColumns}
isHeaderVisible={true}
onItemInvoked={this._onItemInvoked}
/>
</div>
) : (
<p>No items found</p>
)}
</div>
);
}
public componentDidMount(): void {
const newColumns = [...this.state.defaultcolumns];
this.addColumnInList(newColumns);
this.setState({items:this.sortedEventOccurrences});
}
public componentDidUpdate(previousProps: any, previousState: IDetailsListState) {
if (previousProps.cccurrences !== this.props.cccurrences) {
const sortedEventOccurrences = [...this.props.cccurrences].sort(EventOccurrence.StartAscComparer);
this.setState({
items: sortedEventOccurrences
})
}
if (previousProps.selectedKeys !== this.props.selectedKeys) {
const sortedEventOccurrences = [...this.props.cccurrences].sort(EventOccurrence.StartAscComparer);
const newColumns = [...this.state.defaultcolumns];
this.addColumnInList(newColumns);
}
}
public addColumnInList(newColumns:IColumn[]){
const dataProperties = ['eventStartDate','eventEndTime', 'description','isRecurring', 'isAllDay', 'refinerValues', 'location', 'tag', 'isRejected','contacts', 'isConfidential', 'isApproved', 'title', 'recurrence', 'created', 'createdBy', 'modified', 'modifiedBy'];
dataProperties.forEach(property => {
if (this.props.selectedKeys.includes(property))
{
switch (property) {
case 'eventStartDate':
newColumns.push({
key: 'column2',
name: 'Start Time',
fieldName: 'eventStartDate',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
//onColumnClick: this._onColumnClick,
onRender: (item: any) => {
return <span>{item.start.format('M/D/YYYY h:mm A')}</span>;
},
});
this.setState({ columns: newColumns });
break;
case 'eventEndTime':
newColumns.push({
key: 'column3',
name: 'End Time',
fieldName: 'eventEndTime',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.end.format('M/D/YYYY h:mm A')}</span>;
},
});
this.setState({ columns: newColumns });
break;
case 'description':
newColumns.push({
key: 'column4',
name: 'Description',
fieldName: 'description',
minWidth: 310,
maxWidth: 330,
isResizable: true,
data: 'string',
onRender: (item: any) => {
return (
<div
dangerouslySetInnerHTML={{
__html: renderSanitizedHTML(item.description)
}}
/>
);
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'isRecurring':
newColumns.push({
key: 'column5',
name: ' Is Recurring',
fieldName: 'isRecurring',
iconName: 'SyncOccurence',
iconClassName: classNames.fileIconHeaderIcon,
minWidth: 60,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: 'string',
//onColumnClick: this._onColumnClick,
onRender: (item: any) => {
return <div>
{/* <Icon iconName={item.isRecurring ? (item.recurrenceExceptionInstanceDate? 'UnsyncOccurence' :'SyncOccurence') : null} /> */}
<Icon iconName={item.isRecurring ? (item.recurrenceExceptionInstanceDate? 'UnsyncOccurence' :'SyncOccurence') : null} style={{ marginRight:5, fontSize:10}}/>
{/* <span style={{ marginLeft: item.isRecurring ? 0 : '10px' }}>{item.isRecurring.toString()}</span> */}
{/* <span style={{ marginLeft: item.isRecurring ? 0 : '10px' }}>{item.isRecurring ? 'Yes' : 'No'}</span> */}
</div>
},
});
this.setState({ columns: newColumns });
break;
case 'isAllDay':
newColumns.push({
key: 'column6',
name: 'All Day Event',
fieldName: 'isAllDay',
minWidth: 60,
maxWidth: 90,
isResizable: true,
//onColumnClick: this._onColumnClick,
data: 'string',
onRender: (item: any) => {
return <span>{item.isAllDay ? 'Yes' : 'No'}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'refinerValues':
this.props.refiners.forEach((refiner:any, index) => {
const refinerColumn = {
key: `column${index +22}`,
name: refiner.displayName, // You can adjust the name as needed
fieldName: refiner.displayName, // You can adjust the fieldName as needed
minWidth: 100,
maxWidth: 200,
isResizable: true,
data: 'string',
onRender: (item: any) => {
const matchingDisplayName = item.refinerValues.state
.filter((refinerItem: any) =>
refiner.values.state.some((valueItem: any) => valueItem.id === refinerItem.id)
)
.map((matchingItem: any) => matchingItem.displayName)
.join('; '); // Join matching displayNames with ';' separator
return <span>{matchingDisplayName}</span>;
},
isPadded: true,
};
newColumns.push(refinerColumn); // Add the new column to newColumns array
});
this.setState({ columns: newColumns });
// newColumns.push({
// key: 'column7',
// name: 'Refiner Values',
// fieldName: 'refinerValues',
// minWidth: 100,
// maxWidth: 200,
// isResizable: true,
// data: 'string',
// onRender: (item: any) => {
// const arrayElements = item.refinerValues.state;
// const joinedValues = arrayElements.map((element: any, index: number) => {
// return index === arrayElements.length - 1 ? element.displayName : `${element.displayName}; `;
// }).join('');
// return <span>{joinedValues}</span>;
// },
// isPadded: true,
// });
break;
case 'location':
newColumns.push({
key: 'column8',
name: 'Location',
fieldName: 'location',
minWidth: 60,
maxWidth: 80,
isResizable: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.location}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'tag':
newColumns.push({
key: 'column9',
name: 'Tag',
fieldName: 'tag',
minWidth: 60,
maxWidth: 80,
isResizable: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.tag}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'isRejected':
newColumns.push({
key: 'colum10',
name: 'Is Rejected',
fieldName: 'isRejected',
minWidth: 60,
maxWidth: 80,
isResizable: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.isRejected ? 'Yes' : 'No'}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'contacts':
newColumns.push({
key: 'column11',
name: 'Event Contacts',
fieldName: 'contacts',
minWidth: 160,
maxWidth: 180,
isResizable: true,
data: 'string',
onRender: (item: any) => {
let renderedContacts = "";
item.contacts.forEach((contact: any, index: number) => {
if (contact.title) {
renderedContacts += contact.title;
} else {
renderedContacts += contact.email;
}
// Add semicolon separator if there are more items and the current item has a title
if (index < item.contacts.length - 1 && (contact.title || contact.email)) {
renderedContacts += "; ";
}
});
return <span>{renderedContacts}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'isConfidential':
newColumns.push({
key: 'column12',
name: 'Is Confidential',
fieldName: 'isConfidential',
minWidth: 60,
maxWidth: 80,
isResizable: true,
//onColumnClick: this._onColumnClick,
data: 'string',
onRender: (item: any) => {
return <span>{item.isConfidential ? 'Yes' : 'No'}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'isApproved':
newColumns.push({
key: 'colum13',
name: 'Is Approved',
fieldName: 'isApproved',
minWidth: 60,
maxWidth: 80,
isResizable: true,
//onColumnClick: this._onColumnClick,
data: 'string',
onRender: (item: any) => {
return <span>{item.isApproved ? 'Yes' : 'No'}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'title':
newColumns.push({
key: 'column14',
name: 'Title',
fieldName: 'title',
minWidth: 200,
maxWidth: 240,
isRowHeader: true,
isResizable: true,
isSorted: true,
isSortedDescending: false,
sortAscendingAriaLabel: 'Sorted A to Z',
sortDescendingAriaLabel: 'Sorted Z to A',
onColumnClick: this._onColumnClick,
data: 'string',
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'recurrence':
newColumns.push({
key: 'column15',
name: 'Recurrence',
fieldName: 'getSeriesMaster',
minWidth: 200,
maxWidth: 240,
isResizable: true,
//onColumnClick: this._onColumnClick,
data: 'string',
onRender: (item: any) => {
return <span>{item.isRecurring?(item.isAllDay ? `${_strings.AllDay}, ${humanizeRecurrencePattern(item.getSeriesMaster().start, item.recurrence)}` : `${item.getSeriesMaster().start.format('LT')} - ${item.getSeriesMaster().end.format('LT')}, ${humanizeRecurrencePattern(item.getSeriesMaster().start, item.recurrence)}`):null}</span>;
},
isPadded: true,
});
this.setState({ columns: newColumns });
break;
case 'created':
newColumns.push({
key: 'column16',
name: 'Created',
fieldName: 'created',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.created.format('MMM D, YYYY')}</span>;
},
});
this.setState({ columns: newColumns });
break;
case 'createdBy':
newColumns.push({
key: 'column17',
name: 'Created By',
fieldName: 'createdBy',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.createdBy ? (item.createdBy.title ? item.createdBy.title : (item.createdBy.email ? item.createdBy.email : "")) : ""}</span>;
},
});
this.setState({ columns: newColumns });
break;
case 'modified':
newColumns.push({
key: 'column18',
name: 'Modified',
fieldName: 'modified',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.modified.format('MMM D, YYYY')}</span>;
},
});
this.setState({ columns: newColumns });
break;
case 'modifiedBy':
newColumns.push({
key: 'column19',
name: 'Modified By',
fieldName: 'modifiedBy',
minWidth: 80,
maxWidth: 110,
isResizable: true,
isCollapsible: true,
data: 'string',
onRender: (item: any) => {
return <span>{item.modifiedBy ? (item.modifiedBy.title ? item.modifiedBy.title : (item.modifiedBy.email ? item.modifiedBy.email : "")) : ""}</span>;
},
});
this.setState({ columns: newColumns });
break;
default:
this.setState({ columns: newColumns });
break;
}
}
else
{
this.setState({ columns: newColumns });
}
});
}
private _getKey(item: any, index?: number): string {
return item.key;
}
private _onChangeCompactMode = (ev: React.MouseEvent<HTMLElement>, checked: boolean): void => {
this.setState({ isCompactMode: checked });
};
private _onChangeModalSelection = (ev: React.MouseEvent<HTMLElement>, checked: boolean): void => {
this.setState({ isModalSelection: checked });
};
private _onChangeText = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text: string): void => {
this.setState({
items: text ? this._allItems.filter(i => i.name.toLowerCase().indexOf(text) > -1) : this._allItems,
});
};
private _onItemInvoked(item: any): void {
alert(`Item invoked: ${item.name}`);
}
// private _getSelectionDetails(): string {
// const selectionCount = this._selection.getSelectedCount();
// switch (selectionCount) {
// case 0:
// return 'No items selected';
// case 1:
// return '1 item selected: ' + (this._selection.getSelection()[0] as IDocument).name;
// default:
// return `${selectionCount} items selected`;
// }
// }
private _onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
const { columns, items } = this.state;
const newColumns: IColumn[] = columns.slice();
const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0];
newColumns.forEach((newCol: IColumn) => {
if (newCol === currColumn) {
currColumn.isSortedDescending = !currColumn.isSortedDescending;
currColumn.isSorted = true;
this.setState({
announcedMessage: `${currColumn.name} is sorted ${
currColumn.isSortedDescending ? 'descending' : 'ascending'
}`,
});
} else {
newCol.isSorted = false;
newCol.isSortedDescending = true;
}
});
const newItems = _copyAndSort(this.state.items, currColumn.fieldName!, currColumn.isSortedDescending);
this.setState({
columns: newColumns,
items: newItems,
});
};
}
// function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
// const key = columnKey as keyof T;
// return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
// }
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
const key = columnKey as keyof T;
return items.slice(0).sort((a: T, b: T) => {
const aValue = String(a[key]).toLowerCase(); // Convert to lowercase
const bValue = String(b[key]).toLowerCase(); // Convert to lowercase
if (isSortedDescending) {
if (aValue < bValue) return 1;
if (aValue > bValue) return -1;
} else {
if (aValue < bValue) return -1;
if (aValue > bValue) return 1;
}
return 0;
});
}
export const ListViewDescriptor: IViewDescriptor = {
id: ViewKeys.list,
title: ViewNames.List,
renderer: ListView,
dateRotatorController: {
previousIconProps: { iconName: 'ChevronLeft' },
nextIconProps: { iconName: 'ChevronRight' },
previousDate: date => date.clone().subtract(1, 'day'),
nextDate: date => date.clone().add(1, 'day'),
dateString: date => date.format('dddd, MMMM DD, YYYY')
},
dateRange: (date) => {
return {
start: date.clone().startOf('day'),
end: date.clone().endOf('day')
};
}
};

View File

@ -11,14 +11,15 @@ import styles from './MonthView.module.scss';
interface IProps {
row: ContentRowInfo;
onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void;
selectedTemplateKeys?: string[];
}
export const ContentRow: FC<IProps> = ({ row: { items }, onActivate }) =>
export const ContentRow: FC<IProps> = ({ row: { items }, onActivate, selectedTemplateKeys }) =>
<Stack horizontal className={styles.content}>
{items.map((item, idx) =>
<StackItem key={idx} styles={blockStyles(item.duration)}>
{item instanceof EventItemInfo
? <EventItem eventInfo={item} onActivate={onActivate} />
? <EventItem eventInfo={item} onActivate={onActivate} selectedTemplateKeys={selectedTemplateKeys} />
: <ShimItem duration={item.duration} />
}
</StackItem>

View File

@ -6,9 +6,10 @@ import { EventItemInfo } from "./Builder";
interface IProps {
eventInfo: EventItemInfo;
onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void;
selectedTemplateKeys?: string[];
}
export const EventItem: FC<IProps> = ({ eventInfo, onActivate }) => {
export const EventItem: FC<IProps> = ({ eventInfo, onActivate, selectedTemplateKeys }) => {
const { cccurrence, startsInWeek, endsInWeek } = eventInfo;
const root = useRef<HTMLDivElement>();
@ -24,6 +25,7 @@ export const EventItem: FC<IProps> = ({ eventInfo, onActivate }) => {
startsIn={startsInWeek}
endsIn={endsInWeek}
size={EventBarSize.Compact}
selectedTemplateKeys={selectedTemplateKeys}
/>
</div>
);

View File

@ -9,9 +9,13 @@ import { Week } from './Week';
import { ViewNames as strings } from 'ComponentStrings';
import { FocusZone } from '@fluentui/react';
import { useTimeZoneService } from "services";
const MonthView: FC<IViewProps> = ({ anchorDate, eventCommands, viewCommands, cccurrences }) => {
const MonthView: FC<IViewProps> = ({ anchorDate, eventCommands, viewCommands, cccurrences, selectedTemplateKeys }) => {
const { siteTimeZone } = useTimeZoneService();
anchorDate = anchorDate.tz(siteTimeZone.momentId,true);
const weeks = Builder.build(cccurrences, anchorDate);
//console.log("weeks", weeks);
const detailsCallout = useRef<IEventDetailsCallout>();
const onActivate = useCallback((cccurrence: EventOccurrence, target: HTMLElement) => {
@ -28,11 +32,13 @@ const MonthView: FC<IViewProps> = ({ anchorDate, eventCommands, viewCommands, cc
anchorDate={anchorDate}
onActivate={onActivate}
viewCommands={viewCommands}
selectedTemplateKeys={selectedTemplateKeys}
/>
)}
<EventDetailsCallout
commands={eventCommands}
componentRef={detailsCallout}
// channels ={channels}
/>
</FocusZone>
);

View File

@ -14,9 +14,10 @@ interface IProps {
week: WeekInfo;
onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void;
viewCommands: IViewCommands;
selectedTemplateKeys?: string[];
}
export const Week: FC<IProps> = ({ anchorDate, week, onActivate, viewCommands }) => {
export const Week: FC<IProps> = ({ anchorDate, week, onActivate, viewCommands, selectedTemplateKeys }) => {
const { palette: { neutralTertiary } } = useTheme();
const style: CSSProperties = {
@ -27,7 +28,7 @@ export const Week: FC<IProps> = ({ anchorDate, week, onActivate, viewCommands })
<div className={styles.week} style={style}>
<WeekBackground anchorDate={anchorDate} commands={viewCommands} range={week} />
{week.contentRows.map((row, idx) =>
<ContentRow key={idx} row={row} onActivate={onActivate} />
<ContentRow key={idx} row={row} onActivate={onActivate} selectedTemplateKeys={selectedTemplateKeys} />
)}
</div>
);

View File

@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { ActionButton, css, FontSizes, FontWeights, IButtonProps, IButtonStyles, IconButton, IStackItemStyles, mergeStyleSets, Stack, StackItem, useTheme } from '@fluentui/react';
import { MomentRange, now } from 'common';
import { ViewKeys } from 'model';
import { useWindowSize } from '../../hooks';
import { useSettings, useWindowSize } from '../../hooks';
import { IViewCommands } from '../IViewCommands';
import { blockStyles } from './blockStyles';
@ -89,6 +89,10 @@ export const WeekBackground: FC<IProps> = ({ anchorDate, commands: { newEvent, s
const { width } = useWindowSize();
const [
userHasEditPermisison
] = useSettings();
const newEventButtonProps: IButtonProps = useMemo(() => {
return {
className: css(styles.newEventButton, 'ms-motion-fadeIn'),
@ -108,8 +112,8 @@ export const WeekBackground: FC<IProps> = ({ anchorDate, commands: { newEvent, s
</ActionButton>
</StackItem>
{width >= 640
? <ActionButton {...newEventButtonProps}>{strings.Command_NewEvent.Text}</ActionButton>
: <IconButton {...newEventButtonProps} />
? userHasEditPermisison && <ActionButton {...newEventButtonProps}>{strings.Command_NewEvent.Text}</ActionButton>
: userHasEditPermisison && <IconButton {...newEventButtonProps} />
}
</Stack>
</StackItem>

View File

@ -4,14 +4,15 @@ import { IEvent } from "model";
import { EventBar, EventBarSize } from "../../events";
import { EventItemInfo } from "./Builder";
import { QuarterView as strings } from "ComponentStrings";
import { QuarterView as strings, ViewNames as _strings } from "ComponentStrings";
interface IProps {
eventInfos: EventItemInfo[];
onActivate: (event: IEvent, target: HTMLElement) => void;
selectedTemplateKeys?: string[];
}
export const EventItem: FC<IProps> = ({ eventInfos, onActivate }) => {
export const EventItem: FC<IProps> = ({ eventInfos, onActivate, selectedTemplateKeys }) => {
const { event, startsInMonth, isRecurring } = first(eventInfos);
const { endsInMonth } = last(eventInfos);
const { start, isAllDay } = event;
@ -35,6 +36,8 @@ export const EventItem: FC<IProps> = ({ eventInfos, onActivate }) => {
endsIn={endsInMonth}
timeStringOverride={startTimeString}
size={EventBarSize.Compact}
type= {_strings.Quarter}
selectedTemplateKeys={selectedTemplateKeys}
/>
</div>
);

View File

@ -16,6 +16,7 @@ interface IProps {
selectedRefinerValues: Set<RefinerValue>;
onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void;
viewCommands: IViewCommands;
selectedTemplateKeys?: string[];
}
export const Month: FC<IProps> = ({
@ -23,7 +24,8 @@ export const Month: FC<IProps> = ({
columnWidth,
selectedRefinerValues,
onActivate,
viewCommands: { setAnchorDate }
viewCommands: { setAnchorDate },
selectedTemplateKeys
}) => {
const navigate = useNavigate();
const { palette: { themeDarkAlt, neutralLighter, neutralPrimary } } = useTheme();
@ -48,18 +50,18 @@ export const Month: FC<IProps> = ({
}
})}
>
{start.format("MMMM")}
{start.format("MMMM")+ " " +start.format("YYYY")}
</ActionButton>
</div>
<Stack horizontal wrap>
{(!refiner || selectedRefinerValues.has(refiner.blankValue) || (refiner.required && blankValue.eventCount > 0)) &&
<StackItem styles={styles}>
<RefinerValueEvents showTitle={!!refiner} refinerValue={blankValue} onActivate={onActivate} />
<RefinerValueEvents showTitle={!!refiner} refinerValue={blankValue} onActivate={onActivate} selectedTemplateKeys={selectedTemplateKeys} />
</StackItem>
}
{refiner && refiner.values.filter(Entity.NotDeletedFilter).filter(value => selectedRefinerValues.has(value)).map(value => refinerValues.get(value)).map((value, idx) =>
<StackItem key={idx} styles={styles}>
<RefinerValueEvents showTitle refinerValue={value} onActivate={onActivate} />
<RefinerValueEvents showTitle refinerValue={value} onActivate={onActivate} selectedTemplateKeys={selectedTemplateKeys} />
</StackItem>
)}
</Stack>

View File

@ -14,7 +14,7 @@ import { getFiscalQuarter, getFiscalYear } from './Utils';
import { ViewNames as strings } from 'ComponentStrings';
const QuarterView: FC<IViewProps> = ({ anchorDate, cccurrences, refiners, selectedRefinerValues, viewCommands, eventCommands }) => {
const QuarterView: FC<IViewProps> = ({ anchorDate, cccurrences, refiners, selectedRefinerValues, viewCommands, eventCommands, selectedTemplateKeys }) => {
const { active: config } = useConfigurationService();
const groupByRefiner = config.useRefiners ? refiners.find(r => r.id === config.quarterViewGroupByRefinerId) : undefined;
@ -48,6 +48,7 @@ const QuarterView: FC<IViewProps> = ({ anchorDate, cccurrences, refiners, select
selectedRefinerValues={selectedRefinerValues}
onActivate={onActivate}
viewCommands={viewCommands}
selectedTemplateKeys={selectedTemplateKeys}
/>
</StackItem>
)}
@ -55,6 +56,7 @@ const QuarterView: FC<IViewProps> = ({ anchorDate, cccurrences, refiners, select
<EventDetailsCallout
commands={eventCommands}
componentRef={detailsCallout}
// channels={channels}
/>
</div>
);
@ -69,8 +71,8 @@ export const QuarterViewDescriptor: IViewDescriptor = {
nextIconProps: { iconName: 'ChevronDown' },
previousDate: date => date.clone().subtract(3, 'months'),
nextDate: date => date.clone().add(3, 'months'),
dateString: (date, { fiscalYearSartMonth }) => {
const fy = getFiscalYear(date, fiscalYearSartMonth);
dateString: (date, { fiscalYearSartMonth, fiscalYearStartYear }) => {
const fy = getFiscalYear(date, fiscalYearSartMonth,fiscalYearStartYear);
const qtr = getFiscalQuarter(date, fiscalYearSartMonth);
return `FY${fy} Q${qtr}`;
}

View File

@ -11,9 +11,10 @@ interface IProps {
showTitle?: boolean;
refinerValue: RefinerValueInfo;
onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void;
selectedTemplateKeys?: string[];
}
export const RefinerValueEvents: FC<IProps> = ({ showTitle = false, refinerValue: { title, itemsByEvent }, onActivate }) => {
export const RefinerValueEvents: FC<IProps> = ({ showTitle = false, refinerValue: { title, itemsByEvent }, onActivate, selectedTemplateKeys }) => {
const titleStyles: ITextStyles = useConst({ root: { margin: '5px 0', fontWeight: FontWeights.semibold } });
const eventItemStyles: IStackItemStyles = useConst({ root: { marginRight: 10 } });
@ -25,7 +26,7 @@ export const RefinerValueEvents: FC<IProps> = ({ showTitle = false, refinerValue
}
{[...itemsByEvent.values()].map((items, idx) =>
<StackItem key={idx} styles={eventItemStyles}>
<EventItem eventInfos={items} onActivate={onActivate} />
<EventItem eventInfos={items} onActivate={onActivate} selectedTemplateKeys={selectedTemplateKeys} />
</StackItem>
)}
</Stack>

View File

@ -1,7 +1,12 @@
import { Moment } from "moment-timezone"
export const getFiscalYear = (date: Moment, fiscalYearSartMonth: number): string =>
(date.month() >= fiscalYearSartMonth ? date.clone().add(1, 'year') : date).format('YY')
export const getFiscalYear = (date: Moment, fiscalYearSartMonth: number, fiscalYearStartYear:string): string =>{
if(fiscalYearStartYear === "Next Year")
return (date.month() >= fiscalYearSartMonth ? date.clone().add(1, 'year') : date).format('YY')
else{
return (date.month() >= fiscalYearSartMonth ? date.clone() : date.clone().add(-1, 'year')).format('YY')
}
}
export const getFiscalQuarter = (date: Moment, fiscalYearSartMonth: number) =>
Math.floor((date.month() + 12 - fiscalYearSartMonth) % 12 / 3) + 1

View File

@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { ActionButton, css, FontSizes, FontWeights, IButtonProps, IButtonStyles, IconButton, IStackItemStyles, mergeStyleSets, Stack, StackItem, useTheme } from '@fluentui/react';
import { MomentRange, now } from 'common';
import { ViewKeys } from 'model';
import { useWindowSize } from '../../hooks';
import { useSettings, useWindowSize } from '../../hooks';
import { IViewCommands } from '../IViewCommands';
import { blockStyles } from './blockStyles';
@ -89,6 +89,10 @@ export const Background: FC<IProps> = ({ anchorDate, commands: { newEvent, setAn
const { width } = useWindowSize();
const [
userHasEditPermisison
] = useSettings();
const newEventButtonProps: IButtonProps = useMemo(() => {
return {
className: css(styles.newEventButton, 'ms-motion-fadeIn'),
@ -108,8 +112,8 @@ export const Background: FC<IProps> = ({ anchorDate, commands: { newEvent, setAn
</ActionButton>
</StackItem>
{width >= 640
? <ActionButton {...newEventButtonProps}>{strings.Command_NewEvent.Text}</ActionButton>
: <IconButton {...newEventButtonProps} />
? userHasEditPermisison && <ActionButton {...newEventButtonProps}>{strings.Command_NewEvent.Text}</ActionButton>
: userHasEditPermisison && <IconButton {...newEventButtonProps} />
}
</Stack>
</StackItem>

View File

@ -56,7 +56,6 @@ export class ContentRowInfo {
const startPosition = startsInWeek ? start.day() : 0;
const endPosition = endsInWeek ? end.day() + 1 : 7;
const duration = endPosition - startPosition;
const shimDuration = startPosition - this.lastUsedPosition();
if (shimDuration > 0) {
this.items.push(new ShimItemInfo(shimDuration));

View File

@ -1,6 +1,8 @@
import { min, Moment } from "moment-timezone";
import { MomentRange } from "common";
import { DailyRecurrence, MonthlyRecurrence, RecurDay, RecurPattern, RecurPatternOption, Recurrence, RecurUntilType, RecurWeekOfMonth, WeeklyRecurrence, YearlyRecurrence } from "./Recurrence";
import moment from "moment";
import { useConfigurationService } from "services";
const isWeekend = (date: Moment): boolean =>
date.day() === 0 || date.day() === 6
@ -70,8 +72,19 @@ const gotoDateByRecurDay = (current: Moment, weekOf: RecurWeekOfMonth, recurDay:
default: {
if (weekOf === RecurWeekOfMonth.last) current.add(1, 'month');
const month = current.month();
const originalTime = current.clone();
current.startOf('month');
current.hour(originalTime.hour());
current.minute(originalTime.minute());
current.second(originalTime.second());
current.millisecond(originalTime.millisecond());
current.day(recurDay); // sets the date to be the specified day of the week within the current Sunday-Saturday week
if(originalTime.year() > current.year()){
current.add(1, 'week');
current.day(recurDay);
}
if (current.month() < month) current.add(1, 'week'); // if that moved the date backwards to the previous month, add a week to move forward to the current month
current.add(weekOf === RecurWeekOfMonth.last ? -1 : weekOf, 'weeks');
}
@ -103,6 +116,7 @@ class DailyCadenceGenerator implements ICadenceGenerator {
yield current.clone();
current.add(weekdaysOnly ? 1 : every, 'days');
// current.add(!weekdaysOnly ? 1 : every, 'days');
}
}
}
@ -234,7 +248,8 @@ class YearlyByDayCadenceGenerator implements ICadenceGenerator {
export class Cadence {
constructor(
private readonly _start: Moment,
private readonly _recurrence: Recurrence
private readonly _recurrence: Recurrence,
private readonly _isDifferenceInTimezone: boolean
) { }
public *generate(range?: MomentRange): Generator<Moment, undefined> {
@ -253,15 +268,36 @@ export class Cadence {
? min(range.end, until.date)
: range.end;
do {
const { done, value: date } = dates.next();
const _date = moment(date);
const _range = moment(range.start);
// Get time zone identifiers for both dates
const timeZone_date = _date.tz();
const timeZone_range = _range.tz();
// const convertedMoment = _date.clone().tz(targetTimeZoneId);
if (done || !date.isValid() || date.isAfter(end, 'day'))
break;
// if (!this._isDifferenceInTimezone) {
// if (date.isSameOrAfter(range.start, 'day'))
// yield date;
// }
// else {
// if (date.isAfter(range.start, 'day'))
// yield date;
// }
if (timeZone_date !== timeZone_range) {
if (date.isAfter(range.start, 'day'))
yield date;
}
else{
if (date.isSameOrAfter(range.start, 'day'))
yield date;
}
count++;
} while (until.type !== RecurUntilType.count || count < until.count);
}

View File

@ -0,0 +1,110 @@
// import { intersection } from 'lodash';
// import { Moment } from 'moment-timezone';
// import { Guid } from '@microsoft/sp-core-library';
// import { groupBy, IManyToManyRelationship, ManyToManyRelationship, MaxLengthValidationRule, RequiredValidationRule, User, ValidationRule } from 'common';
// import { ListItemEntity } from "common/sharepoint";
// import { RefinerValue } from './RefinerValue';
// import { Refiner } from './Refiner';
// interface IState {
// channelName: string;
// teamsId: string;
// channelId: string;
// teamsName: string;
// actualChannelName: string;
// //channel_originalName: string;
// }
// export class ChannelsConfigurations extends ListItemEntity<IState> {
// public static readonly ChannelNameValidations = [
// new RequiredValidationRule<ChannelsConfigurations>(e => e.channelName),
// new MaxLengthValidationRule<ChannelsConfigurations>(e => e.channelName, 255)
// ];
// public static readonly TeamsIdValidations = [
// new RequiredValidationRule<ChannelsConfigurations>(e => e.teamsId),
// new MaxLengthValidationRule<ChannelsConfigurations>(e => e.teamsId, 255)
// ];
// public static readonly ChannelIdValidations = [
// new RequiredValidationRule<ChannelsConfigurations>(e => e.channelId),
// new MaxLengthValidationRule<ChannelsConfigurations>(e => e.channelId, 255)
// ];
// // public static appliesTo(channelsConfigurations: ChannelsConfigurations, eventValuesByRefiner: Map<Refiner, RefinerValue[]>): boolean {
// // const { refinerValuesByRefiner: approverValuesByRefiner } = channelsConfigurations;
// // return [...approverValuesByRefiner.keys()].every(refiner => {
// // const approverValues = approverValuesByRefiner.get(refiner);
// // const eventValues = eventValuesByRefiner.get(refiner);
// // return intersection(approverValues, eventValues).length > 0;
// // });
// // }
// // public static appliesToAny(channelsConfigurations: ChannelsConfigurations[], eventValuesByRefiner: Map<Refiner, RefinerValue[]>): boolean {
// // return channelsConfigurations.some(a => ChannelsConfigurations.appliesTo(a, eventValuesByRefiner));
// // }
// constructor(author?: User, editor?: User, created?: Moment, modified?: Moment, id?: number, uniqueId?: Guid, etag?: number) {
// super(author, editor, created, modified, id, uniqueId, etag);
// this.channelName = "";
// this.teamsId = "";
// this.channelId = "";
// this.teamsName = "";
// this.actualChannelName="";
// }
// public readonly refinerValues: IManyToManyRelationship<RefinerValue>;
// private _refinerValuesByRefiner: Map<Refiner, RefinerValue[]> = undefined;
// public get refinerValuesByRefiner() {
// return (this._refinerValuesByRefiner = this._refinerValuesByRefiner ||
// groupBy(this.refinerValues.get(), value => value.refiner.get())
// );
// }
// // public hasChanges(specificProperty?: string | number | symbol): boolean {
// // if (specificProperty)
// // return super.hasChanges(specificProperty);
// // else
// // return super.hasChanges() || this.refinerValues.hasChanges();
// // }
// public immortalize() {
// this._refinerValuesByRefiner = undefined;
// super.immortalize();
// }
// public endLiveUpdate() {
// this._refinerValuesByRefiner = undefined;
// super.endLiveUpdate();
// }
// public get channelName(): string { return this.state.channelName; }
// public set channelName(val: string) { this.state.channelName = val; }
// public get teamsId(): string { return this.state.teamsId; }
// public set teamsId(val: string) { this.state.teamsId = val; }
// public get channelId(): string { return this.state.channelId; }
// public set channelId(val: string) { this.state.channelId = val; }
// public get teamsName(): string { return this.state.teamsName; }
// public set teamsName(val: string) { this.state.teamsName = val; }
// public get actualChannelName(): string { return this.state.actualChannelName; }
// public set actualChannelName(val: string) { this.state.actualChannelName = val; }
// protected validationRules(): ValidationRule<ChannelsConfigurations>[] {
// return [
// ...ChannelsConfigurations.ChannelNameValidations,
// ...ChannelsConfigurations.TeamsIdValidations,
// ...ChannelsConfigurations.ChannelIdValidations
// ];
// }
// }
// export type ChannelsConfigurationsMap = Map<number, ChannelsConfigurations>;
// export type ReadonlyChannelsConfigurationsMap = ReadonlyMap<number, ChannelsConfigurations>;

View File

@ -31,6 +31,7 @@ interface IState {
moderator: User | undefined;
moderationTimestamp: Moment | undefined;
moderationMessage: string;
teamsGroupChatId: string;
}
export class Event extends ListItemEntity<IState> implements IEvent {
@ -75,6 +76,10 @@ export class Event extends ListItemEntity<IState> implements IEvent {
public static readonly Count_Until_Recurrence_Validations = [
new Count_Until_Recurrence_Required_ValidationRule()
];
// public static readonly TeamsGroupChatId_Validations = [
// new RequiredValidationRule<Event>(e => e.teamsGroupChatId),
// new MaxLengthValidationRule<Event>(e => e.teamsGroupChatId, 255)
// ];
public static ApprovedFilter = ({ isApproved }: Event): boolean => isApproved;
public static PendingFilter = ({ isPendingApproval }: Event): boolean => isPendingApproval;
@ -104,6 +109,7 @@ export class Event extends ListItemEntity<IState> implements IEvent {
this.state.moderator = undefined;
this.state.moderationTimestamp = undefined;
this.state.moderationMessage = "";
this.state.teamsGroupChatId = "";
this.refinerValues = ManyToManyRelationship.create<Event, RefinerValue>(this, 'events', { comparer: Event.RefinerValueOrderAscComparer });
this.includeInBoundedContext(this.refinerValues);
@ -310,6 +316,9 @@ export class Event extends ListItemEntity<IState> implements IEvent {
public get moderationMessage(): string { return this._seriesMasterOrThisState.moderationMessage; }
public set moderationMessage(val: string) { if (!this.isSeriesException) this.state.moderationMessage = val; }
public get teamsGroupChatId(): string { return this.state.teamsGroupChatId; }
public set teamsGroupChatId(val: string) { this.state.teamsGroupChatId = val; }
public get creator(): User { return (this.isSeriesException ? this.seriesMaster.get() : this).author; }
private get _seriesMasterOrThisState(): IState {
@ -332,9 +341,23 @@ export class Event extends ListItemEntity<IState> implements IEvent {
return this.usersDifference('restrictedToAccounts');
}
public expandOccurrences(range?: MomentRange): EventOccurrence[] {
public expandOccurrences(isDifferenceInTimezone: boolean, range?: MomentRange, viewType?: string, siteTimeZone?:string): EventOccurrence[] {
if (this.isSeriesMaster) {
const cadence = new Cadence(this.start, this.recurrence);
if(viewType === 'list'){
const originalStart = range.start;
const convertedStartMoment = originalStart && originalStart.clone().tz(siteTimeZone,true);
convertedStartMoment.startOf('day');
range.start = convertedStartMoment;
const originalEnd = range.end;
const convertedEndMoment = originalEnd && originalEnd.clone().tz(siteTimeZone,true);
convertedEndMoment.endOf('day');
range.end = convertedEndMoment;
//return (!range || MomentRange.overlaps(this, range)) ? (!this.recurrenceInstanceCancelled ? [new EventOccurrence(this)]:[] ): [];
}
const cadence = new Cadence(this.start, this.recurrence, isDifferenceInTimezone);
const dates = Array.from(cadence.generate(range));
const exceptionsInRange = multifilter(this.exceptions.get(), inverseFilter(Entity.NewAndGhostableFilter), e => MomentRange.overlaps(range, e));
@ -358,18 +381,38 @@ export class Event extends ListItemEntity<IState> implements IEvent {
date.startOf('day');
const start = date.clone().add(this.startTime);
const end = start.clone().add(this.duration);
const startOffset = start && start.utcOffset();
const endOffset = end && end.utcOffset();
const startdst = start && start.isDST();
const enddst = end && end.isDST();
const dateOffset = date && date.utcOffset();
if(startdst !== enddst)
{
startOffset-endOffset < 0 ? end.add(startOffset-endOffset,'minutes'): end.add(startOffset-endOffset,'minutes')// for handling daylight saving scenario
// console.log(end.add(-1,'hour'))
}
if(dateOffset !== startOffset){
start.add(dateOffset - startOffset,'minutes');
end.add(dateOffset - startOffset,'minutes');
}
return new EventOccurrence(this, start, end);
}
})
.filter(Boolean)
.concat(exceptionsInRange.map(e => new EventOccurrence(e)));
} else {
if(viewType === 'list'){
return (!range || MomentRange.overlaps(this, range)) ? (!this.recurrenceInstanceCancelled ? [new EventOccurrence(this)]:[] ): [];
}
else{
return (!range || MomentRange.overlaps(this, range)) ? [new EventOccurrence(this)] : [];
}
}
}
public findOrCreateExceptionForDate(date: Moment): Event {
const occurrence = first(this.expandOccurrences({ start: date, end: date }));
public findOrCreateExceptionForDate(date: Moment, isDifferenceInTimezone: boolean): Event {
const occurrence = first(this.expandOccurrences(isDifferenceInTimezone, { start: date, end: date }));
return occurrence ? this.createSeriesException(occurrence.start, occurrence.end) : undefined;
}
@ -407,6 +450,7 @@ export class Event extends ListItemEntity<IState> implements IEvent {
...Event.Date_YearlyByDate_Recurrence_Validations,
...Event.EndDate_Until_Recurrence_Validations,
...Event.Count_Until_Recurrence_Validations
//...Event.TeamsGroupChatId_Validations
];
}
}

View File

@ -20,7 +20,7 @@ export class EventModerationStatus {
) {
}
public clone(): this {
public clone(): EventModerationStatus {
return this;
}
}

View File

@ -2,6 +2,7 @@ import { Comparer, momentAscComparer } from "common";
import { Moment } from "moment-timezone";
import { IEvent } from "./IEvent";
import { Event } from "./Event";
import React from "react";
export class EventOccurrence implements IEvent {
public static readonly StartAscComparer: Comparer<EventOccurrence> = (a, b) => momentAscComparer(a.start, b.start);
@ -28,6 +29,14 @@ export class EventOccurrence implements IEvent {
public get isSeriesException() { return this.event.isRecurring; } // an event occurrence is always an exception if the event is recurring
public get isConfidential() { return this.event.isConfidential; }
public get refinerValues() { return this.event.refinerValues; }
public get contacts() { return this.event.contacts; }
public get description() { return this.event.description ? this.event.description : undefined; }
public get recurrenceExceptionInstanceDate() { return this.event.recurrenceExceptionInstanceDate; }
public get created(){ return this.event.created; }
public get createdBy(){ return this.event.creator; }
public get modified(){ return this.event.modified; }
public get modifiedBy(){ return this.event.editor; }
public getWrappedEvent(): Event {
return this.event;
@ -37,6 +46,42 @@ export class EventOccurrence implements IEvent {
return this.event.getSeriesMaster();
}
public convertToPlainText(html:any) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
public parseHTML(htmlString: string): (JSX.Element | string)[] {
const doc = new DOMParser().parseFromString(htmlString, 'text/html');
const elements = Array.from(doc.body.childNodes);
const inlineStyles = {
margin: 0,
padding: 0,
lineHeight: 1.2 // Adjust this value as needed
};
return elements.map((element, index) => {
const key = `htmlElement_${index}`;
switch (element.nodeType) {
case Node.ELEMENT_NODE:
const tagName = (element as HTMLElement).tagName.toLowerCase();
const attributes: { [key: string]: any } = { key, style: inlineStyles };
//const attributes: { [key: string]: string } = {};
if (element instanceof HTMLElement) {
Array.from(element.attributes).forEach(attribute => {
attributes[attribute.name] = attribute.value;
});
}
return React.createElement(tagName, { key, ...attributes }, ...this.parseHTML((element as HTMLElement).innerHTML));
case Node.TEXT_NODE:
return element.nodeValue;
default:
return null;
}
});
}
public getExceptionOrEvent(): Event {
if (this.event.isSeriesMaster) {
return this.event.createSeriesException(this.start, this.end);

View File

@ -0,0 +1,24 @@
import { stringToEnum } from "../common";
export const ListViewKeys = stringToEnum([
"selectAll",
"displayName",
"eventStartDate",
"eventEndTime",
"description",
"isRecurring",
"isAllDay",
"refinerValues",
"location",
"tag",
"isRejected",
"contacts",
"isConfidential",
"isApproved",
"title",
"recurrence",
"created", "createdBy", "modified", "modifiedBy"
]);
export type ListViewKeys = keyof (typeof ListViewKeys);
export const DefaultListViewKeys = ListViewKeys["displayName"];

View File

@ -0,0 +1,11 @@
import { stringToEnum } from "../common";
export const TemplateViewKeys = stringToEnum([
"eventTitle",
"tag",
"location",
"starttime",
]);
export type TemplateViewKeys = keyof (typeof TemplateViewKeys);
export const DefaultTemplateViewKeys = TemplateViewKeys["eventTitle"];

View File

@ -4,7 +4,8 @@ export const ViewKeys = stringToEnum([
"daily",
"weekly",
"monthly",
"quarter"
"quarter",
"list"
]);
export type ViewKeys = keyof (typeof ViewKeys);

View File

@ -0,0 +1,10 @@
import { stringToEnum } from "common";
export const ViewYearFYKeys = stringToEnum([
"Current Year",
"Next Year"
]);
export type ViewYearFYKeys = keyof (typeof ViewYearFYKeys);
export const DefaultViewYearFYKey = ViewYearFYKeys["Next Year"];

View File

@ -1,4 +1,5 @@
export { Approvers, type ApproversMap, type ReadonlyApproversMap } from "./Approvers";
//export { ChannelsConfigurations, type ChannelsConfigurationsMap, type ReadonlyChannelsConfigurationsMap } from "./ChannelsConfigurations";
export { type IEvent } from "./IEvent";
export { Event, type EventMap, type ReadonlyEventMap } from "./Event";
export { EventOccurrence } from "./EventOccurrence";
@ -8,3 +9,5 @@ export { Recurrence, RecurDay, RecurPattern, RecurPatternOption, RecurWeekOfMont
export { Refiner, type RefinerMap, type ReadonlyRefinerMap } from "./Refiner";
export { RefinerValue, type RefinerValueMap, type ReadonlyRefinerValueMap } from "./RefinerValue";
export { ViewKeys, DefaultViewKey } from './ViewKeys';
export { ViewYearFYKeys, DefaultViewYearFYKey } from './ViewYearFYKeys';
export {ListViewKeys, DefaultListViewKeys} from './ListViewKeys';

View File

@ -1,9 +1,10 @@
import { Guid } from "@microsoft/sp-core-library";
import { User } from "common";
import { ListItemEntity } from "common/sharepoint";
import { ViewKeys } from "model";
import { ViewKeys, ViewYearFYKeys, ListViewKeys } from "model";
import { Moment } from "moment-timezone";
import { CurrentSchemaVersion, IRhythmOfBusinessCalendarSchema, RhythmOfBusinessCalendarSchema } from "./RhythmOfBusinessCalendarSchema";
import { TemplateViewKeys } from "model/TemplateViewKeys";
interface IState {
schemaVersion: number;
@ -15,6 +16,12 @@ interface IState {
quarterViewGroupByRefinerId: number;
useApprovals: boolean;
allowConfidentialEvents: boolean;
useApprovalsTeamsNotification: boolean;
useApprovalsEmailNotification: boolean;
fiscalYearStartYear: ViewYearFYKeys;
listViewColumn: ListViewKeys[];
templateView: TemplateViewKeys[];
useAddToOutlook: boolean;
}
export class Configuration extends ListItemEntity<IState> {
@ -32,6 +39,12 @@ export class Configuration extends ListItemEntity<IState> {
this.state.quarterViewGroupByRefinerId = undefined;
this.state.useApprovals = false;
this.state.allowConfidentialEvents = false;
this.state.useApprovalsTeamsNotification = false;
this.state.useApprovalsEmailNotification = false;
this.state.fiscalYearStartYear = ViewYearFYKeys["Next Year"];
this.state.listViewColumn = [ListViewKeys["displayName"]];
this.state.useAddToOutlook = false;
this.state.templateView = [TemplateViewKeys["eventTitle"]];
this._schema = RhythmOfBusinessCalendarSchema;
}
@ -66,6 +79,25 @@ export class Configuration extends ListItemEntity<IState> {
public get allowConfidentialEvents(): boolean { return this.state.allowConfidentialEvents; }
public set allowConfidentialEvents(val: boolean) { this.state.allowConfidentialEvents = val; }
public get useApprovalsTeamsNotification(): boolean { return this.state.useApprovalsTeamsNotification; }
public set useApprovalsTeamsNotification(val: boolean) { this.state.useApprovalsTeamsNotification = val; }
public get useApprovalsEmailNotification(): boolean { return this.state.useApprovalsEmailNotification; }
public set useApprovalsEmailNotification(val: boolean) { this.state.useApprovalsEmailNotification = val; }
public get fiscalYearStartYear(): ViewYearFYKeys { return this.state.fiscalYearStartYear; }
public set fiscalYearStartYear(val: ViewYearFYKeys) { this.state.fiscalYearStartYear = val; }
public get listViewColumn(): ListViewKeys[] { return this.state.listViewColumn; }
public set listViewColumn(val: ListViewKeys[]) { this.state.listViewColumn = val; }
public get templateView(): TemplateViewKeys[] { return this.state.templateView; }
public set templateView(val: TemplateViewKeys[]) { this.state.templateView = val; }
public get useAddToOutlook(): boolean { return this.state.useAddToOutlook; }
public set useAddToOutlook(val: boolean) { this.state.useAddToOutlook = val; }
}
export type ConfigurationMap = Map<number, Configuration>;

View File

@ -6,7 +6,7 @@ const Environments = {
PROD: { Prefix: '' }
};
const Environment = Environments.LOCAL;
const Environment = Environments.PROD;
const AppPrefix = "RoB Calendar";
const combine = (...segments: string[]) => segments.join(' ').trim();
@ -18,6 +18,7 @@ export const Defaults = {
Events: title('Events'),
Refiners: title('Refiners'),
RefinerValues: title('Refiner Values'),
Approvers: title('Approvers')
Approvers: title('Approvers'),
// ChannelsConfigurations: title('ChannelsConfigurations')
}
};

View File

@ -1,7 +1,8 @@
import { IElementDefinitions, IListDefinition, buildLiveSchema } from "common/sharepoint";
import { ConfigurationList, IEventsListDefinition, EventsList, RefinersList, RefinerValuesList, ApproversList, IRefinersListDefinition, IRefinerValuesListDefinition, IApproversListDefinition } from "./lists";
import { IROBCalendarUpgrade, Upgrade_to_V5_0_0, Upgrade_to_V4_0_0, Upgrade_to_V3_0_0, Upgrade_to_V2_0_0 } from "./upgrades";
export const CurrentSchemaVersion: number = 1.0;
export const CurrentSchemaVersion: number = 5.0;
export interface IRhythmOfBusinessCalendarSchema extends IElementDefinitions {
configurationList: IListDefinition;
@ -9,6 +10,8 @@ export interface IRhythmOfBusinessCalendarSchema extends IElementDefinitions {
refinersList: IRefinersListDefinition;
refinerValuesList: IRefinerValuesListDefinition;
approversList: IApproversListDefinition;
// channelsConfigurationsList:IChannelsConfigurationsListDefinition;
upgrades?: IROBCalendarUpgrade[];
}
export const RhythmOfBusinessCalendarSchema = buildLiveSchema<IRhythmOfBusinessCalendarSchema>({
@ -19,12 +22,13 @@ export const RhythmOfBusinessCalendarSchema = buildLiveSchema<IRhythmOfBusinessC
RefinersList,
RefinerValuesList,
ApproversList
// ChannelsConfigurationsList
],
upgrades: [
],
upgrades: [Upgrade_to_V2_0_0, Upgrade_to_V3_0_0, Upgrade_to_V4_0_0, Upgrade_to_V5_0_0],
configurationList: ConfigurationList,
eventsList: EventsList,
refinersList: RefinersList,
refinerValuesList: RefinerValuesList,
approversList: ApproversList
//channelsConfigurationsList: ChannelsConfigurationsList
});

View File

@ -1,3 +1,3 @@
export { Configuration, ConfigurationMap, ReadonlyConfigurationMap } from './Configuration';
export { ConfigurationList } from './lists';
export { ConfigurationList, EventsList } from './lists';
export { IRhythmOfBusinessCalendarSchema } from './RhythmOfBusinessCalendarSchema';

View File

@ -0,0 +1,78 @@
// import { IListDefinition, FieldType, IViewDefinition, ITextFieldDefinition, includeStandardViewFields, ListTemplateType, RoleOperation, RoleType } from "common/sharepoint";
// import { Defaults } from "../Defaults";
// const Field_ChannelName: ITextFieldDefinition = {
// type: FieldType.Text,
// name: 'ChannelName',
// displayName: 'Channel Name'
// };
// const Field_TeamsId: ITextFieldDefinition = {
// type: FieldType.Text,
// name: 'TeamsId',
// displayName: 'Teams Id',
// multi: true
// };
// const Field_ChannelId: ITextFieldDefinition = {
// type: FieldType.Text,
// name: 'ChannelId',
// displayName: 'Channel Id',
// multi: true
// };
// const Field_TeamsName: ITextFieldDefinition = {
// type: FieldType.Text,
// name: 'TeamsName',
// displayName: 'Teams Name'
// };
// const Field_ActualChannelName: ITextFieldDefinition = {
// type: FieldType.Text,
// name: 'ActualChannelName',
// displayName: 'Actual Channel Name'
// };
// const View_AllChannelsConfigurations: IViewDefinition = {
// title: "AllChannelsConfigurations",
// rowLimit: 250,
// paged: true,
// default: true,
// fields: includeStandardViewFields(
// Field_ChannelName,
// Field_TeamsId,
// Field_ChannelId,
// Field_TeamsName,
// Field_ActualChannelName
// )
// };
// export interface IChannelsConfigurationsListDefinition extends IListDefinition {
// view_AllChannelsConfigurations: IViewDefinition;
// }
// export const ChannelsConfigurationsList: IChannelsConfigurationsListDefinition = {
// title: Defaults.ListTitles.ChannelsConfigurations,
// description: '',
// template: ListTemplateType.GenericList,
// dependencies: [],
// permissions: {
// copyRoleAssignments: false,
// userRoles: [
// { operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
// { operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'memberGroup' },
// { operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
// ]
// },
// fields: [
// Field_ChannelName,
// Field_TeamsId,
// Field_ChannelId,
// Field_TeamsName,
// Field_ActualChannelName
// ],
// views: [
// View_AllChannelsConfigurations
// ],
// view_AllChannelsConfigurations: View_AllChannelsConfigurations
// };

View File

@ -1,6 +1,7 @@
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, INumberFieldDefinition, ListTemplateType, IChoiceFieldDefinition, IBooleanFieldDefinition, RoleOperation, RoleType } from "common/sharepoint";
import { ViewKeys } from "model";
import { ListViewKeys, ViewKeys, ViewYearFYKeys } from "model";
import { Defaults } from "../Defaults";
import { TemplateViewKeys } from "model/TemplateViewKeys";
const Field_SchemaVersion: INumberFieldDefinition = {
type: FieldType.Number,
@ -25,6 +26,54 @@ const Field_FiscalYearSartMonth: INumberFieldDefinition = {
default: "1"
};
const Field_FiscalYearStartYear: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'FiscalYearStartYear',
displayName: "Fiscal Year Start Year",
choices: Object.keys(ViewYearFYKeys),
default: ViewYearFYKeys["Next Year"]
};
const Field_ListViewColumn: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'ListViewColumn',
displayName: "List View Column",
choices: Object.keys(ListViewKeys),
default: ListViewKeys["displayName"],
multi: true
};
const Field_UseApprovalsEmailNotification: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseApprovalsEmailNotification',
displayName: "Use Approvals Email Notification",
default: "No"
};
const Field_TemplateView: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'TemplateView',
displayName: "Template View",
choices: Object.keys(TemplateViewKeys),
default: TemplateViewKeys["eventTitle"],
multi: true
};
const Field_UseApprovalsTeamsNotification: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseApprovalsTeamsNotification',
displayName: "Use Approvals Teams Notification",
default: "No"
};
const Field_UseAddToOutlook: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseAddToOutlook',
displayName: "Use Add To Outlook",
default: "Yes"
};
const Field_DefaultView: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'DefaultView',
@ -82,7 +131,13 @@ const View_AllItems: IViewDefinition = {
Field_RefinerRailInitiallyExpanded,
Field_QuarterViewGroupByRefinerId,
Field_UseApprovals,
Field_AllowConfidentialEvents
Field_AllowConfidentialEvents,
Field_FiscalYearStartYear,
Field_UseApprovalsEmailNotification,
Field_UseApprovalsTeamsNotification,
Field_UseAddToOutlook,
Field_ListViewColumn,
Field_TemplateView
)
};
@ -112,7 +167,13 @@ export const ConfigurationList: IConfigurationListDefinition = {
Field_RefinerRailInitiallyExpanded,
Field_QuarterViewGroupByRefinerId,
Field_UseApprovals,
Field_AllowConfidentialEvents
Field_AllowConfidentialEvents,
Field_FiscalYearStartYear,
Field_UseApprovalsEmailNotification,
Field_UseApprovalsTeamsNotification,
Field_UseAddToOutlook,
Field_ListViewColumn,
Field_TemplateView
],
views: [View_AllItems],
view_AllItems: View_AllItems

View File

@ -147,6 +147,13 @@ const Field_ModerationMessage: ITextFieldDefinition = {
required: false
};
const Field_TeamsGroupChatId: ITextFieldDefinition = {
type: FieldType.Text,
name: 'TeamsGroupChatId',
displayName: 'Teams Group Chat Id',
required: false
};
const View_AllEvents: IViewDefinition = {
title: "All RoB Events",
rowLimit: 600,
@ -172,7 +179,8 @@ const View_AllEvents: IViewDefinition = {
Field_ModerationStatus,
Field_Moderator,
Field_ModerationTimestamp,
Field_ModerationMessage
Field_ModerationMessage,
Field_TeamsGroupChatId
),
// need to sort by ID ascending in order to ensure the series master is loaded before any exceptions to the series
query: `
@ -220,7 +228,8 @@ export const EventsList: IEventsListDefinition = {
Field_ModerationStatus,
Field_Moderator,
Field_ModerationTimestamp,
Field_ModerationMessage
Field_ModerationMessage,
Field_TeamsGroupChatId
],
views: [
View_AllEvents

View File

@ -3,3 +3,4 @@ export { ConfigurationList } from './ConfigurationList';
export { EventsList, IEventsListDefinition } from './EventsList';
export { RefinersList, IRefinersListDefinition } from './RefinersList';
export { RefinerValuesList, IRefinerValuesListDefinition } from './RefinerValuesList';
//export { ChannelsConfigurationsList,IChannelsConfigurationsListDefinition} from './ChannelsConfigurationsList';

View File

@ -0,0 +1,6 @@
import { IUpgrade } from "common/sharepoint";
import {IROBCalendarUpgradeAction} from "./IROBCalendarUpgradeAction";
export interface IROBCalendarUpgrade extends IUpgrade{
actions:IROBCalendarUpgradeAction[];
}

View File

@ -0,0 +1,6 @@
import { IUpgradeAction } from "common/sharepoint";
export interface IROBCalendarUpgradeAction extends IUpgradeAction{
readonly shared?: boolean;
execute() : Promise<void>;
}

View File

@ -0,0 +1,6 @@
export { Definition as Upgrade_to_V2_0_0 } from "./v2.0.0/Definition";
export { Definition as Upgrade_to_V3_0_0 } from "./v3.0.0/Definition";
export { Definition as Upgrade_to_V4_0_0 } from "./v4.0.0/Definition";
export { Definition as Upgrade_to_V5_0_0 } from "./v5.0.0/Definition";
export { IROBCalendarUpgrade } from "./IROBCalendarUpgrade";
export { IROBCalendarUpgradeAction } from "./IROBCalendarUpgradeAction";

View File

@ -0,0 +1,18 @@
import { ElementProvisioner } from "common/sharepoint";
import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction";
import {ConfigurationList,Field_FiscalYearStartYear,Field_UseApprovalsEmailNotification, Field_UseApprovalsTeamsNotification} from "./schemaSnapshot/index";
export class AddFYStartYearColumnToConfigutationList
implements IROBCalendarUpgradeAction
{
public get description(): string {
return `Adding fields to list '${ConfigurationList.title}'`;
}
public async execute(): Promise<void> {
const provisioner: ElementProvisioner = new ElementProvisioner();
await provisioner.ensureField(Field_FiscalYearStartYear, ConfigurationList);
await provisioner.ensureField(Field_UseApprovalsEmailNotification, ConfigurationList);
await provisioner.ensureField(Field_UseApprovalsTeamsNotification, ConfigurationList);
}
}

View File

@ -0,0 +1,9 @@
import { IROBCalendarUpgrade } from "../IROBCalendarUpgrade";
import { AddFYStartYearColumnToConfigutationList } from "./AddFYStartYearColumnToConfigutationList";
import { UpdateAllConfigurationListView } from "./UpdateAllConfigurationListView";
export const Definition: IROBCalendarUpgrade = {
fromVersion: 1.0,
toVersion: 2.0,
actions: [new AddFYStartYearColumnToConfigutationList(),
new UpdateAllConfigurationListView(),],
};

View File

@ -0,0 +1,15 @@
import { AddOrUpdateViewUpgradeAction } from "common/sharepoint";
import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction";
import {ConfigurationList, View_AllItems} from "./schemaSnapshot/index";
export class UpdateAllConfigurationListView extends AddOrUpdateViewUpgradeAction implements IROBCalendarUpgradeAction {
public readonly shared: boolean = false;
constructor() {
super(ConfigurationList, View_AllItems);
}
}

View File

@ -0,0 +1,68 @@
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, IBooleanFieldDefinition, IUserFieldDefinition, ILookupFieldDefinition, ListTemplateType, RoleOperation, RoleType } from "common/sharepoint";
import { Defaults } from "schema/Defaults";
import { RefinerValuesList } from "./RefinerValuesList";
const Field_RefinerValues: ILookupFieldDefinition = {
type: FieldType.Lookup,
name: 'RefinerValues',
displayName: 'Refiner Values',
required: false,
multi: true,
lookupListTitle: RefinerValuesList.title,
showField: RefinerValuesList.field_Value.name
};
const Field_IncludeInApprovalEmail: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'IncludeInApprovalEmail',
displayName: 'Include In Approval Email',
default: 'Yes'
};
const Field_Users: IUserFieldDefinition = {
type: FieldType.User,
name: 'Users',
userSelectionMode: "PeopleOnly",
required: true,
multi: true
};
const View_AllApprovers: IViewDefinition = {
title: "All Approvers",
rowLimit: 250,
paged: true,
default: true,
fields: includeStandardViewFields(
Field_RefinerValues,
Field_IncludeInApprovalEmail,
Field_Users
)
};
export interface IApproversListDefinition extends IListDefinition {
view_AllApprovers: IViewDefinition;
}
export const ApproversList: IApproversListDefinition = {
title: Defaults.ListTitles.Approvers,
description: '',
template: ListTemplateType.GenericList,
dependencies: [RefinerValuesList],
permissions: {
copyRoleAssignments: false,
userRoles: [
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'memberGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
]
},
fields: [
Field_RefinerValues,
Field_IncludeInApprovalEmail,
Field_Users
],
views: [
View_AllApprovers
],
view_AllApprovers: View_AllApprovers
};

View File

@ -0,0 +1,147 @@
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, INumberFieldDefinition, ListTemplateType, IChoiceFieldDefinition, IBooleanFieldDefinition, RoleOperation, RoleType } from "common/sharepoint";
import { ViewKeys, ViewYearFYKeys } from "model";
import { Defaults } from "schema/Defaults";
const Field_SchemaVersion: INumberFieldDefinition = {
type: FieldType.Number,
name: 'SchemaVersion',
displayName: "Schema Version",
required: true
};
const Field_CurrentUpgradeAction: INumberFieldDefinition = {
type: FieldType.Number,
name: 'CurrentUpgradeAction',
displayName: "Current Upgrade Action"
};
const Field_FiscalYearSartMonth: INumberFieldDefinition = {
type: FieldType.Number,
name: 'FiscalYearSartMonth',
displayName: "Fiscal Year Sart Month",
min: 1,
max: 12,
required: true,
default: "1"
};
export const Field_FiscalYearStartYear: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'FiscalYearStartYear',
displayName: "Fiscal Year Start Year",
choices: Object.keys(ViewYearFYKeys),
default: ViewYearFYKeys["Next Year"]
};
const Field_DefaultView: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'DefaultView',
displayName: "Default View",
choices: Object.keys(ViewKeys),
default: ViewKeys.monthly
};
const Field_UseRefiners: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseRefiners',
displayName: "Use Refiners",
default: "Yes"
};
const Field_RefinerRailInitiallyExpanded: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'RefinerRailInitiallyExpanded',
displayName: "Refiner Rail Initially Expanded",
default: "Yes"
};
const Field_QuarterViewGroupByRefinerId: INumberFieldDefinition = {
type: FieldType.Number,
name: 'QuarterViewGroupByRefinerId',
displayName: 'Quarter View Group By Refiner Id'
};
const Field_UseApprovals: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseApprovals',
displayName: "Use Approvals",
default: "No"
};
const Field_AllowConfidentialEvents: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'AllowConfidentialEvents',
displayName: "Allow Confidential Events",
default: "No"
};
export const Field_UseApprovalsEmailNotification: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseApprovalsEmailNotification',
displayName: "Use Approvals Email Notification",
default: "No"
};
export const Field_UseApprovalsTeamsNotification: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'UseApprovalsTeamsNotification',
displayName: "Use Approvals Teams Notification",
default: "No"
};
export const View_AllItems: IViewDefinition = {
title: "All Configurations",
rowLimit: 1,
paged: false,
default: true,
query: '',
fields: includeStandardViewFields(
Field_SchemaVersion,
Field_CurrentUpgradeAction,
Field_FiscalYearSartMonth,
Field_DefaultView,
Field_UseRefiners,
Field_RefinerRailInitiallyExpanded,
Field_QuarterViewGroupByRefinerId,
Field_UseApprovals,
Field_AllowConfidentialEvents,
Field_FiscalYearStartYear,
Field_UseApprovalsEmailNotification,
Field_UseApprovalsTeamsNotification
)
};
export interface IConfigurationListDefinition extends IListDefinition {
view_AllItems: IViewDefinition;
}
export const ConfigurationList: IConfigurationListDefinition = {
title: Defaults.ListTitles.Configuration,
description: '',
template: ListTemplateType.GenericList,
permissions: {
copyRoleAssignments: false,
userRoles: [
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'memberGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
]
},
siteFields: [],
fields: [
Field_SchemaVersion,
Field_CurrentUpgradeAction,
Field_FiscalYearSartMonth,
Field_DefaultView,
Field_UseRefiners,
Field_RefinerRailInitiallyExpanded,
Field_QuarterViewGroupByRefinerId,
Field_UseApprovals,
Field_AllowConfidentialEvents,
Field_FiscalYearStartYear,
Field_UseApprovalsEmailNotification,
Field_UseApprovalsTeamsNotification
],
views: [View_AllItems],
view_AllItems: View_AllItems
};

View File

@ -0,0 +1,229 @@
import { DateTimeFieldFormatType } from "@pnp/sp/fields";
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, ITextFieldDefinition, IBooleanFieldDefinition, IUserFieldDefinition, ILookupFieldDefinition, IDateTimeFieldDefinition, ListTemplateType, ITitleFieldDefinition, IRecurrenceFieldDefinition, IIntegerFieldDefinition, IGuidFieldDefinition, RoleOperation, RoleType, IChoiceFieldDefinition } from "common/sharepoint";
import { EventModerationStatus } from "model";
import { Defaults } from "schema/Defaults";
import { RefinerValuesList } from "./RefinerValuesList";
const Field_Title: ITitleFieldDefinition = {
type: FieldType.Text,
name: 'Title',
required: true
};
const Field_Description: ITextFieldDefinition = {
type: FieldType.Text,
name: 'Description',
multi: true
};
const Field_Location: ITextFieldDefinition = {
type: FieldType.Text,
name: 'Location'
};
const Field_EventDate: IDateTimeFieldDefinition = {
type: FieldType.DateTime,
name: 'EventDate',
displayName: 'Start Time',
required: true,
dateTimeFormat: DateTimeFieldFormatType.DateTime
};
const Field_EndDate: IDateTimeFieldDefinition = {
type: FieldType.DateTime,
name: 'EndDate',
displayName: 'End Time',
required: true,
dateTimeFormat: DateTimeFieldFormatType.DateTime
};
const Field_fAllDayEvent: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'fAllDayEvent',
displayName: 'All Day Event',
default: 'No'
};
const Field_fRecurrence: IRecurrenceFieldDefinition = {
type: FieldType.Recurrence,
name: 'fRecurrence',
displayName: 'Recurrence'
};
const Field_EventType: IIntegerFieldDefinition = {
type: FieldType.Integer,
name: 'EventType',
displayName: 'Event Type'
};
const Field_UID: IGuidFieldDefinition = {
type: FieldType.Guid,
name: 'UID'
};
const Field_RecurrenceID: IDateTimeFieldDefinition = {
type: FieldType.DateTime,
name: 'RecurrenceID',
displayName: 'Recurrence ID',
dateTimeFormat: DateTimeFieldFormatType.DateTime
};
const Field_MasterSeriesItemID: IIntegerFieldDefinition = {
type: FieldType.Integer,
name: 'MasterSeriesItemID'
};
const Field_RecurrenceData: ITextFieldDefinition = {
type: FieldType.Text,
name: 'RecurrenceData',
multi: true
};
const Field_Duration: IIntegerFieldDefinition = {
type: FieldType.Integer,
name: 'Duration'
};
const Field_RefinerValues: ILookupFieldDefinition = {
type: FieldType.Lookup,
name: 'RefinerValues',
displayName: 'Refiner Values',
required: false,
multi: true,
lookupListTitle: RefinerValuesList.title,
showField: RefinerValuesList.field_Value.name
};
const Field_Contacts: IUserFieldDefinition = {
type: FieldType.User,
name: 'Contacts',
userSelectionMode: "PeopleOnly",
multi: true
};
const Field_Confidential: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'IsConfidential',
displayName: 'Is Confidential',
default: 'No'
};
const Field_RestrictedToAccounts: IUserFieldDefinition = {
type: FieldType.User,
name: 'RestrictedToAccounts',
displayName: 'Restricted To Accounts',
userSelectionMode: "PeopleAndGroups",
multi: true
};
const Field_ModerationStatus: IChoiceFieldDefinition = {
type: FieldType.Choice,
name: 'ModerationStatus',
displayName: 'Moderation Status',
choices: EventModerationStatus.all.map(s => s.name),
default: EventModerationStatus.Pending.name
};
const Field_Moderator: IUserFieldDefinition = {
type: FieldType.User,
name: 'Moderator',
userSelectionMode: "PeopleOnly",
required: false
};
const Field_ModerationTimestamp: IDateTimeFieldDefinition = {
type: FieldType.DateTime,
name: 'ModerationTimestamp',
displayName: 'Moderation Timestamp',
required: false,
dateTimeFormat: DateTimeFieldFormatType.DateTime
};
const Field_ModerationMessage: ITextFieldDefinition = {
type: FieldType.Text,
name: 'ModerationMessage',
displayName: 'Moderation Message',
multi: true,
required: false
};
const View_AllEvents: IViewDefinition = {
title: "All RoB Events",
rowLimit: 600,
paged: true,
default: false,
fields: includeStandardViewFields(
Field_Description,
Field_Location,
Field_EventDate,
Field_EndDate,
Field_fAllDayEvent,
Field_fRecurrence,
Field_EventType,
Field_UID,
Field_RecurrenceID,
Field_MasterSeriesItemID,
Field_RecurrenceData,
Field_Duration,
Field_RefinerValues,
Field_Contacts,
Field_Confidential,
Field_RestrictedToAccounts,
Field_ModerationStatus,
Field_Moderator,
Field_ModerationTimestamp,
Field_ModerationMessage
),
// need to sort by ID ascending in order to ensure the series master is loaded before any exceptions to the series
query: `
<OrderBy>
<FieldRef Name="ID" Ascending="TRUE"/>
</OrderBy>
`
};
export interface IEventsListDefinition extends IListDefinition {
view_AllEvents: IViewDefinition;
}
export const EventsList: IEventsListDefinition = {
title: Defaults.ListTitles.Events,
description: '',
template: ListTemplateType.EventsList,
dependencies: [RefinerValuesList],
permissions: {
copyRoleAssignments: false,
userRoles: [
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'memberGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
]
},
fields: [
Field_Title,
Field_Description,
Field_Location,
Field_EventDate,
Field_EndDate,
Field_fAllDayEvent,
Field_fRecurrence,
Field_EventType,
Field_UID,
Field_RecurrenceID,
Field_MasterSeriesItemID,
Field_RecurrenceData,
Field_Duration,
Field_RefinerValues,
Field_Contacts,
Field_Confidential,
Field_RestrictedToAccounts,
Field_ModerationStatus,
Field_Moderator,
Field_ModerationTimestamp,
Field_ModerationMessage
],
views: [
View_AllEvents
],
view_AllEvents: View_AllEvents
};

View File

@ -0,0 +1,116 @@
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, ITitleFieldDefinition, INumberFieldDefinition, ITextFieldDefinition, IBooleanFieldDefinition, viewFields, ILookupFieldDefinition, IFieldDefinition, ListTemplateType, RoleOperation, RoleType } from "common/sharepoint";
import { Defaults } from "schema/Defaults";
import { RefinersList } from "./RefinersList";
const Field_Value: ITitleFieldDefinition = {
type: FieldType.Text,
name: 'Title',
displayName: 'Value',
required: true,
maxLength: 50
};
const Field_Refiner: ILookupFieldDefinition = {
type: FieldType.Lookup,
name: 'Refiner',
required: true,
lookupListTitle: RefinersList.title,
showField: RefinersList.field_Name.name
};
const Field_Order: INumberFieldDefinition = {
type: FieldType.Number,
name: 'Order',
min: 0
};
const Field_Tag: ITextFieldDefinition = {
type: FieldType.Text,
name: 'Tag',
maxLength: 3
};
const Field_Color: ITextFieldDefinition = {
type: FieldType.Text,
name: 'Color'
};
const Field_Archived: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: "Archived",
default: 'No'
};
const View_AllRefinerValues: IViewDefinition = {
title: "All Refiner Values",
rowLimit: 1000,
paged: true,
default: false,
fields: includeStandardViewFields(
Field_Order,
Field_Refiner,
Field_Tag,
Field_Color,
Field_Archived
)
};
const View_ActiveRefinerValues: IViewDefinition = {
title: "Active Refiner Values",
rowLimit: 500,
paged: true,
default: true,
fields: viewFields(
Field_Value,
Field_Refiner
),
query: `
<GroupBy>
<FieldRef Name="${Field_Refiner.name}" />
</GroupBy>
<OrderBy>
<FieldRef Name="${Field_Order.name}" Ascending="TRUE"/>
<FieldRef Name="${Field_Value.name}" Ascending="TRUE"/>
</OrderBy>
<Where>
<Neq>
<FieldRef Name='${Field_Archived.name}' />
<Value Type='Integer'>1</Value>
</Neq>
</Where>
`
};
export interface IRefinerValuesListDefinition extends IListDefinition {
field_Value: IFieldDefinition;
view_AllRefinerValues: IViewDefinition;
}
export const RefinerValuesList: IRefinerValuesListDefinition = {
title: Defaults.ListTitles.RefinerValues,
description: '',
template: ListTemplateType.GenericList,
dependencies: [RefinersList],
permissions: {
copyRoleAssignments: false,
userRoles: [
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'memberGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
]
},
fields: [
Field_Value,
Field_Order,
Field_Refiner,
Field_Tag,
Field_Color,
Field_Archived
],
views: [
View_AllRefinerValues,
View_ActiveRefinerValues
],
field_Value: Field_Value,
view_AllRefinerValues: View_AllRefinerValues
};

View File

@ -0,0 +1,114 @@
import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, ITitleFieldDefinition, INumberFieldDefinition, IBooleanFieldDefinition, IFieldDefinition, ListTemplateType, RoleOperation, RoleType } from "common/sharepoint";
import { Defaults } from "schema/Defaults";
const Field_Name: ITitleFieldDefinition = {
type: FieldType.Text,
name: 'Title',
displayName: 'Name',
required: true,
maxLength: 50
};
const Field_Order: INumberFieldDefinition = {
type: FieldType.Number,
name: 'Order',
min: 0
};
const Field_AllowMultiselect: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'AllowMultiselect',
displayName: 'Allow Multiselect',
default: 'No'
};
const Field_Required: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'Required',
default: 'No'
};
const Field_InitiallyExpanded: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'InitiallyExpanded',
displayName: 'Initially Expanded',
default: 'Yes'
};
const Field_EnableColors: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'EnableColors',
displayName: 'Enable Colors',
default: 'No'
};
const Field_EnableTags: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'EnableTags',
displayName: 'Enable Tags',
default: 'No'
};
const Field_CustomSort: IBooleanFieldDefinition = {
type: FieldType.Boolean,
name: 'CustomSort',
displayName: 'Custom Sort',
default: 'No'
};
const View_AllRefiners: IViewDefinition = {
title: "All Refiners",
rowLimit: 100,
paged: true,
default: true,
fields: includeStandardViewFields(
Field_Order,
Field_AllowMultiselect,
Field_Required,
Field_InitiallyExpanded,
Field_EnableColors,
Field_EnableTags,
Field_CustomSort
),
query: `
<OrderBy>
<FieldRef Name="${Field_Order.name}" Ascending="TRUE"/>
<FieldRef Name="${Field_Name.name}" Ascending="TRUE"/>
</OrderBy>
`
};
export interface IRefinersListDefinition extends IListDefinition {
field_Name: IFieldDefinition;
view_AllRefiners: IViewDefinition;
}
export const RefinersList: IRefinersListDefinition = {
title: Defaults.ListTitles.Refiners,
description: '',
template: ListTemplateType.GenericList,
dependencies: [],
permissions: {
copyRoleAssignments: false,
userRoles: [
{ operation: RoleOperation.Add, roleType: RoleType.Administrator, userType: 'ownerGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'memberGroup' },
{ operation: RoleOperation.Add, roleType: RoleType.Reader, userType: 'visitorGroup' }
]
},
fields: [
Field_Name,
Field_Order,
Field_AllowMultiselect,
Field_Required,
Field_InitiallyExpanded,
Field_EnableColors,
Field_EnableTags,
Field_CustomSort
],
views: [
View_AllRefiners
],
field_Name: Field_Name,
view_AllRefiners: View_AllRefiners
};

Some files were not shown because too many files have changed in this diff Show More