Compare commits
5 Commits
b21fe189e4
...
257303bb86
Author | SHA1 | Date |
---|---|---|
Mrigango | 257303bb86 | |
Mrigango Deb | 6ea6c35683 | |
Mrigango Deb | b83406b179 | |
Mrigango Deb | 99d6278688 | |
Mrigango Deb | 01f03d34a2 |
|
@ -0,0 +1 @@
|
|||
v16.15.1
|
|
@ -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" />
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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.1.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
|
@ -1,17 +1,29 @@
|
|||
{
|
||||
"name": "rhythm-of-business-calendar",
|
||||
"version": "1.0.0",
|
||||
"version": "5.0.1",
|
||||
"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 |
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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;
|
|
@ -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>;
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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]);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
|
@ -0,0 +1,5 @@
|
|||
@import 'common.module';
|
||||
|
||||
.infoLabelStyle > label:first-child{
|
||||
display: inline !important;
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
// export { default as ChannelsPanel, IChannelsPanel } from './ChannelsPanel';
|
||||
// export { default as ConfigureChannelsPanel, IConfigureChannelsPanel } from './ConfigureChannelsPanel';
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
);
|
||||
};
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 && <>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>;
|
||||
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
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';
|
||||
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' }}>{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;
|
||||
|
|
@ -0,0 +1,647 @@
|
|||
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';
|
||||
|
||||
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 <span>{item.description}</span>;
|
||||
},
|
||||
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')
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>;
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export class EventModerationStatus {
|
|||
) {
|
||||
}
|
||||
|
||||
public clone(): this {
|
||||
public clone(): EventModerationStatus {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -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.parseHTML(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);
|
||||
|
|
|
@ -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"];
|
|
@ -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"];
|
|
@ -4,7 +4,8 @@ export const ViewKeys = stringToEnum([
|
|||
"daily",
|
||||
"weekly",
|
||||
"monthly",
|
||||
"quarter"
|
||||
"quarter",
|
||||
"list"
|
||||
]);
|
||||
|
||||
export type ViewKeys = keyof (typeof ViewKeys);
|
||||
|
|
|
@ -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"];
|
|
@ -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';
|
|
@ -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>;
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
};
|
|
@ -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
|
||||
});
|
|
@ -1,3 +1,3 @@
|
|||
export { Configuration, ConfigurationMap, ReadonlyConfigurationMap } from './Configuration';
|
||||
export { ConfigurationList } from './lists';
|
||||
export { ConfigurationList, EventsList } from './lists';
|
||||
export { IRhythmOfBusinessCalendarSchema } from './RhythmOfBusinessCalendarSchema';
|
|
@ -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
|
||||
// };
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
|
@ -0,0 +1,6 @@
|
|||
import { IUpgrade } from "common/sharepoint";
|
||||
import {IROBCalendarUpgradeAction} from "./IROBCalendarUpgradeAction";
|
||||
|
||||
export interface IROBCalendarUpgrade extends IUpgrade{
|
||||
actions:IROBCalendarUpgradeAction[];
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IUpgradeAction } from "common/sharepoint";
|
||||
export interface IROBCalendarUpgradeAction extends IUpgradeAction{
|
||||
readonly shared?: boolean;
|
||||
execute() : Promise<void>;
|
||||
|
||||
}
|
|
@ -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";
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(),],
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
Loading…
Reference in New Issue