From 01f03d34a2c70516669af8eda0860d09d75271f2 Mon Sep 17 00:00:00 2001 From: Mrigango Deb Date: Fri, 13 Sep 2024 16:39:50 +0530 Subject: [PATCH 1/5] implementation of fatures enhancement --- .../src/apps/RhythmOfBusinessCalendarApp.tsx | 2 +- .../src/assets/onboarding/ETemplateView.png | Bin 0 -> 2830 bytes .../src/assets/onboarding/OIP.jpg | Bin 0 -> 5425 bytes .../onboarding/ProductivityStudioLogo.png | Bin 0 -> 88058 bytes .../src/assets/onboarding/webpartTeamsApp.png | Bin 0 -> 893 bytes .../src/common/MomentRange.ts | 29 +- .../src/common/components/CalendarPicker.tsx | 2 +- .../src/common/components/InfoTooltip.tsx | 8 +- .../src/common/components/LiveDatePicker.tsx | 25 +- .../common/components/LiveMultiTextField.tsx | 120 ++ .../src/common/components/LiveTextField.tsx | 8 +- .../src/common/components/LiveUserPicker.tsx | 136 +- .../src/common/components/LiveUtils.ts | 15 + .../src/common/components/TimePicker.tsx | 10 +- .../src/common/components/UserPicker.tsx | 145 +- .../src/common/components/index.ts | 1 + .../styles/LiveTextField.module.scss | 5 + .../directory/DirectoryServiceDescriptor.ts | 1 + .../directory/OnlineDirectoryService.ts | 186 ++- .../timezones/OnlineTimeZoneService.ts | 93 +- .../timezones/TimeZoneServiceDescriptor.ts | 14 +- .../src/common/sharepoint/SPField.ts | 359 +++- .../sharepoint/schema/ElementProvisioner.ts | 12 +- .../ChannelsConfiguration/ChannelsPanel.tsx | 285 ++++ .../ConfigureChannelsPanel.tsx | 266 +++ .../components/ChannelsConfiguration/index.ts | 2 + .../ProductivityStudioLogo.module.scss | 27 + .../src/components/ProductivityStudioLogo.tsx | 27 + .../components/approvals/ApprovalDialog.tsx | 9 +- .../components/events/EventBar.module.scss | 17 + .../src/components/events/EventBar.tsx | 40 +- .../components/events/EventDetailsCallout.tsx | 3 + .../src/components/events/EventFilter.tsx | 57 +- .../events/EventOverview.module.scss | 17 +- .../src/components/events/EventOverview.tsx | 4 +- .../src/components/events/EventPanel.tsx | 1463 ++++++++++++----- .../src/components/events/IEventCommands.ts | 6 +- .../useEventCommandActionButtons.module.scss | 19 + .../hooks/useEventCommandActionButtons.tsx | 142 +- .../hooks/useExecuteEventDeepLink.ts | 6 +- .../src/components/hooks/useSettings.ts | 6 +- .../src/components/loc/componentstrings.d.ts | 76 +- .../src/components/loc/en-us.js | 76 +- .../settings/SettingsPanel.module.scss | 13 +- .../src/components/settings/SettingsPanel.tsx | 248 ++- .../components/views/DateRotatorController.ts | 9 +- .../src/components/views/IViewDescriptor.ts | 3 +- .../src/components/views/IViewProps.ts | 6 + .../components/views/ViewRoute.module.scss | 92 ++ .../src/components/views/ViewRoute.tsx | 1213 +++++++++++--- .../src/components/views/Views.ts | 4 +- .../src/components/views/day/DayView.tsx | 4 +- .../components/views/list/ExportToExcel.tsx | 125 ++ .../src/components/views/list/ListView.tsx | 647 ++++++++ .../src/components/views/month/ContentRow.tsx | 5 +- .../src/components/views/month/EventItem.tsx | 4 +- .../src/components/views/month/MonthView.tsx | 10 +- .../src/components/views/month/Week.tsx | 5 +- .../components/views/month/WeekBackground.tsx | 10 +- .../components/views/quarter/EventItem.tsx | 7 +- .../src/components/views/quarter/Month.tsx | 10 +- .../components/views/quarter/QuarterView.tsx | 8 +- .../views/quarter/RefinerValueEvents.tsx | 5 +- .../src/components/views/quarter/Utils.ts | 9 +- .../src/components/views/week/Background.tsx | 10 +- .../src/components/views/week/Builder.ts | 1 - .../src/model/Cadence.ts | 42 +- .../src/model/ChannelsConfigurations.ts | 110 ++ .../src/model/Event.ts | 54 +- .../src/model/EventModerationStatus.ts | 2 +- .../src/model/EventOccurrence.ts | 45 + .../src/model/ListViewKeys.ts | 24 + .../src/model/TemplateViewKeys.ts | 11 + .../src/model/ViewKeys.ts | 3 +- .../src/model/ViewYearFYKeys.ts | 10 + .../src/model/index.ts | 5 +- .../src/schema/Configuration.ts | 34 +- .../src/schema/Defaults.ts | 5 +- .../schema/RhythmOfBusinessCalendarSchema.ts | 10 +- .../src/schema/index.ts | 2 +- .../lists/ChannelsConfigurationsList.ts | 78 + .../src/schema/lists/ConfigurationList.ts | 67 +- .../src/schema/lists/EventsList.ts | 13 +- .../src/schema/lists/index.ts | 3 +- .../schema/upgrades/IROBCalendarUpgrade.ts | 6 + .../upgrades/IROBCalendarUpgradeAction.ts | 6 + .../src/schema/upgrades/index.ts | 6 + ...ddFYStartYearColumnToConfigutationList.tsx | 18 + .../src/schema/upgrades/v2.0.0/Definition.ts | 9 + .../v2.0.0/UpdateAllConfigurationListView.ts | 15 + .../v2.0.0/schemaSnapshot/ApproversList.ts | 68 + .../schemaSnapshot/ConfigurationList.ts | 147 ++ .../v2.0.0/schemaSnapshot/EventsList.ts | 229 +++ .../schemaSnapshot/RefinerValuesList.ts | 116 ++ .../v2.0.0/schemaSnapshot/RefinersList.ts | 114 ++ .../upgrades/v2.0.0/schemaSnapshot/index.ts | 6 + ...EnableOutlookColumnToConfigutationList.tsx | 16 + .../AddTeamsGroupChatIdToEventsList.tsx | 16 + .../CreateChannelsConfigurationsListAction.ts | 9 + .../src/schema/upgrades/v3.0.0/Definition.ts | 19 + ...UpdateAllChannelsConfigurationsListView.ts | 15 + .../v3.0.0/UpdateAllConfigurationListView.ts | 15 + .../v3.0.0/UpdateAllEventsListView.ts | 12 + .../v3.0.0/schemaSnapshot/ApproversList.ts | 68 + .../ChannelsConfigurationsList.ts | 78 + .../schemaSnapshot/ConfigurationList.ts | 156 ++ .../v3.0.0/schemaSnapshot/EventsList.ts | 238 +++ .../schemaSnapshot/RefinerValuesList.ts | 116 ++ .../v3.0.0/schemaSnapshot/RefinersList.ts | 114 ++ .../upgrades/v3.0.0/schemaSnapshot/index.ts | 6 + .../AddListViewColumnToConfigutationList.tsx | 16 + .../src/schema/upgrades/v4.0.0/Definition.ts | 11 + .../v4.0.0/UpdateAllConfigurationListView.ts | 15 + .../v4.0.0/schemaSnapshot/ApproversList.ts | 68 + .../schemaSnapshot/ConfigurationList.ts | 168 ++ .../v4.0.0/schemaSnapshot/EventsList.ts | 238 +++ .../schemaSnapshot/RefinerValuesList.ts | 116 ++ .../v4.0.0/schemaSnapshot/RefinersList.ts | 114 ++ .../upgrades/v4.0.0/schemaSnapshot/index.ts | 6 + ...dTemplateViewColumnToConfigutationList.tsx | 16 + .../src/schema/upgrades/v5.0.0/Definition.ts | 11 + .../v5.0.0/UpdateAllConfigurationListView.ts | 15 + .../v5.0.0/schemaSnapshot/ApproversList.ts | 68 + .../schemaSnapshot/ConfigurationList.ts | 180 ++ .../v5.0.0/schemaSnapshot/EventsList.ts | 238 +++ .../schemaSnapshot/RefinerValuesList.ts | 116 ++ .../v5.0.0/schemaSnapshot/RefinersList.ts | 114 ++ .../upgrades/v5.0.0/schemaSnapshot/index.ts | 6 + .../configuration/ConfigurationLoader.ts | 29 +- .../events/ChannelsConfigurationsLoader.ts | 57 + .../src/services/events/EventLoader.ts | 183 ++- .../events/EventsServiceDescriptor.ts | 9 +- .../services/events/OnlineEventsService.ts | 671 +++++++- .../src/services/events/RecurrenceData.ts | 2 +- .../services/events/iCalendarFileBuilder.ts | 26 +- 135 files changed, 9718 insertions(+), 1078 deletions(-) create mode 100644 samples/react-rhythm-of-business-calendar/src/assets/onboarding/ETemplateView.png create mode 100644 samples/react-rhythm-of-business-calendar/src/assets/onboarding/OIP.jpg create mode 100644 samples/react-rhythm-of-business-calendar/src/assets/onboarding/ProductivityStudioLogo.png create mode 100644 samples/react-rhythm-of-business-calendar/src/assets/onboarding/webpartTeamsApp.png create mode 100644 samples/react-rhythm-of-business-calendar/src/common/components/LiveMultiTextField.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/common/components/styles/LiveTextField.module.scss create mode 100644 samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ChannelsPanel.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ConfigureChannelsPanel.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.module.scss create mode 100644 samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.module.scss create mode 100644 samples/react-rhythm-of-business-calendar/src/components/views/list/ExportToExcel.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/components/views/list/ListView.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/model/ChannelsConfigurations.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/model/ListViewKeys.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/model/TemplateViewKeys.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/model/ViewYearFYKeys.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/lists/ChannelsConfigurationsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/IROBCalendarUpgrade.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/IROBCalendarUpgradeAction.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/AddFYStartYearColumnToConfigutationList.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/Definition.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/UpdateAllConfigurationListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ApproversList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ConfigurationList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/EventsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinerValuesList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinersList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddEnableOutlookColumnToConfigutationList.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddTeamsGroupChatIdToEventsList.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/CreateChannelsConfigurationsListAction.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/Definition.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllChannelsConfigurationsListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllConfigurationListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllEventsListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ApproversList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ChannelsConfigurationsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ConfigurationList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/EventsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinerValuesList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinersList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/AddListViewColumnToConfigutationList.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/Definition.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/UpdateAllConfigurationListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ApproversList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ConfigurationList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/EventsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinerValuesList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinersList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/AddTemplateViewColumnToConfigutationList.tsx create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/Definition.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/UpdateAllConfigurationListView.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ApproversList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ConfigurationList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/EventsList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinerValuesList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinersList.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/index.ts create mode 100644 samples/react-rhythm-of-business-calendar/src/services/events/ChannelsConfigurationsLoader.ts diff --git a/samples/react-rhythm-of-business-calendar/src/apps/RhythmOfBusinessCalendarApp.tsx b/samples/react-rhythm-of-business-calendar/src/apps/RhythmOfBusinessCalendarApp.tsx index 385373fa9..68e4c66e6 100644 --- a/samples/react-rhythm-of-business-calendar/src/apps/RhythmOfBusinessCalendarApp.tsx +++ b/samples/react-rhythm-of-business-calendar/src/apps/RhythmOfBusinessCalendarApp.tsx @@ -81,7 +81,7 @@ class RhythmOfBusinessCalendarApp extends Component { return ( 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 literal 0 HcmV?d00001 diff --git a/samples/react-rhythm-of-business-calendar/src/assets/onboarding/OIP.jpg b/samples/react-rhythm-of-business-calendar/src/assets/onboarding/OIP.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6728a7f859ffe3634d80839306f15082490b80ef GIT binary patch literal 5425 zcmcIo2|QG7+dq~WG{`W6v6MY~m~2UwtQ93xNJ3$3Swcio(_G+QO){X+q zyThS{(m??YAgK>Z17(B$je;R!m)lz2S=(Hz?m25&keiT{nH?BYdp5|Bue*B1^>caU z@i9t3qS+Fmt??2|+uP;wd$UHS=K9AJ%_b{XT{s>=8#3}ejnM5KE$(Ff~*!95}(hZDn*3y$vM zfR~=Fh=Z2MRWUH@q^9=g3P?4Hh)yr(RF7GMTDtc$T2Jv`6b%e`Qa z^Uul0F`j1TZw(1uwc~Qb~8$)1ZxAx_bf8GX$iIxaVqA^1Y?xh_rv9ABfs-`n5! z_V4;oq0Q$qF|Yrff6-C$YQ#xAy`F_In)`r}b*nuP zR=3nE{YhDSSzw+)#DQJ!Dmx9+-Fr20aSg883snzmB)Vwqhbda{`~`vCLA+#&6y#av zoS)&1^c){!0hKoW>vzDUG~_#hCJM!RfK_+KfoG)7Gzzn^$*BrVt$X$kofWANoT&JUgu1duK3_p%m=Pjs?-EG?zi$=+#$Exr=v{5C3_O0demg3 zV_$�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 literal 0 HcmV?d00001 diff --git a/samples/react-rhythm-of-business-calendar/src/assets/onboarding/ProductivityStudioLogo.png b/samples/react-rhythm-of-business-calendar/src/assets/onboarding/ProductivityStudioLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4c2f7d77dda58827d0b59f16014c5b9a3353d2f1 GIT binary patch literal 88058 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 ZLfNESgk##k?E~PSilT->;YG`!{{v>@!l?iN literal 0 HcmV?d00001 diff --git a/samples/react-rhythm-of-business-calendar/src/assets/onboarding/webpartTeamsApp.png b/samples/react-rhythm-of-business-calendar/src/assets/onboarding/webpartTeamsApp.png new file mode 100644 index 0000000000000000000000000000000000000000..52d28ca62f6b7e59b2e69124207569c4db449cfe GIT binary patch literal 893 zcmV-@1A_dCP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0}@F@K~z{r&6i2Y zO<@?vuX&zlks(7NLo#ioD2fybSy(D7_I9>x>{uzu!iJP2r6fWr3rZvwrb3y0h7Z5r z?Y^h?-h0mJ-qX3a?_a;pxo5cNf1dYwp7%YKzOH_?ZJInc2u8yI_y^zM7yPOH?zplH z_J`px2Q(R9r?Fnp><{<}-)q~yP^$4PgCk)SOt1cGO`7`+J)mK?0nZgumca=y2FBI) zjiwZl=8SHLv{9E&@CrifG804`|p?ua*&`UU$ zD9xbN8^6slxC0@%%itwk28Y^b>SGw#Z|f90_M#!M5G;~qa1{KrFJJ|hC8K6nmke*LG!OE4BXLB`ImK0zSILx=fX)jG7NQr_$lQS*sjk3yKfWy@;zM%7O-fW=4wiW5eU^&cHZy{=7DP%)-d1SPvf@ZFQ6Zw63U)zdNlUr~T zPQeZM3eUg>{~V0E_s0RSEBOCA!RxH@k6;mn<(Vt@%yp!_ZXdW->qetC;*N$X;z-Sc zYE5KSt$(l - !range1.start.isAfter(range2.end) && !range1.end.isBefore(range2.start, units) -} \ No newline at end of file + + 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); + } + + + } +} + + + + + + diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/CalendarPicker.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/CalendarPicker.tsx index 8f4209943..a6c34397d 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/CalendarPicker.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/CalendarPicker.tsx @@ -46,7 +46,7 @@ export const CalendarPicker: FC = ({ return ( - + {buttonLabel} {showCalendar && diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/InfoTooltip.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/InfoTooltip.tsx index 7d264d8f3..2d883f605 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/InfoTooltip.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/InfoTooltip.tsx @@ -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 = ({ text, hideIcon = false, tooltipHostProps, - children + children, + isCssClassName = false }: IProps) => + {children} {text && !hideIcon && } + diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/LiveDatePicker.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/LiveDatePicker.tsx index aa68e36dc..3972fde2c 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/LiveDatePicker.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/LiveDatePicker.tsx @@ -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 = , P extends PropsOfType) => updateField(e => setValue(e, propertyName, val)), [updateField, propertyName]); const renderValue = useCallback((val: LiveType) => {(val as DataType)?.isValid() ? (val as DataType).format('dddd, MMMM DD, YYYY') : ''}, []); const formatDate = useCallback((val: Date) => formatMoment(moment(val)), [formatMoment]); @@ -52,7 +73,7 @@ const LiveDatePicker = , P extends PropsOfType 0 ? value && value.add(totalTime,'seconds').toDate() : value && value.add(totalTime,'seconds').toDate() } onSelectDate={onChange} /> {!label && renderLiveUpdateMark()} diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/LiveMultiTextField.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/LiveMultiTextField.tsx new file mode 100644 index 000000000..271df26e7 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/common/components/LiveMultiTextField.tsx @@ -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 { + parse: (val: string) => T; + toString: (val: T) => string; +} + +class NonConverter implements IConverter { + public parse(val: string) { + return val; + } + public toString(val: string) { + return val; + } +} + +interface IProps< + E extends ListItemEntity, + P extends PropsOfType +> extends ITextFieldProps { + entity: E; + propertyName: P; + updateField: (update: (data: E) => void, callback?: () => any) => void; + converter?: IConverter>; + rules?: ValidationRule[]; + showValidationFeedback?: boolean; + liveUpdateMarkClassName?: string; + tooltip?: string; + nextFocusComponent?: (assignFocus: boolean) => void; +} + +const LiveMultiTextField = < + E extends ListItemEntity, + P extends PropsOfType +>( + props: IProps +) => { + const { + entity, + propertyName, + converter = new NonConverter() as unknown as IConverter>, + rules, + showValidationFeedback, + label, + liveUpdateMarkClassName, + updateField, + nextFocusComponent, + } = props; + + const value = converter.toString(getCurrentValue(entity, propertyName)); + const updateValue = useCallback( + (val: LiveType) => + updateField((e) => setValue(e, propertyName, val)), + [updateField, propertyName] + ); + const renderValue = useCallback( + (val: LiveType) => ( + <>{(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) + ); + }); + }, + [updateField, propertyName, converter] + ); + + const onChange = (val: string) => { + onChangeNew(entity, val); + if (val.endsWith("\t

")) nextFocusComponent(true); + return val; + }; + + return ( + + + {(renderLiveUpdateMark) => ( + <> + + + {!label && + renderLiveUpdateMark({ + className: liveUpdateMarkClassName, + })} + + )} + + + ); +}; + +export default LiveMultiTextField; diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/LiveTextField.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/LiveTextField.tsx index acb044217..f6865e66a 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/LiveTextField.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/LiveTextField.tsx @@ -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 { @@ -27,12 +27,14 @@ interface IProps, P extends PropsOfType void, callback?: () => any) => void; } const LiveTextField = , P extends PropsOfType>(props: IProps) => { const { entity, + isCssClassName = false, propertyName, converter = new NonConverter() as unknown as IConverter>, rules, @@ -59,7 +61,7 @@ const LiveTextField = , P extends PropsOfType { return label && - {defaultRender(textFieldProps)} + {defaultRender(textFieldProps)} {renderLiveUpdateMark({ className: liveUpdateMarkClassName })} ; }} diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/LiveUserPicker.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/LiveUserPicker.tsx index 2f7d4d596..e890256c4 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/LiveUserPicker.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/LiveUserPicker.tsx @@ -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, P extends PropsOfType | PropsOfType> extends Omit { +interface IProps< + E extends ListItemEntity, + P extends PropsOfType | PropsOfType +> extends Omit { entity: E; propertyName: P; rules?: ValidationRule[]; @@ -22,10 +25,16 @@ interface IProps, P extends PropsOfType | tooltip?: string; required?: boolean; onUsersChanging?: (users: User[]) => User[]; + setComponentRef?: boolean; updateField: (update: (data: E) => void, callback?: () => any) => void; } -const LiveUserPicker = , P extends PropsOfType | PropsOfType>(props: IProps) => { +const LiveUserPicker = < + E extends ListItemEntity, + P extends PropsOfType | PropsOfType +>( + props: IProps +) => { const { entity, propertyName, @@ -34,36 +43,85 @@ const LiveUserPicker = , P extends PropsOfType users, - updateField + onUsersChanging = (users) => users, + updateField, + setComponentRef, } = props; - const value = getCurrentValue(entity, propertyName) as (User | User[]); - const updateValue = useCallback((val: LiveType) => updateField(e => setValue(e, propertyName, val)), [updateField, propertyName]); - const renderValue = useCallback((val: LiveType) => Array.isArray(val) ? (val as User[]).map((v, idx) => {idx > 0 ? '; ' : ''}{v.title}) : (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)); - }, [onUsersChanging, updateField, propertyName, value]); + const value = getCurrentValue(entity, propertyName) as User | User[]; + const updateValue = useCallback( + (val: LiveType) => + updateField((e) => setValue(e, propertyName, val)), + [updateField, propertyName] + ); + const renderValue = useCallback( + (val: LiveType) => + Array.isArray(val) + ? (val as User[]).map((v, idx) => ( + + {idx > 0 ? "; " : ""} + {v.title} + + )) + : (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] + ); return ( - - - {(renderLiveUpdateMark) => <> - {label && - - {renderLiveUpdateMark()} - } - - {!label && renderLiveUpdateMark()} - } + + + {(renderLiveUpdateMark) => ( + <> + {label && ( + + + + + {renderLiveUpdateMark()} + + )} + + {!label && renderLiveUpdateMark()} + + )} ); diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/LiveUtils.ts b/samples/react-rhythm-of-business-calendar/src/common/components/LiveUtils.ts index 5354fc04d..6e56c914f 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/LiveUtils.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/components/LiveUtils.ts @@ -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 = (obj: any): obj is IOneToManyRelationship => obj instanceof OneToManyRelationship @@ -100,4 +101,18 @@ export const setValue = , P extends keyof E>(entit } else { 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'] + } + }); }; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/TimePicker.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/TimePicker.tsx index 482e30d89..feaa70166 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/TimePicker.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/TimePicker.tsx @@ -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 = ({ 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]); diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/UserPicker.tsx b/samples/react-rhythm-of-business-calendar/src/common/components/UserPicker.tsx index 585999dbd..b49b0298c 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/UserPicker.tsx +++ b/samples/react-rhythm-of-business-calendar/src/common/components/UserPicker.tsx @@ -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 => { +const resolveSuggestions = async ( + searchText: string, + currentUserPersonas: IUserPersonaProps[], + directoryService: IDirectoryService, + onChangedFn: OnChangedCallback, + restrictToGroupMembers?: SharePointGroup, + restrictPrincipalType?: PrincipalType +): Promise => { 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 = ({ 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) => , + (props: IPeoplePickerItemSelectedProps) => ( + + ), [fixHighContrastPeoplePickerItemStyles] ); + const dropDownRef = useRef(); + 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,15 +232,21 @@ const UserPicker: FC = ({ }; return ( -
- {label && +
+ {label && ( - + - } + )} {renderPicker()}
); }; -export default UserPicker; \ No newline at end of file +export default UserPicker; diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/index.ts b/samples/react-rhythm-of-business-calendar/src/common/components/index.ts index 2e1437099..69dce25af 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/components/index.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/components/index.ts @@ -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'; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/common/components/styles/LiveTextField.module.scss b/samples/react-rhythm-of-business-calendar/src/common/components/styles/LiveTextField.module.scss new file mode 100644 index 000000000..351a94f84 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/common/components/styles/LiveTextField.module.scss @@ -0,0 +1,5 @@ +@import 'common.module'; + + .infoLabelStyle > label:first-child{ + display: inline !important; + } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/common/services/directory/DirectoryServiceDescriptor.ts b/samples/react-rhythm-of-business-calendar/src/common/services/directory/DirectoryServiceDescriptor.ts index 46c4131c0..032704761 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/services/directory/DirectoryServiceDescriptor.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/services/directory/DirectoryServiceDescriptor.ts @@ -27,6 +27,7 @@ export interface IDirectoryService extends IService { findGroupByTitle(title: string, web?: IWeb): Promise; persistGroup(group: SharePointGroup, web?: IWeb): Promise; changeGroupOwner(group: SharePointGroup, owner: SharePointGroup | User): Promise; + readonly userHasEditPermisison?: boolean; } export type DirectoryServiceProp = { diff --git a/samples/react-rhythm-of-business-calendar/src/common/services/directory/OnlineDirectoryService.ts b/samples/react-rhythm-of-business-calendar/src/common/services/directory/OnlineDirectoryService.ts index 8b96faf3d..c37eee14a 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/services/directory/OnlineDirectoryService.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/services/directory/OnlineDirectoryService.ts @@ -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>(); private readonly _searchCache = new Map>(); private readonly _ensureUserCache = new Map>(); - private readonly _roleDefinitionIdCache = new Map>(); + private readonly _roleDefinitionIdCache = new Map< + RoleType, + Promise + >(); 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 { - } + public async initialize(): Promise {} 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 => { + private readonly _resolveCore = async ( + input: string, + batch?: SPBatch + ): Promise => { 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 { + public search( + input: string, + principalType: PrincipalType = PrincipalType.All + ): Promise { 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 { + public ensureUsers( + users: User[], + batch?: SPBatch, + web?: IWeb + ): Promise { 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 { + public async ensureLogin( + users: readonly User[], + web?: IWeb + ): Promise { 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 => { + private _ensureLoginCore = async ( + user: User, + batch?: SPBatch + ): Promise => { 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 { 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 { 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 { @@ -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 { + public async findGroupByTitle( + title: string, + web?: IWeb + ): Promise { 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 { + private async _loadSiteGroup( + siteGroup: ISiteGroup, + web: IWeb + ): Promise { 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 { + public async persistGroup( + group: SharePointGroup, + web?: IWeb + ): Promise { 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 { - const rootId = '740c6a0b-85e2-48a0-a494-e0f1759d4aa7'; + public async changeGroupOwner( + group: SharePointGroup, + owner: SharePointGroup | User + ): Promise { + 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: - ` + body: ` @@ -265,12 +347,20 @@ export class OnlineDirectoryService implements IDirectoryService { - - + + - ` + `, }; - await this._spHttpClient.post(processQuery, SPHttpClient.configurations.v1, options); + await this._spHttpClient.post( + processQuery, + SPHttpClient.configurations.v1, + options + ); } -} \ No newline at end of file +} diff --git a/samples/react-rhythm-of-business-calendar/src/common/services/timezones/OnlineTimeZoneService.ts b/samples/react-rhythm-of-business-calendar/src/common/services/timezones/OnlineTimeZoneService.ts index eb0260f35..71c051e26 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/services/timezones/OnlineTimeZoneService.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/services/timezones/OnlineTimeZoneService.ts @@ -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) { this._dev = dev; this._currentWebUrl = context.pageContext.web.absoluteUrl; @@ -80,17 +96,23 @@ export class OnlineTimeZoneService implements ITimeZoneService { } public async initialize(): Promise { - 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 { - 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)}`); - } - } + }, + }, }; -} \ No newline at end of file +} diff --git a/samples/react-rhythm-of-business-calendar/src/common/services/timezones/TimeZoneServiceDescriptor.ts b/samples/react-rhythm-of-business-calendar/src/common/services/timezones/TimeZoneServiceDescriptor.ts index 33faf99f8..ea6084da9 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/services/timezones/TimeZoneServiceDescriptor.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/services/timezones/TimeZoneServiceDescriptor.ts @@ -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; } @@ -25,10 +26,15 @@ export type TimeZoneServiceProp = { [TimeZoneService]: ITimeZoneService; }; -export const useTimeZoneService = () => useServices()[TimeZoneService]; +export const useTimeZoneService = () => + useServices()[TimeZoneService]; -export const TimeZoneServiceDescriptor: IServiceDescriptor = { +export const TimeZoneServiceDescriptor: IServiceDescriptor< + typeof TimeZoneService, + ITimeZoneService, + TimeZoneServiceProp +> = { symbol: TimeZoneService, dependencies: [], - online: OnlineTimeZoneService -}; \ No newline at end of file + online: OnlineTimeZoneService, +}; diff --git a/samples/react-rhythm-of-business-calendar/src/common/sharepoint/SPField.ts b/samples/react-rhythm-of-business-calendar/src/common/sharepoint/SPField.ts index 5499fb934..6a85780f1 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/sharepoint/SPField.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/sharepoint/SPField.ts @@ -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 = (row: T, fieldName: PropsOfType, { momentId }: ITimeZone): Moment => { +export const fromDateTime = ( + row: T, + fieldName: PropsOfType, + { 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 = ( + row: T, + fieldName: PropsOfType, + { 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 = (row: T, fieldName: PropsOfType, defaultValue: boolean = false): boolean => { +export const fromYesNo = ( + row: T, + fieldName: PropsOfType, + 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 = (row: T, fieldName: PropsOfType & AllowedIntegerFieldNames): number => { +export const fromInteger = ( + row: T, + fieldName: PropsOfType & AllowedIntegerFieldNames +): number => { const value: string = (row as any)[fieldName]; return parseIntOrDefault(value, undefined, 10); }; -export const fromInt = (row: T, fieldName: PropsOfType, defaultValue: number = Number.NaN, radix: number = 10): number => { +export const fromInt = ( + row: T, + fieldName: PropsOfType, + defaultValue: number = Number.NaN, + radix: number = 10 +): number => { const value: string = (row as any)[`${String(fieldName)}.`]; return parseIntOrDefault(value, defaultValue, radix); }; -export const fromFloat = (row: T, fieldName: PropsOfType, defaultValue: number = Number.NaN): number => { +export const fromFloat = ( + row: T, + fieldName: PropsOfType, + defaultValue: number = Number.NaN +): number => { const value: string = (row as any)[`${String(fieldName)}.`]; return parseFloatOrDefault(value, defaultValue); }; -export const fromCurrency = (row: T, fieldName: PropsOfType, defaultValue: number = Number.NaN): number => { +export const fromCurrency = ( + row: T, + fieldName: PropsOfType, + defaultValue: number = Number.NaN +): number => { const value: string = (row as any)[`${String(fieldName)}.`]; return parseFloatOrDefault(value, defaultValue); }; -export const fromGuid = (row: T, fieldName: PropsOfType): Guid => { +export const fromGuid = ( + row: T, + fieldName: PropsOfType +): Guid => { const value: string = (row as any)[fieldName]; return Guid.tryParse(value); }; -export const fromRecurrence = (row: T, fieldName: PropsOfType & "fRecurrence"): boolean => { +export const fromRecurrence = ( + row: T, + fieldName: PropsOfType & "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 = >(entities: ReadonlyArray): Update_LookupIdMulti => { - return new UpdateMultiLookup(entities.map(e => e.id)); +export const toLookupMulti = >( + entities: ReadonlyArray +): Update_LookupIdMulti => { + return new UpdateMultiLookup(entities.map((e) => e.id)); +}; + +export const toChoiceMulti = >( + entities: ReadonlyArray +): 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 = (value: Query_Lookup, lookup: ReadonlyMap) => { +export const fromLookup = ( + value: Query_Lookup, + lookup: ReadonlyMap +) => { return lookupHasValue(value) ? lookup.get(first(value).lookupId) : null; }; -export const fromLookupMulti = (values: Query_LookupMulti, lookup: ReadonlyMap) => { - return lookupHasValue(values) ? values.map(value => lookup.get(value.lookupId)) : []; +export const fromLookupMulti = ( + values: Query_LookupMulti, + lookup: ReadonlyMap +) => { + return lookupHasValue(values) + ? values.map((value) => lookup.get(value.lookupId)) + : []; }; -export const fromLookupAsync = async (value: Query_Lookup, lookup: (id: number) => T | Promise) => { +export const fromLookupAsync = async ( + value: Query_Lookup, + lookup: (id: number) => T | Promise +) => { return lookupHasValue(value) ? await lookup(value[0].lookupId) : null; }; -export const fromLookupMultiAsync = async (values: Query_LookupMulti, lookup: (id: number) => T | Promise) => { - return lookupHasValue(values) ? await Promise.all(values.map(value => lookup(value.lookupId))) : []; +export const fromLookupMultiAsync = async ( + values: Query_LookupMulti, + lookup: (id: number) => T | Promise +) => { + return lookupHasValue(values) + ? await Promise.all(values.map((value) => lookup(value.lookupId))) + : []; }; -export const toTaxonomy = >(term: T): Update_Taxonomy => { +export const toTaxonomy = >( + term: T +): Update_Taxonomy => { return term ? new UpdateTaxonomy(term.label, term.termId.toString()) : null; }; -export const toTaxonomyMulti = >(terms: readonly T[]): Update_TaxonomyMulti => { - return (terms || []).map(term => `-1;#${term.label}|${term.termId.toString()}`).join(';#'); +export const toTaxonomyMulti = >( + terms: readonly T[] +): Update_TaxonomyMulti => { + return (terms || []) + .map((term) => `-1;#${term.label}|${term.termId.toString()}`) + .join(";#"); }; -export const fromTaxonomy = >(value: Query_Taxonomy, lookup: ReadonlyMap) => { +export const fromTaxonomy = >( + value: Query_Taxonomy, + lookup: ReadonlyMap +) => { return lookup.get(value?.TermID || value?.TermGuid); }; -export const fromTaxonomyMulti = >(values: Query_TaxonomyMulti, lookup: ReadonlyMap) => { - return (values || []).map(value => lookup.get(value.TermID)).filter(Boolean); +export const fromTaxonomyMulti = >( + values: Query_TaxonomyMulti, + lookup: ReadonlyMap +) => { + return (values || []) + .map((value) => lookup.get(value.TermID)) + .filter(Boolean); }; -export const fromTaxonomyAsync = async >(value: Query_Taxonomy, lookup: (guid: string) => T | Promise) => { +export const fromTaxonomyAsync = async >( + value: Query_Taxonomy, + lookup: (guid: string) => T | Promise +) => { return lookup(value?.TermID); }; -export const fromTaxonomyMultiAsync = async >(values: Query_TaxonomyMulti, lookup: (guid: string) => T | Promise) => { - return Promise.all((values || []).map(value => lookup(value.TermID))); +export const fromTaxonomyMultiAsync = async < + T extends TaxonomyTermEntity +>( + values: Query_TaxonomyMulti, + lookup: (guid: string) => T | Promise +) => { + 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: >(field: ITaxonomyFieldDefinition, value: T) => { - return { FieldName: field.name, FieldValue: value ? `${value.label}|${value.termId.toString()};` : '' }; + SingleMMD: >( + field: ITaxonomyFieldDefinition, + value: T + ) => { + return { + FieldName: field.name, + FieldValue: value + ? `${value.label}|${value.termId.toString()};` + : "", + }; }, - MultiMMD: >(field: ITaxonomyFieldDefinition, value: T[]) => { - return { FieldName: field.name, FieldValue: value?.map(term => `${term.label}|${term.termId.toString()}`).join(';') || '' }; + MultiMMD: >( + 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 }; - } -}; \ No newline at end of file + return { FieldName: "FileLeafRef", FieldValue: value }; + }, +}; diff --git a/samples/react-rhythm-of-business-calendar/src/common/sharepoint/schema/ElementProvisioner.ts b/samples/react-rhythm-of-business-calendar/src/common/sharepoint/schema/ElementProvisioner.ts index eb63e15c1..1474dd28a 100644 --- a/samples/react-rhythm-of-business-calendar/src/common/sharepoint/schema/ElementProvisioner.ts +++ b/samples/react-rhythm-of-business-calendar/src/common/sharepoint/schema/ElementProvisioner.ts @@ -384,10 +384,20 @@ 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); diff --git a/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ChannelsPanel.tsx b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ChannelsPanel.tsx new file mode 100644 index 000000000..73f10375c --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ChannelsPanel.tsx @@ -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 { +// } + +// interface IOwnProps { +// sendDataToParent?: (data: boolean) => void; +// isTeamsMessageRequired?:boolean; +// } +// type IProps = IOwnProps & IEntityPanelProps & ServicesProp; + +// interface IOwnState { +// refinerValueOptionsByRefiner?: Map; +// refiners?: readonly Refiner[]; +// dataToSend?: boolean; +// } +// type IState = IOwnState & IDataPanelBaseState; + +// class ChannelsPanel extends EntityPanelBase 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(); +// 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 <> +// The steps to get the Teams Id and Channel Id are given below : +// +//
    +//
  • Click on the three dots near the channel name within Microsoft Teams.
  • +//
  • Select 'Get a link to the channel' and copy the link.
  • +//
  • Select the 'groupId' value from the copied link and paste it for the teams id field.
  • +//
  • Next copy the text starting after the 'channel/' from the copied link and select till the '.tacv2'.
  • +//
  • Now paste the link within the channel id field.
  • + +//
+//
+// ; +// } + +// protected renderDisplayContent(): JSX.Element { +// const entity = this.entity; +// const liveProps = { +// entity +// }; + +// return ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
+// +// +// {this.channelDescription()} +// +// +// +// +// +// +// +//
+//
+// ); +// } + +// protected renderEditContent(): JSX.Element { +// const { showValidationFeedback } = this.state; +// const entity = this.entity; +// const liveProps = { +// entity, +// showValidationFeedback, +// updateField: this.updateField, +// hideIcon:true +// }; + +// return ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
+// +// +// {this.channelDescription()} +// +// +// +// +// +// +// +//
+// ); +// } + +// 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); \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ConfigureChannelsPanel.tsx b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ConfigureChannelsPanel.tsx new file mode 100644 index 000000000..5f56cc848 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/ConfigureChannelsPanel.tsx @@ -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; +// } +// type IProps = IOwnProps & ServicesProp; + +// interface IState { +// hidden: boolean; +// channelsConfigurationsAsync: IAsyncData; +// refinersAsync: IAsyncData; +// isTeamsMessageRequired: boolean; +// } + +// class ConfigureChannelsPanel extends Component implements IConfigureChannelsPanel { +// private readonly _channelsPanel = createRef(); +// private readonly _selection: Selection; + +// constructor(props: IProps) { +// super(props); + +// const { +// [EventsService]: { channelsConfigurationsAsync, refinersAsync } +// } = this.props.services; + +// this.state = { +// hidden: true, +// channelsConfigurationsAsync, +// refinersAsync, +// isTeamsMessageRequired: false +// }; + +// this._selection = new Selection({ +// onSelectionChanged: () => this.setState({}), +// items: [] +// }); +// } + +// handleDataFromChild = (data:boolean) => { +// this.setState({ isTeamsMessageRequired: data }); +// }; + + +// public componentDidMount() { +// (this.props.componentRef as MutableRefObject).current = this; +// } + +// public componentWillUnmount(): void { +// (this.props.componentRef as MutableRefObject).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 { +// // 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 ( +// {channelsConfigurations => +// {refiners => <> +// +// +// +// {this._filteredAndSortdChannels.length === 0 && +// {strings.NoChannelsDefined} +// } +// { + +// } +// { this.state.isTeamsMessageRequired && +// {strings.Message_Teams} +// +// } +// +// +// } +// } +// ); +// } +// } + +// export default withServices(ConfigureChannelsPanel); \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/index.ts b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/index.ts new file mode 100644 index 000000000..faf303136 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/ChannelsConfiguration/index.ts @@ -0,0 +1,2 @@ +// export { default as ChannelsPanel, IChannelsPanel } from './ChannelsPanel'; +// export { default as ConfigureChannelsPanel, IConfigureChannelsPanel } from './ConfigureChannelsPanel'; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.module.scss b/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.module.scss new file mode 100644 index 000000000..0e8844b2c --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.module.scss @@ -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; + } + +} \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.tsx b/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.tsx new file mode 100644 index 000000000..d0faa2583 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/ProductivityStudioLogo.tsx @@ -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 = (props: IProductivityStudioLogoProps) => { + return ( +

+ + { Created by the } + + {strings.Command_ProductivityLogoLink} + + + Productivity Studio Logo + + +

+ + ); +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/approvals/ApprovalDialog.tsx b/samples/react-rhythm-of-business-calendar/src/components/approvals/ApprovalDialog.tsx index e1cfd3620..c33b18218 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/approvals/ApprovalDialog.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/approvals/ApprovalDialog.tsx @@ -32,7 +32,14 @@ class ApprovalDialog extends EntityDialogBase 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)) { diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.module.scss b/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.module.scss index 655d08c29..214f86b62 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.module.scss +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.module.scss @@ -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; } diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.tsx b/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.tsx index c1670c503..c33fdcf34 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventBar.tsx @@ -20,9 +20,12 @@ interface IProps { endsIn: boolean; timeStringOverride?: string; size?: EventBarSize; + selectedTemplateKeys?: string[]; + + type?: string; } -export const EventBar: FC = ({ event, startsIn, endsIn, timeStringOverride, size = EventBarSize.Compact }) => { +export const EventBar: FC = ({ 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 = ({ 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 ( - {tag && [{tag}]} + {selectedTemplateKeys && selectedTemplateKeys.includes('tag') && size === EventBarSize.Compact && tag && [{tag}]} + {/* {((size == EventBarSize.Compact && type=== "Quarter") || (size !== EventBarSize.Compact)) && tag && [{tag}]} */} + {(size !== EventBarSize.Compact) && tag && [{tag}]} - {size === EventBarSize.Compact && startTimeString && `${startTimeString}, `} - {title} + {selectedTemplateKeys && selectedTemplateKeys.includes('starttime') && size === EventBarSize.Compact && startTimeString && `${startTimeString} `} - {isConfidential && } - - {isRecurring && } + + + + + {title} + + {isConfidential && ( + + )} + {isRecurring && ( + + + )} + + + + {selectedTemplateKeys && selectedTemplateKeys.includes('location') && size === EventBarSize.Compact && + <> + {location || '-'} + } {size === EventBarSize.Large && <> diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventDetailsCallout.tsx b/samples/react-rhythm-of-business-calendar/src/components/events/EventDetailsCallout.tsx index f69856d58..a7baa2187 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventDetailsCallout.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventDetailsCallout.tsx @@ -13,6 +13,7 @@ export interface IEventDetailsCallout { interface IProps { componentRef: RefObject; commands: IEventCommands; + // channels: readonly ChannelsConfigurations[]; enable for share event } export const EventDetailsCallout: FC = ({ componentRef, commands }) => { @@ -50,6 +51,7 @@ export const EventDetailsCallout: FC = ({ componentRef, commands }) => { viewCommand, addToOutlookCommand, getLinkCommand + // shareCommand //uncomment to share functionality ] = useEventCommandActionButtons(commands, event); return (isOpen && @@ -67,6 +69,7 @@ export const EventDetailsCallout: FC = ({ componentRef, commands }) => { {viewCommand} {addToOutlookCommand} {getLinkCommand} + {/* {shareCommand} */} diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventFilter.tsx b/samples/react-rhythm-of-business-calendar/src/components/events/EventFilter.tsx index a652f198d..468dcb822 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventFilter.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventFilter.tsx @@ -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; approvers: readonly Approvers[]; + searchText: string; + viewType?: string; + exactMatch: boolean; + selectedItem: any; + siteTimeZone?: string; children: (cccurrences: readonly EventOccurrence[]) => ReactElement; + + } -export const EventFilter: FC = ({ events, dateRange, refiners, selectedRefinerValues, approvers, children }) => { +export const EventFilter: FC = ({ 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(Entity.NotDeletedFilter) + .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 { if (event.isApproved) { return true; @@ -38,7 +68,24 @@ export const EventFilter: FC = ({ 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 { const valuesByRefiner = occurrence.event.valuesByRefiner(); return !useRefiners || refiners.every(refiner => { diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.module.scss b/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.module.scss index 5a422f799..04c65110f 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.module.scss +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.module.scss @@ -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; } } -} \ No newline at end of file + .textDayView { + @include eventDayView-font(); + text-overflow: ellipsis; + display: block; + } + +} + diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.tsx b/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.tsx index 46b2020b3..cd89ad586 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventOverview.tsx @@ -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 = ({ event, className }) => { {isRecurring && - {event.getSeriesMaster().start.format('LT')} - {event.getSeriesMaster().end.format('LT')}, {humanizeRecurrencePattern(start, recurrence)} + { isAllDay ? `${_strings.AllDay}, ${humanizeRecurrencePattern(event.getSeriesMaster().start, recurrence)}` : `${event.getSeriesMaster().start.format('LT')} - ${event.getSeriesMaster().end.format('LT')}, ${humanizeRecurrencePattern(event.getSeriesMaster().start, recurrence)}`} } {location && diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/EventPanel.tsx b/samples/react-rhythm-of-business-calendar/src/components/events/EventPanel.tsx index d9b35d304..bb7c73ef1 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/EventPanel.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/events/EventPanel.tsx @@ -1,52 +1,138 @@ -import { PrincipalType } from '@pnp/sp'; -import { Guid } from '@microsoft/sp-core-library'; -import React from 'react'; -import { FocusZone, format, ICommandBarItemProps, IDropdownOption, Label, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react"; -import { Entity, ErrorHandler, humanizeDuration, mapToArray, now, User, ValidationRule } from 'common'; -import { EntityPanelBase, IEntityPanelProps, IDataPanelBaseState, ResponsiveGrid, GridRow, GridCol, LiveText, LiveUpdate, IDataPanelBase, LiveToggle, LiveUserPicker, LiveTextField, LiveTimePicker, LiveDatePicker, Validation, ITransformer, LiveMultiselectDropdown, LiveDropdown } from "common/components"; -import { Event, Refiner, RefinerValue, RecurPattern, EventModerationStatus, Approvers, humanizeRecurrencePattern } from "model"; -import { withServices, ServicesProp, EventsServiceProp, EventsService, ConfigurationServiceProp, ConfigurationService, DirectoryServiceProp, DirectoryService } from 'services'; -import { EventOverview } from '../events'; -import { RefinerValuePill } from '../refiners'; -import { ListItemTechnicals } from '../shared'; -import { PatternChoiceGroup, DailyEditor, WeeklyEditor, MonthlyEditor, YearlyEditor, UntilEditor } from '../recurrence'; -import { IEventCommands } from './IEventCommands'; +import { PrincipalType } from "@pnp/sp"; +import { Guid } from "@microsoft/sp-core-library"; +import React from "react"; +import { + FocusZone, + format, + ICommandBarItemProps, + IDropdownOption, + Label, + Link, + MessageBar, + MessageBarType, + Stack, + Text, +} from "@fluentui/react"; +import { + Entity, + ErrorHandler, + humanizeDuration, + mapToArray, + now, + User, + ValidationRule, +} from "common"; +import { + EntityPanelBase, + IEntityPanelProps, + IDataPanelBaseState, + ResponsiveGrid, + GridRow, + GridCol, + LiveText, + LiveUpdate, + IDataPanelBase, + LiveToggle, + LiveUserPicker, + LiveTextField, + LiveTimePicker, + LiveDatePicker, + Validation, + ITransformer, + LiveMultiselectDropdown, + LiveDropdown, + LiveMultiTextField, +} from "common/components"; +import { + Event, + Refiner, + RefinerValue, + RecurPattern, + EventModerationStatus, + Approvers, + humanizeRecurrencePattern, +} from "model"; +import { + withServices, + ServicesProp, + EventsServiceProp, + EventsService, + ConfigurationServiceProp, + ConfigurationService, + DirectoryServiceProp, + DirectoryService, + TimeZoneService, + TimeZoneServiceProp +} from "services"; +import { EventOverview } from "../events"; +import { RefinerValuePill } from "../refiners"; +import { ListItemTechnicals } from "../shared"; +import { + PatternChoiceGroup, + DailyEditor, + WeeklyEditor, + MonthlyEditor, + YearlyEditor, + UntilEditor, +} from "../recurrence"; +import { IEventCommands } from "./IEventCommands"; -import { PersistConcurrencyFailureMessage, Validation as validationStrings, EventPanel as strings } from "ComponentStrings"; +import { + PersistConcurrencyFailureMessage, + Validation as validationStrings, + EventPanel as strings, +} from "ComponentStrings"; -import styles from './EventPanel.module.scss'; +import styles from "./EventPanel.module.scss"; +import { renderSanitizedHTML } from "common/components/LiveUtils"; +import moment from "moment-timezone"; export class RefinerValueValidationRule extends ValidationRule { - constructor( - private _refiner: Refiner - ) { - super((e: Event) => this._isValid(e), validationStrings.Refiners.Required); + constructor(private _refiner: Refiner) { + super( + (e: Event) => this._isValid(e), + validationStrings.Refiners.Required + ); } private _isValid({ refinerValues }: Event): boolean { - return !this._refiner.required || refinerValues.get().some(v => v.refiner.get() === this._refiner); + return ( + !this._refiner.required || + refinerValues.get().some((v) => v.refiner.get() === this._refiner) + ); } } -export interface IEventPanel extends IDataPanelBase { -} +export interface IEventPanel extends IDataPanelBase {} interface IOwnProps { commands: IEventCommands; + timeZoneDiff?: any; } -type IProps = IOwnProps & IEntityPanelProps & ServicesProp; +type IProps = IOwnProps & + IEntityPanelProps & + ServicesProp< + DirectoryServiceProp & ConfigurationServiceProp & EventsServiceProp & TimeZoneServiceProp + >; interface IOwnState { refinerValueOptionsByRefiner: Map; refiners: readonly Refiner[]; + shiftFocus: boolean; } type IState = IOwnState & IDataPanelBaseState; -class EventPanel extends EntityPanelBase implements IEventPanel { - private readonly _refinerValueValidationRulesByRefiner = new Map(); +class EventPanel + extends EntityPanelBase + implements IEventPanel +{ + private readonly _refinerValueValidationRulesByRefiner = new Map< + Refiner, + RefinerValueValidationRule + >(); protected get title() { - return this.entity?.displayName || (this.isNew ? strings.NewEvent : ''); + return this.entity?.displayName || (this.isNew ? strings.NewEvent : ""); } protected resetState(): IState { @@ -56,13 +142,17 @@ class EventPanel extends EntityPanelBase implements IEven return { ...super.resetState(), refinerValueOptionsByRefiner: new Map(), - refiners: [] + refiners: [], + shiftFocus: false, }; } protected validate(): boolean { const rules = mapToArray(this._refinerValueValidationRulesByRefiner); - return super.validate() && rules.every(rule => rule.validate(this.entity)); + return ( + super.validate() && + rules.every((rule) => rule.validate(this.entity)) + ); } public componentShouldRender() { @@ -71,8 +161,14 @@ class EventPanel extends EntityPanelBase implements IEven this._buildRefinerValueValidationRules(); } + public shiftFocusToNextEle(shoudShift: boolean) { + this.setState({ shiftFocus: shoudShift }); + } + private async _buildRefinerValueOptions() { - const { [EventsService]: { refinersAsync } } = this.props.services; + const { + [EventsService]: { refinersAsync }, + } = this.props.services; await refinersAsync.promise; @@ -84,7 +180,10 @@ class EventPanel extends EntityPanelBase implements IEven return { key, text, data: value } as IDropdownOption; }; - const refinerValueOptionsByRefiner = new Map(); + const refinerValueOptionsByRefiner = new Map< + Refiner, + IDropdownOption[] + >(); for (const refiner of refiners) { const { required, allowMultiselect, blankValue } = refiner; const options: IDropdownOption[] = []; @@ -93,7 +192,11 @@ class EventPanel extends EntityPanelBase implements IEven options.push(refinerValueToDropdownOption(blankValue)); } - options.push(...refiner.values.filter(Entity.NotDeletedFilter).map(refinerValueToDropdownOption)); + options.push( + ...refiner.values + .filter(Entity.NotDeletedFilter) + .map(refinerValueToDropdownOption) + ); refinerValueOptionsByRefiner.set(refiner, options); } @@ -102,7 +205,9 @@ class EventPanel extends EntityPanelBase implements IEven } private async _buildRefinerValueValidationRules() { - const { [EventsService]: { refinersAsync } } = this.props.services; + const { + [EventsService]: { refinersAsync }, + } = this.props.services; await refinersAsync.promise; @@ -119,32 +224,51 @@ class EventPanel extends EntityPanelBase implements IEven protected async persistChangesCore() { const { [DirectoryService]: { currentUserIsSiteAdmin, currentUser }, - [ConfigurationService]: { active: { useApprovals } }, - [EventsService]: events + [ConfigurationService]: { + active: { useApprovals }, + }, + [EventsService]: events, + [TimeZoneService]:{siteTimeZone} } = this.props.services; - const { isApproved, isRejected, isDeleted, isConfidential } = this.entity; + const { isApproved, isRejected, isDeleted, isConfidential } = + this.entity; - const userCanApprove = currentUserIsSiteAdmin || this._currentUserIsAnApprover(); + const userCanApprove = + currentUserIsSiteAdmin || this._currentUserIsAnApprover(); try { if (isRejected && !userCanApprove) { this.entity.moderationStatus = EventModerationStatus.Pending; - } - else if (!isApproved && (!useApprovals || userCanApprove)) { + } else if (!isApproved && (!useApprovals || userCanApprove)) { this.entity.moderationStatus = EventModerationStatus.Approved; this.entity.moderator = currentUser; this.entity.moderationTimestamp = now(); } if (this.entity.hasRecurrenceChanges() && !isDeleted) { - this.entity.exceptions.forEach(e => e.delete()); + this.entity.exceptions.forEach((e) => e.delete()); this.entity.recurrenceUID = Guid.newGuid(); } if (!isConfidential) { this.entity.restrictedToAccounts = []; } + console.log(siteTimeZone); + const originalMomentStart = moment(this.entity.start); + const originalMomentEnd = moment(this.entity.end); + + //Converting event time to site timezone from local time + const convertedMoment = originalMomentStart && originalMomentStart.clone().tz(siteTimeZone.momentId,true); + const convertedMomentend = originalMomentEnd && originalMomentEnd.clone().tz(siteTimeZone.momentId,true); + + this.entity.start = convertedMoment; + this.entity.end = convertedMomentend; + if (this.entity.recurrence && this.entity.recurrence.until && this.entity.recurrence.until.date) { + const originalRecurrance = moment(this.entity.recurrence.until.date); + const convertedRecurrance = originalRecurrance && originalRecurrance.clone().tz(siteTimeZone.momentId,true); + this.entity.recurrence.until.date = convertedRecurrance; + } events.track(this.entity); await events.persist(); } catch (e) { @@ -161,165 +285,370 @@ class EventPanel extends EntityPanelBase implements IEven private _renderModerationStatus() { const { [DirectoryService]: { currentUserIsSiteAdmin, currentUser }, - [ConfigurationService]: { active: { useApprovals } } + [ConfigurationService]: { + active: { useApprovals }, + }, } = this.props.services; - const { creator, isApproved, isPendingApproval, isRejected, moderator, moderationMessage, moderationTimestamp } = this.entity + const { + creator, + isApproved, + isPendingApproval, + isRejected, + moderator, + moderationMessage, + moderationTimestamp, + } = this.entity; if (!useApprovals) return <>; - const userCanApprove = currentUserIsSiteAdmin || this._currentUserIsAnApprover(); + const userCanApprove = + currentUserIsSiteAdmin || this._currentUserIsAnApprover(); const userIsCreator = this.isNew || User.equal(creator, currentUser); - return <> - {this.inDisplayMode && isPendingApproval && <> - - {strings.Moderation.EventIsPendingApproval} - - } - {this.inEditMode && isPendingApproval && !userCanApprove && <> - - {strings.Moderation.EventWillNeedApproval} - - } - {this.inEditMode && isPendingApproval && userCanApprove && <> - - {strings.Moderation.EventWillBeAutoApproved} - - } - {isApproved && (userIsCreator || userCanApprove) && <> - - {format(strings.Moderation.EventIsApproved, moderator.title, moderationTimestamp.format('LLL'))} - {moderationMessage && <> - - {moderationMessage} - } - - } - {isRejected && (userIsCreator || userCanApprove) && <> - - {format(strings.Moderation.EventIsRejected, moderator.title, moderationTimestamp.format('LLL'))} - {moderationMessage && <> - - {moderationMessage} - } - - } - ; + return ( + <> + {this.inDisplayMode && isPendingApproval && ( + <> + + {strings.Moderation.EventIsPendingApproval} + + + )} + {this.inEditMode && isPendingApproval && !userCanApprove && ( + <> + + {strings.Moderation.EventWillNeedApproval} + + + )} + {this.inEditMode && isPendingApproval && userCanApprove && ( + <> + + {strings.Moderation.EventWillBeAutoApproved} + + + )} + {isApproved && (userIsCreator || userCanApprove) && ( + <> + + {format( + strings.Moderation.EventIsApproved, + moderator.title, + moderationTimestamp.format("LLL") + )} + {moderationMessage && ( + <> + + {moderationMessage} + + )} + + + )} + {isRejected && (userIsCreator || userCanApprove) && ( + <> + + {format( + strings.Moderation.EventIsRejected, + moderator.title, + moderationTimestamp.format("LLL") + )} + {moderationMessage && ( + <> + + {moderationMessage} + + )} + + + )} + + ); } protected renderDisplayContent(): JSX.Element { - const { [ConfigurationService]: { active: config } } = this.props.services; + const { + [ConfigurationService]: { active: config }, + } = this.props.services; const { refiners } = this.state; const event = this.entity; const liveProps = { - entity: event + entity: event, }; - const { isAllDay, start, isConfidential, isRecurring, isSeriesMaster, isSeriesException, seriesMaster } = event; - const isConfidentialPrevious = event.hasPrevious && event.previousValue('isConfidential'); - const isConfidentialSnapshot = event.hasSnapshot && event.snapshotValue('isConfidential'); - const confidentialFieldEnabled = (isConfidential || isConfidentialSnapshot || isConfidentialPrevious || config.allowConfidentialEvents); + const { + isAllDay, + isConfidential, + isRecurring, + isSeriesMaster, + isSeriesException, + seriesMaster, + } = event; + const isConfidentialPrevious = + event.hasPrevious && event.previousValue("isConfidential"); + const isConfidentialSnapshot = + event.hasSnapshot && event.snapshotValue("isConfidential"); + const confidentialFieldEnabled = + isConfidential || + isConfidentialSnapshot || + isConfidentialPrevious || + config.allowConfidentialEvents; return ( - - {val => {val || "-"}} + + {(val) => ( + {val || "-"} + )} - {!isSeriesMaster && + {!isSeriesMaster && ( - - {start => {start.format('dddd, MMMM DD, YYYY')}} + + {(start) => ( + + {start.format( + "dddd, MMMM DD, YYYY" + )} + + )} - } + )} - + {(start, state) => { let isAllDay = false; switch (state) { - case 'current': isAllDay = this.entity.currentValue('isAllDay'); break; - case 'snapshot': isAllDay = this.entity.snapshotValue('isAllDay'); break; - case 'previous': isAllDay = this.entity.previousValue('isAllDay'); break; + case "current": + isAllDay = + this.entity.currentValue( + "isAllDay" + ); + break; + case "snapshot": + isAllDay = + this.entity.snapshotValue( + "isAllDay" + ); + break; + case "previous": + isAllDay = + this.entity.previousValue( + "isAllDay" + ); + break; } - return {isAllDay ? strings.AllDay : start.format('LT')}; + return ( + + {isAllDay + ? strings.AllDay + : start.format("LT")} + + ); }} - {!isSeriesMaster && + {!isSeriesMaster && ( - - {end => {end.format('dddd, MMMM DD, YYYY')}} + + {(end) => ( + + {end.format("dddd, MMMM DD, YYYY")} + + )} - } + )} - {!isAllDay && - + {!isAllDay && ( + {(end, state) => { let isAllDay = false; switch (state) { - case 'current': isAllDay = this.entity.currentValue('isAllDay'); break; - case 'snapshot': isAllDay = this.entity.snapshotValue('isAllDay'); break; - case 'previous': isAllDay = this.entity.previousValue('isAllDay'); break; + case "current": + isAllDay = + this.entity.currentValue( + "isAllDay" + ); + break; + case "snapshot": + isAllDay = + this.entity.snapshotValue( + "isAllDay" + ); + break; + case "previous": + isAllDay = + this.entity.previousValue( + "isAllDay" + ); + break; } - return {isAllDay ? strings.AllDay : end.format('LT')}; + return ( + + {isAllDay + ? strings.AllDay + : end.format("LT")} + + ); }} - } + )} - {isRecurring && + {isRecurring && ( - - {recurrence => {humanizeRecurrencePattern(start, recurrence)}} + + {(recurrence) => ( + + {humanizeRecurrencePattern( + event.getSeriesMaster().start, + recurrence + )} + + )} - } + )} - - {val => {val || "-"}} + + {(val) => ( + {val || "-"} + )} - - {val => {val || "-"}} + + {(val) => ( +

+ )} - - {val => {val.map(({ title }) => title).join(', ') || "-"}} + + {(val) => ( + + {val + .map(({ title }) => title) + .join(", ") || "-"} + + )} - {refiners.map(refiner => { + {refiners.map((refiner) => { const transformer = { - transform: (values: RefinerValue[]) => values.filter(v => v.refiner.get() === refiner), - reverse: (values: RefinerValue[]) => values + transform: (values: RefinerValue[]) => + values.filter( + (v) => v.refiner.get() === refiner + ), + reverse: (values: RefinerValue[]) => values, }; return ( - - - {val => - - {val.map(refinerValue => - - )} + + + {(val) => ( + + {val.map((refinerValue) => ( + + ))} - } + )} ); @@ -330,25 +659,59 @@ class EventPanel extends EntityPanelBase implements IEven {this._renderModerationStatus()} - {confidentialFieldEnabled && + {confidentialFieldEnabled && ( - - {val => {val ? strings.Field_Confidential.OnText : strings.Field_Confidential.OffText}} + + {(val) => ( + + {val + ? strings.Field_Confidential + .OnText + : strings.Field_Confidential + .OffText} + + )} - {isConfidential && - - {val => {val.map(({ title }) => title).join(', ') || "-"}} + {isConfidential && ( + + {(val) => ( + + {val + .map(({ title }) => title) + .join(", ") || "-"} + + )} - } + )} - } + )} - + @@ -357,17 +720,39 @@ class EventPanel extends EntityPanelBase implements IEven } protected renderEditContent(): JSX.Element { - const { [ConfigurationService]: { active: config } } = this.props.services; - const { refiners, refinerValueOptionsByRefiner, showValidationFeedback } = this.state; + const { + [ConfigurationService]: { active: config }, + [TimeZoneService]:{siteTimeZone} + } = this.props.services; + const { + refiners, + refinerValueOptionsByRefiner, + showValidationFeedback, + } = this.state; const event = this.entity; - const { isAllDay, start, isConfidential, isRecurring, isSeriesException, recurrence, recurrenceExceptionInstanceDate } = event; - const isConfidentialPrevious = event.hasPrevious && event.previousValue('isConfidential'); - const isConfidentialSnapshot = event.hasSnapshot && event.snapshotValue('isConfidential'); - const confidentialFieldEnabled = (isConfidential || isConfidentialSnapshot || isConfidentialPrevious || config.allowConfidentialEvents); + const { + isAllDay, + start, + isConfidential, + isRecurring, + isSeriesException, + recurrence, + recurrenceExceptionInstanceDate, + } = event; + const isConfidentialPrevious = + event.hasPrevious && event.previousValue("isConfidential"); + const isConfidentialSnapshot = + event.hasSnapshot && event.snapshotValue("isConfidential"); + const confidentialFieldEnabled = + isConfidential || + isConfidentialSnapshot || + isConfidentialPrevious || + config.allowConfidentialEvents; const liveProps = { entity: event, showValidationFeedback, - updateField: this.updateField + updateField: this.updateField, + siteTimeZoneId:siteTimeZone.momentId, }; return ( @@ -377,7 +762,7 @@ class EventPanel extends EntityPanelBase implements IEven @@ -387,23 +772,25 @@ class EventPanel extends EntityPanelBase implements IEven - {(!isRecurring || isSeriesException) && + {(!isRecurring || isSeriesException) && ( - } + )} @@ -412,55 +799,72 @@ class EventPanel extends EntityPanelBase implements IEven - {(!isRecurring || isSeriesException) && + {(!isRecurring || isSeriesException) && ( - } + )} - {!isAllDay && <> - - {humanizeDuration(this.entity.duration)} - } + {!isAllDay && ( + <> + + + {humanizeDuration( + this.entity.duration + )} + + + )} - {isSeriesException && + {isSeriesException && ( - {format(strings.ThisInstanceOccursOn, recurrenceExceptionInstanceDate.format('LL'))} + + {format( + strings.ThisInstanceOccursOn, + recurrenceExceptionInstanceDate.format("LL") + )} +
- {humanizeRecurrencePattern(start, recurrence)} + + {humanizeRecurrencePattern(start, recurrence)} +
- } - {!isSeriesException && + )} + {!isSeriesException && ( implements IEven label={strings.Field_Recurring.Label} onText={strings.Field_Recurring.OnText} offText={strings.Field_Recurring.OffText} - propertyName='isRecurring' + propertyName="isRecurring" /> - {isRecurring && - humanizeRecurrencePattern(start, recur)} updateValue={recurrence => this.updateField(e => e.recurrence = recurrence)}>{renderLiveUpdateMark => { - const { pattern } = recurrence; - return ( - - {renderLiveUpdateMark()} - this.updateField(e => e.recurrence.pattern = parseInt(opt.key))} - /> - {pattern === RecurPattern.daily && - - } - {pattern === RecurPattern.weekly && - - } - {pattern === RecurPattern.monthly && - - } - {pattern === RecurPattern.yearly && - - } - - - - ); - }} + {isRecurring && ( + + humanizeRecurrencePattern(start, recur) + } + updateValue={(recurrence) => + this.updateField( + (e) => (e.recurrence = recurrence) + ) + } + > + {(renderLiveUpdateMark) => { + const { pattern } = recurrence; + return ( + + {renderLiveUpdateMark()} + + this.updateField( + (e) => + (e.recurrence.pattern = + parseInt( + opt.key + )) + ) + } + /> + {pattern === + RecurPattern.daily && ( + + )} + {pattern === + RecurPattern.weekly && ( + + )} + {pattern === + RecurPattern.monthly && ( + + )} + {pattern === + RecurPattern.yearly && ( + + )} + + + + ); + }} - } + )} - {this.entity.hasRecurrenceChanges() && - {strings.Recurrence.UpdateWarning} - } + {this.entity.hasRecurrenceChanges() && ( + + {strings.Recurrence.UpdateWarning} + + )} - } + )} - @@ -541,97 +992,161 @@ class EventPanel extends EntityPanelBase implements IEven - {refiners.filter(Entity.NotDeletedFilter).map(refiner => { - const { displayName, required, allowMultiselect } = refiner; - const rules = [this._refinerValueValidationRulesByRefiner.get(refiner)]; + {refiners.filter(Entity.NotDeletedFilter).map((refiner) => { + const { displayName, required, allowMultiselect } = + refiner; + const rules = [ + this._refinerValueValidationRulesByRefiner.get( + refiner + ), + ]; const transformer: ITransformer = { transform: (values: RefinerValue[]) => { - return values.filter(v => v.refiner.get() === refiner); + return values.filter( + (v) => v.refiner.get() === refiner + ); }, - reverse: (values: RefinerValue | RefinerValue[]) => { + reverse: ( + values: RefinerValue | RefinerValue[] + ) => { return [ - ...event.refinerValues.filter(v => v.refiner.get() !== refiner), - ...(values instanceof Array ? values : [values]).filter(v => v !== refiner.blankValue) + ...event.refinerValues.filter( + (v) => v.refiner.get() !== refiner + ), + ...(values instanceof Array + ? values + : [values] + ).filter((v) => v !== refiner.blankValue), ].filter(Boolean); - } + }, }; const livePropsForRefinerDropdowns = { entity: event, showValidationFeedback, updateField: (update: (event: Event) => void) => { - this.updateField(event => { + this.updateField((event) => { update(event); - event.moderationStatus = EventModerationStatus.Pending; + event.moderationStatus = + EventModerationStatus.Pending; }); - } - } + }, + }; return ( - - {allowMultiselect - ? + {allowMultiselect ? ( + val.key} - renderValue={vals => - - {vals.map(v => )} + options={refinerValueOptionsByRefiner.get( + refiner + )} + getKeyFromValue={(val) => val.key} + renderValue={(vals) => ( + + {vals.map((v) => ( + + ))} + )} + /> + ) : ( + + val?.key || 0 + } + renderValue={(val) => + val && + val.length > 0 && ( + + ) } /> - : val?.key || 0} - renderValue={val => val && val.length > 0 && } - /> - } + )} ); })} - - {this._renderModerationStatus()} - + {this._renderModerationStatus()} {confidentialFieldEnabled && - (isSeriesException - ? + (isSeriesException ? ( + - - {val => val ? strings.Field_Confidential.OnText : strings.Field_Confidential.OffText} + + {(val) => + val + ? strings.Field_Confidential.OnText + : strings.Field_Confidential.OffText + } - {isConfidential && - - {val => val.map(({ title }) => title).join(', ') || "-"} + {isConfidential && ( + + {(val) => + val + .map(({ title }) => title) + .join(", ") || "-" + } - } + )} - : + ) : ( + implements IEven onText={strings.Field_Confidential.OnText} offText={strings.Field_Confidential.OffText} tooltip={strings.Field_Confidential.Tooltip} - propertyName='isConfidential' + propertyName="isConfidential" /> - {isConfidential && + {isConfidential && ( - } + )} - )} + ))} @@ -668,19 +1187,37 @@ class EventPanel extends EntityPanelBase implements IEven protected renderEditHeader(): JSX.Element { const event = this.entity; - const { isSeriesException, recurrenceExceptionInstanceDate, seriesMaster } = event; - const onEditSeries = () => { this.edit(seriesMaster.get()); }; + const { + isSeriesException, + recurrenceExceptionInstanceDate, + seriesMaster, + } = event; + const onEditSeries = () => { + this.edit(seriesMaster.get()); + }; - return <> - - {isSeriesException && - - {format(strings.Recurrence.EditingInstanceWarning, recurrenceExceptionInstanceDate.format('LL'))}. -   - {strings.Recurrence.Command_EditSeries.Text} {strings.Recurrence.EditSeriesButtonExplanation} - - } - ; + return ( + <> + + {isSeriesException && ( + + {format( + strings.Recurrence.EditingInstanceWarning, + recurrenceExceptionInstanceDate.format("LL") + )} + .   + + {strings.Recurrence.Command_EditSeries.Text} + {" "} + {strings.Recurrence.EditSeriesButtonExplanation} + + )} + + ); } protected markEntityDeleted(): void { @@ -688,8 +1225,7 @@ class EventPanel extends EntityPanelBase implements IEven if (this.entity.isSeriesException) this.entity.recurrenceInstanceCancelled = true; - else - this.entity.delete(); + else this.entity.delete(); } private _currentUserIsAnApprover(): boolean { @@ -698,204 +1234,285 @@ class EventPanel extends EntityPanelBase implements IEven [EventsService]: { approversAsync }, } = this.props.services; - const currentUserApprovers = approversAsync.data?.filter(a => a.userIsAnApprover(currentUser)) || []; + const currentUserApprovers = + approversAsync.data?.filter((a) => + a.userIsAnApprover(currentUser) + ) || []; - return Approvers.appliesToAny(currentUserApprovers, this.entity.valuesByRefiner()); + return Approvers.appliesToAny( + currentUserApprovers, + this.entity.valuesByRefiner() + ); } protected buildDisplayHeaderCommands(): ICommandBarItemProps[] { const { - commands: { approve, reject, addToOutlook, addSeriesToOutlook, getLink }, - services: { [DirectoryService]: { currentUserIsSiteAdmin, currentUser } } + commands: { + approve, + reject, + addToOutlook, + addSeriesToOutlook, + getLink, + }, + services: { + [DirectoryService]: { currentUserIsSiteAdmin, currentUser }, + }, } = this.props; - const { isRecurring, isSeriesException, isSeriesMaster, seriesMaster, isDeleted, isNew, isApproved, creator } = this.entity; - const onEdit = () => { this.edit(); }; - const onEditSeries = () => { this.edit(seriesMaster.get(), false); }; - const onDelete = () => { this.confirmDelete(); }; + const { + isRecurring, + isSeriesException, + isSeriesMaster, + seriesMaster, + isDeleted, + isNew, + isApproved, + creator, + } = this.entity; + const onEdit = () => { + this.edit(); + }; + const onEditSeries = () => { + this.edit(seriesMaster.get(), false); + }; + const onDelete = () => { + this.confirmDelete(); + }; const onDeleteSeries = () => { this.edit(seriesMaster.get(), false); this.confirmDelete(); }; - const onApprove = () => { approve(this.entity); }; - const onReject = () => { reject(this.entity); }; - const onAddToOutlook = () => { addToOutlook(this.entity); }; - const onAddSeriesToOutlook = () => { addSeriesToOutlook(this.entity); }; - const onGetLink = () => { getLink(this.entity); }; + const onApprove = () => { + if (isRecurring && isSeriesException) { + approve(seriesMaster.get()); + //this.entity.moderationStatus = EventModerationStatus.Approved; + } + else { + approve(this.entity); + } + }; + const onReject = () => { + if (isRecurring && isSeriesException) { + reject(seriesMaster.get()); + } + else { + reject(this.entity); + } + }; + const onAddToOutlook = () => { + addToOutlook(this.entity, this.props.timeZoneDiff); + }; + const onAddSeriesToOutlook = () => { + addSeriesToOutlook(this.entity); + }; + const onGetLink = () => { + getLink(this.entity); + }; const editSingleCommand: ICommandBarItemProps = { - key: 'edit', + key: "edit", text: strings.Command_Edit.Text, - iconProps: { iconName: 'Edit' }, + iconProps: { iconName: "Edit" }, disabled: isDeleted, - onClick: onEdit + onClick: onEdit, }; const editSeriesCommand: ICommandBarItemProps = { - key: 'edit', + key: "edit", text: "Edit series", - iconProps: { iconName: 'Edit' }, + iconProps: { iconName: "Edit" }, disabled: isDeleted, - onClick: onEdit + onClick: onEdit, }; const editRecurringCommand: ICommandBarItemProps = { - key: 'edit', + key: "edit", text: strings.Command_Edit.Text, - iconProps: { iconName: 'Edit' }, + iconProps: { iconName: "Edit" }, disabled: isDeleted, subMenuProps: { - items: [{ - key: 'edit-series', - text: strings.Command_Edit_Recurring_Series.Text, - onClick: onEditSeries - }, { - key: 'edit-occurrence', - text: strings.Command_Edit_Recurring_Instance.Text, - onClick: onEdit - }] - } + items: [ + { + key: "edit-series", + text: strings.Command_Edit_Recurring_Series.Text, + onClick: onEditSeries, + }, + { + key: "edit-occurrence", + text: strings.Command_Edit_Recurring_Instance.Text, + onClick: onEdit, + }, + ], + }, }; const moderationCommand: ICommandBarItemProps = { - key: 'moderation', + key: "moderation", text: strings.Command_Approval.Text, - iconProps: { iconName: 'EventAccepted' }, + iconProps: { iconName: "EventAccepted" }, disabled: isDeleted, subMenuProps: { - items: [{ - key: 'approve', - iconProps: { iconName: 'Accept' }, - text: strings.Command_Approval_Approve.Text, - onClick: onApprove - }, { - key: 'decline', - iconProps: { iconName: 'Clear' }, - text: strings.Command_Approval_Reject.Text, - onClick: onReject - }] - } + items: [ + { + key: "approve", + iconProps: { iconName: "Accept" }, + text: strings.Command_Approval_Approve.Text, + onClick: onApprove, + }, + { + key: "decline", + iconProps: { iconName: "Clear" }, + text: strings.Command_Approval_Reject.Text, + onClick: onReject, + }, + ], + }, }; const deleteSingleCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, - onClick: onDelete + onClick: onDelete, }; const deleteSeriesMasterCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete_Series.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, - onClick: onDelete + onClick: onDelete, }; const deleteRecurringCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, subMenuProps: { - items: [{ - key: 'delete-series', - text: strings.Command_Delete_Recurring_Series.Text, - onClick: onDeleteSeries - }, { - key: 'delete-occurrence', - text: strings.Command_Delete_Recurring_Instance.Text, - onClick: onDelete - }] - } + items: [ + { + key: "delete-series", + text: strings.Command_Delete_Recurring_Series.Text, + onClick: onDeleteSeries, + }, + { + key: "delete-occurrence", + text: strings.Command_Delete_Recurring_Instance.Text, + onClick: onDelete, + }, + ], + }, }; const addToOutlookSingleCommand: ICommandBarItemProps = { - key: 'add-to-outlook', + key: "add-to-outlook", text: strings.Command_AddToOutlook.Text, - iconProps: { iconName: 'AddEvent' }, + iconProps: { iconName: "AddEvent" }, disabled: isDeleted, - onClick: onAddToOutlook + onClick: onAddToOutlook, }; const addToOutlookSeriesCommand: ICommandBarItemProps = { - key: 'add-to-outlook', + key: "add-to-outlook", text: strings.Command_AddToOutlook.Text, - iconProps: { iconName: 'AddEvent' }, + iconProps: { iconName: "AddEvent" }, disabled: isDeleted, - onClick: onAddSeriesToOutlook + onClick: onAddSeriesToOutlook, }; const addToOutlookRecurringCommand: ICommandBarItemProps = { - key: 'add-to-outlook', + key: "add-to-outlook", text: strings.Command_AddToOutlook.Text, - iconProps: { iconName: 'AddEvent' }, + iconProps: { iconName: "AddEvent" }, disabled: isDeleted, subMenuProps: { - items: [{ - key: 'add-to-outlook-series', - text: strings.Command_AddToOutlook_Recurring_Series.Text, - onClick: onAddSeriesToOutlook - }, { - key: 'add-to-outlook-occurrence', - text: strings.Command_AddToOutlook_Recurring_Instance.Text, - onClick: onAddToOutlook - }] - } + items: [ + { + key: "add-to-outlook-series", + text: strings.Command_AddToOutlook_Recurring_Series + .Text, + onClick: onAddSeriesToOutlook, + }, + { + key: "add-to-outlook-occurrence", + text: strings.Command_AddToOutlook_Recurring_Instance + .Text, + onClick: onAddToOutlook, + }, + ], + }, }; const getLinkCommand: ICommandBarItemProps = { - key: 'get-link', + key: "get-link", text: strings.Command_GetLink.Text, - iconProps: { iconName: 'Link' }, + iconProps: { iconName: "Link" }, disabled: isDeleted, - onClick: onGetLink + onClick: onGetLink, }; - const userCanApprove = currentUserIsSiteAdmin || this._currentUserIsAnApprover(); + const { + [ConfigurationService]: { active: config }, + } = this.props.services; + + + + const userCanApprove = + currentUserIsSiteAdmin || this._currentUserIsAnApprover(); const userIsCreator = User.equal(creator, currentUser); const canEdit = userIsCreator || userCanApprove; const canModerate = !isApproved && userCanApprove; const canDelete = (!isNew || isSeriesException) && canEdit; const canAddToOutlook = (!isNew || isSeriesException) && isApproved; + const enableAddToOutlook = config && config.useAddToOutlook; return [ - canEdit && ( - isRecurring - ? (isSeriesMaster + canEdit && + (isRecurring + ? isSeriesMaster ? editSeriesCommand : editRecurringCommand - ) - : editSingleCommand - ), + : editSingleCommand), canModerate && moderationCommand, - canDelete && ( - isRecurring - ? (isSeriesMaster + canDelete && + (isRecurring + ? isSeriesMaster ? deleteSeriesMasterCommand : deleteRecurringCommand - ) - : deleteSingleCommand - ), - canAddToOutlook && ( - isRecurring - ? (isSeriesMaster + : deleteSingleCommand), + enableAddToOutlook && canAddToOutlook && + (isRecurring + ? isSeriesMaster ? addToOutlookSeriesCommand : addToOutlookRecurringCommand - ) - : addToOutlookSingleCommand - ), - getLinkCommand + : addToOutlookSingleCommand), + getLinkCommand, ].filter(Boolean); } protected buildEditHeaderCommands(): ICommandBarItemProps[] { - const { [DirectoryService]: { currentUserIsSiteAdmin, currentUser } } = this.props.services; + const { + [DirectoryService]: { currentUserIsSiteAdmin, currentUser }, + } = this.props.services; const { submitting } = this.state; - const { isRecurring, isSeriesException, isSeriesMaster, seriesMaster, isDeleted, isNew, creator } = this.entity; - const onSubmit = () => this.submit(() => { this.display(); }); + const { + isRecurring, + isSeriesException, + isSeriesMaster, + seriesMaster, + isDeleted, + isNew, + creator, + } = this.entity; + const onSubmit = () => + this.submit(() => { + this.display(); + }); const onConfirmDiscard = () => this.confirmDiscard(); - const onDelete = () => { this.confirmDelete(); }; + const onDelete = () => { + this.confirmDelete(); + }; const onDeleteSeries = () => { this.entity.revert(); this.edit(seriesMaster.get(), false); @@ -903,67 +1520,71 @@ class EventPanel extends EntityPanelBase implements IEven }; const deleteSingleCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, - onClick: onDelete + onClick: onDelete, }; const deleteSeriesMasterCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete_Series.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, - onClick: onDelete + onClick: onDelete, }; const deleteRecurringCommand: ICommandBarItemProps = { - key: 'delete', + key: "delete", text: strings.Command_Delete.Text, - iconProps: { iconName: 'Delete' }, + iconProps: { iconName: "Delete" }, disabled: isDeleted, subMenuProps: { - items: [{ - key: 'delete-series', - text: strings.Command_Delete_Recurring_Series.Text, - onClick: onDeleteSeries - }, { - key: 'delete-occurrence', - text: strings.Command_Delete_Recurring_Instance.Text, - onClick: onDelete - }] - } + items: [ + { + key: "delete-series", + text: strings.Command_Delete_Recurring_Series.Text, + onClick: onDeleteSeries, + }, + { + key: "delete-occurrence", + text: strings.Command_Delete_Recurring_Instance.Text, + onClick: onDelete, + }, + ], + }, }; - const userCanApprove = currentUserIsSiteAdmin || this._currentUserIsAnApprover(); + const userCanApprove = + currentUserIsSiteAdmin || this._currentUserIsAnApprover(); const userIsCreator = User.equal(creator, currentUser); const canEdit = userIsCreator || userCanApprove; const canDelete = (!isNew || isSeriesException) && canEdit; - return [{ - key: 'save', - text: strings.Command_Save.Text, - iconProps: { iconName: 'Save' }, - disabled: submitting || isDeleted, - onClick: onSubmit - }, { - key: 'discard', - text: strings.Command_Discard.Text, - iconProps: { iconName: 'Cancel' }, - disabled: isDeleted, - onClick: onConfirmDiscard - }, - canDelete && ( - isRecurring - ? (isSeriesMaster - ? deleteSeriesMasterCommand - : deleteRecurringCommand - ) - : deleteSingleCommand - ) + return [ + { + key: "save", + text: strings.Command_Save.Text, + iconProps: { iconName: "Save" }, + disabled: submitting || isDeleted, + onClick: onSubmit, + }, + { + key: "discard", + text: strings.Command_Discard.Text, + iconProps: { iconName: "Cancel" }, + disabled: isDeleted, + onClick: onConfirmDiscard, + }, + canDelete && + (isRecurring + ? isSeriesMaster + ? deleteSeriesMasterCommand + : deleteRecurringCommand + : deleteSingleCommand), ].filter(Boolean); } } -export default withServices(EventPanel); \ No newline at end of file +export default withServices(EventPanel); diff --git a/samples/react-rhythm-of-business-calendar/src/components/events/IEventCommands.ts b/samples/react-rhythm-of-business-calendar/src/components/events/IEventCommands.ts index eae9806d5..2d5e39c91 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/events/IEventCommands.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/events/IEventCommands.ts @@ -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; export interface IEventCommands { view: EventCommand; @@ -8,5 +10,7 @@ export interface IEventCommands { reject: EventCommand; addToOutlook: EventCommand; addSeriesToOutlook: EventCommand; + // sharedEventLink: ChannelsEventCommand; getLink: EventCommand; + configEnableOutlook : boolean; } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.module.scss b/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.module.scss new file mode 100644 index 000000000..5bc16a453 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.module.scss @@ -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 +} diff --git a/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.tsx b/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.tsx index 86d25e853..850d221a6 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/hooks/useEventCommandActionButtons.tsx @@ -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(); + const [isSaveBtnEnable,setIsSaveBtnEnable]= useState(false); + const { active: config } = useConfigurationService(); + const enableAddToOutlook = config && config.useAddToOutlook; const viewCommand = useMemo(() => 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, 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 = { dropdown: { width: 300 } }; + + + //enable to share event + + // const shareToTeams = useMemo(() => + //

, + // [event, sharedEventLink,linkShared,channelId, groupId,isShared,isSuccess,isError,errorMessage,selectedItem,isSaveBtnEnable] + // ); + + + //enable to share event channel + + // const shareCommand = useMemo(() => + // <> + // + // { + // setLinkShared(true); + // }}> + //
+ // {strings.Command_Share.Text} + //
+ //
+ // {shareToTeams} + // + // , + // [event, sharedEventLink,linkShared,channelId, groupId,isShared,isSuccess,isError,errorMessage,selectedItem,isSaveBtnEnable] + // ); + + return [ viewCommand, addToOutlookCommand, getLinkCommand + //shareCommand //uncomment to share functionality ] as const; }; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/hooks/useExecuteEventDeepLink.ts b/samples/react-rhythm-of-business-calendar/src/components/hooks/useExecuteEventDeepLink.ts index 40cf3a9c5..a44845ad1 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/hooks/useExecuteEventDeepLink.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/hooks/useExecuteEventDeepLink.ts @@ -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(); const [recurrenceDate, setRecurrenceDate] = useState(); const [event, setEvent] = useState(); + 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]); diff --git a/samples/react-rhythm-of-business-calendar/src/components/hooks/useSettings.ts b/samples/react-rhythm-of-business-calendar/src/components/hooks/useSettings.ts index 0185877ed..66e4443f0 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/hooks/useSettings.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/hooks/useSettings.ts @@ -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(); + //const configureChannelsPanel = useRef(); //enable to share event return [ userCanManageSettings, settingsPanel, configureApproversPanel, + //configureChannelsPanel, //enable to share event + userHasEditPermisison, editSettings ] as const; }; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/loc/componentstrings.d.ts b/samples/react-rhythm-of-business-calendar/src/components/loc/componentstrings.d.ts index b0c02119d..1deaa7773 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/loc/componentstrings.d.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/loc/componentstrings.d.ts @@ -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; diff --git a/samples/react-rhythm-of-business-calendar/src/components/loc/en-us.js b/samples/react-rhythm-of-business-calendar/src/components/loc/en-us.js index 9913c378f..8fb224ac0 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/loc/en-us.js +++ b/samples/react-rhythm-of-business-calendar/src/components/loc/en-us.js @@ -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" + } }; }); \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.module.scss b/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.module.scss index c82f2b637..01bfb7a03 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.module.scss +++ b/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.module.scss @@ -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; @@ -20,4 +30,5 @@ } } } -} \ No newline at end of file +} + diff --git a/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.tsx b/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.tsx index 412a129de..0bcc93075 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/settings/SettingsPanel.tsx @@ -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 { } @@ -42,6 +58,9 @@ interface IOwnProps { onNewRefiner: () => void; onEditRefiner: (refiner: Refiner) => void; configureApproversPanel: RefObject; + selectedTemplateKeys?: string[]; + // configureChannelsPanel: RefObject; + } type IProps = IOwnProps & IEntityPanelProps & ServicesProp; @@ -49,9 +68,47 @@ interface IOwnState { groupByRefinerOptions: IDropdownOption[]; refiners: Refiner[]; refinersById: ReadonlyRefinerMap; + selectedKeys: string[]; + selectedTemplateKeys: string[]; } type IState = IOwnState & IDataPanelBaseState; +const comboBoxStyles: Partial = { 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 implements ISettingsPanel { protected get title() { return strings.Heading; @@ -64,10 +121,40 @@ class SettingsPanel extends EntityPanelBase imple ...super.resetState(), groupByRefinerOptions: [], refiners: [], - refinersById: new Map() + refinersById: new Map(), + selectedKeys: ['displayName'], + selectedTemplateKeys:['eventTitle'] }; } + // public onChange = ( + // event: React.FormEvent, + // 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,7 +206,9 @@ class SettingsPanel extends EntityPanelBase 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; const { showValidationFeedback, groupByRefinerOptions, refiners, refinersById } = this.state; @@ -130,11 +219,13 @@ class SettingsPanel extends EntityPanelBase imple showValidationFeedback, updateField: this.updateFieldAndSubmit }; - + + return ( +

{strings.Heading_GeneralSettings}

- + imple renderValue={v => ViewDescriptorsById.get(v).title} /> - + imple renderValue={v => monthNames[v]} /> + + v} + renderValue={v => ViewYearFYKeys[v]} + /> + - + imple propertyName='useRefiners' /> - + imple /> + + + + + + + + + + val} + // placeholder={anyValueString} + // onRenderTitle={() => <>{humanizedString(selectedValues)}} + renderValue= {(vals) => ( + ListViewKeys + + )} + + /> + + + +
+

{strings.Heading_ApprovalSettings}

imple - + + + + +
+

{strings.Heading_TemplateSettings}

+ + +
{/* Container for dropdown and image */} + val} + // placeholder={anyValueString} + // onRenderTitle={() => <>{humanizedString(selectedValues)}} + renderValue= {(vals) => ( + TemplateViewKeys + + )} + style={{ width: '188px' }} + + /> +
+ Preview +
+ {(this.props.selectedTemplateKeys.includes('tag') && this.props.selectedTemplateKeys.includes('starttime')) ?
[TAG] [Start Time - End Time]
: this.props.selectedTemplateKeys.includes('tag') ?
[TAG]
: this.props.selectedTemplateKeys.includes('starttime') ?
[Start Time - End Time]
: null} +
[Event Name]
+ {this.props.selectedTemplateKeys.includes('location') &&
[Location]
} +
+ {/* Template Preview */} +
+ +
+
+
+ {/* enable this code for the share option */} + {/*
+

{strings.Heading_ChannelsSettings} + + {} +

+ + + + + {strings.Command_ConfigureChannels.Text} + + + + */} +
{useRefiners && diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/DateRotatorController.ts b/samples/react-rhythm-of-business-calendar/src/components/views/DateRotatorController.ts index 9884a8e3c..7fc5cec4c 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/DateRotatorController.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/DateRotatorController.ts @@ -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)); diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/IViewDescriptor.ts b/samples/react-rhythm-of-business-calendar/src/components/views/IViewDescriptor.ts index 77224fd80..f6e8f419d 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/IViewDescriptor.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/IViewDescriptor.ts @@ -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; + } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/IViewProps.ts b/samples/react-rhythm-of-business-calendar/src/components/views/IViewProps.ts index 9df06a8f0..5763d69ef 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/IViewProps.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/IViewProps.ts @@ -10,4 +10,10 @@ export interface IViewProps { selectedRefinerValues: Set; eventCommands: IEventCommands; viewCommands: IViewCommands; + siteTimeZone?: string; + selectedKeys?: string[]; + selectedTemplateKeys?: string[]; + onStateChange?: (stateVariable: any) => void; + + // channels: readonly ChannelsConfigurations[]; } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.module.scss b/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.module.scss index e70ec5cb8..9ec33e632 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.module.scss +++ b/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.module.scss @@ -8,4 +8,96 @@ @include lg-up { 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; } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.tsx index b23375a4d..c58caf8fa 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/ViewRoute.tsx @@ -1,65 +1,358 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { IComponentStyles } from '@uifabric/foundation'; -import { useBoolean, useForceUpdate } from '@fluentui/react-hooks'; -import { ActionButton, CommandBar, ICommandBarItemProps, IconButton, IIconProps, IStackItemSlots, IStackTokens, Panel, PanelType, Stack, StackItem, Text, TooltipHost } from '@fluentui/react'; -import { BackEventListener } from 'common'; -import { AsyncDataComponent, DateRotator } from 'common/components'; -import { IEvent } from 'model'; -import { useConfigurationService, useDirectoryService, useEventsService } from 'services'; -import { ApprovalDialog, ConfigureApproversPanel, MyApprovalsFilter, MyApprovalsPanel } from '../approvals'; -import { useApprovals, useCopyLinkDialog, useExecuteEventDeepLink, useEventPanel, useRefinerPanel, useSettings, useRefinerValues, useWindowSize } from '../hooks'; -import { Refiners, RefinerPanel } from '../refiners'; -import { SettingsPanel } from '../settings'; -import { EventFilter, EventPanel, IEventCommands } from '../events'; -import { CopyLinkDialog, Rail, SwipedEvents, SwipeEventListener } from '../shared'; -import { IViewCommands, useDataRotatorController, useView, ViewNav } from '.'; +import React, { FC, useCallback, useEffect, useMemo, useState, useRef } from "react"; +import { IComponentStyles } from "@uifabric/foundation"; +import { useBoolean, useForceUpdate } from "@fluentui/react-hooks"; +import { + ActionButton, + CommandBar, + ICommandBarItemProps, + IconButton, + IIconProps, + IStackItemSlots, + IStackTokens, + Panel, + PanelType, + Stack, + StackItem, + Text, + TooltipHost, + Spinner, SpinnerSize, SearchBox, Dropdown, IDropdownOption, DefaultButton, ITooltipHostStyles, DatePicker, defaultDatePickerStrings, mergeStyles, + ComboBox, IComboBox, IComboBoxOption, IComboBoxStyles, SelectableOptionMenuItemType +} from "@fluentui/react"; +import { BackEventListener } from "common"; +import { AsyncDataComponent, DateRotator } from "common/components"; +import { IEvent, EventOccurrence } from "model"; +import { + useConfigurationService, + useDirectoryService, + useEventsService, + useTimeZoneService, +} from "services"; +import { + ApprovalDialog, + ConfigureApproversPanel, + MyApprovalsFilter, + MyApprovalsPanel, +} from "../approvals"; +// import { +// ConfigureChannelsPanel +// } from "../ChannelsConfiguration"; +import { + useApprovals, + useCopyLinkDialog, + useExecuteEventDeepLink, + useEventPanel, + useRefinerPanel, + useSettings, + useRefinerValues, + useWindowSize, +} from "../hooks"; +import { Refiners, RefinerPanel } from "../refiners"; +import { SettingsPanel } from "../settings"; +import { EventFilter, EventPanel, IEventCommands } from "../events"; +import { + CopyLinkDialog, + Rail, + SwipedEvents, + SwipeEventListener, +} from "../shared"; +import { IViewCommands, useDataRotatorController, useView, ViewNav } from "."; import { ViewRoute as strings } from "ComponentStrings"; -import styles from './ViewRoute.module.scss'; - +import styles from "./ViewRoute.module.scss"; +import html2canvas from 'html2canvas'; +import pptxgen from 'pptxgenjs'; +import { ProductivityStudioLogo } from "../ProductivityStudioLogo"; +import moment from "moment"; +import { FontSizes } from "office-ui-fabric-react"; +import { useId } from '@fluentui/react-hooks'; +import ExportToExcel from './list/ExportToExcel'; +//import { DetailsListDocumentsExample } from "./list/ListView"; const RefinerRailPanelDisplayBreakpoint = 1024; const rootStackTokens: IStackTokens = { childrenGap: 16 }; -const refinerRailStackTokens: IStackTokens = { childrenGap: 16, padding: '10px 0 0 20px' }; -const viewStackTokens: IStackTokens = { childrenGap: 8, padding: '10px 0' }; - +const refinerRailStackTokens: IStackTokens = { + childrenGap: 16, + padding: "10px 0 0 20px", +}; +const viewStackTokens: IStackTokens = { childrenGap: 8, padding: "10px 0" }; +const calloutProps = { gapSpace: 0 }; const calendarViewStackItemStyles: IComponentStyles = { - root: { minWidth: 0 } + root: { minWidth: 0 }, }; -const addRefinerIconProps: IIconProps = { iconName: 'Add' }; -const collapseRefinerRailIconProps: IIconProps = { iconName: 'ClosePaneMirrored' }; -const expandRefinerRailIconProps: IIconProps = { iconName: 'ClosePane' }; +const addRefinerIconProps: IIconProps = { iconName: "Add" }; +const collapseRefinerRailIconProps: IIconProps = { + iconName: "ClosePaneMirrored", +}; +const expandRefinerRailIconProps: IIconProps = { iconName: "ClosePane" }; + +function getStartOfMonth() { + const currentDate = new Date(); + //currentDate.setDate(1); // Set the day of the month to 1 to get the start of the month + return currentDate; +} +function getEndOfMonth() { + const currentDate = new Date(); + // currentDate.setMonth(currentDate.getMonth() + 1); + // currentDate.setDate(1); + // currentDate.setDate(currentDate.getDate() - 1); + const endDate = new Date(currentDate); // Create a copy of today's date + endDate.setDate(endDate.getDate() + 30); // Add 30 days to today's date + return endDate; +} + const ViewRoute: FC = () => { + const eventsSectionRef = useRef(null); + const rootClass = mergeStyles({ maxWidth: 500, selectors: { '> *': { marginBottom: 15 } } }); + const settingsConfigurationref = useRef(null); + const add_refinerref= useRef(null); + const [isLoader, setIsLoader] = useState(false); + const [searchText,setSearchText]= useState(""); + const [startDateList,setStartDateList]= useState(getStartOfMonth()); + const [endDateList,setEndDateList]= useState(getEndOfMonth()); + const [isListView, setIsListView] = useState(false); + const [exactMatch,setExactMatch]= useState(true); + const [selectedItem, setSelectedItem] = React.useState(); + const tooltipId = useId('tooltip'); + const hostStyles: Partial = { root: { display: 'inline-block', position: 'absolute', + transform: 'translateY(-50%)', + zIndex: '1', // Ensure the tooltip is above the search box + top: '69px', + right: '30px', + backgroundColor: 'white', // Set default background color + } }; const { active: config } = useConfigurationService(); const { currentUserIsSiteAdmin } = useDirectoryService(); - + const { isDifferenceInTimezone, siteTimeZone } = useTimeZoneService(); + const [selectedKeys, setSelectedKeys] = React.useState(['displayName']); + const [selectedTemplateKeys, setSelectedTemplateKeys] = React.useState(['eventTitle']); + const [isDifferenceInTimezoneVal, setIsDifferenceInTimezoneVal] = + useState(false); const view = useView(); const View = view.renderer; + useEffect(() => { + setIsDifferenceInTimezoneVal(isDifferenceInTimezone); + }, []); + + useEffect(() => { + // Update selectedKeys based on config changes + setSelectedKeys(config?.listViewColumn || ['displayName']); + }, [config.listViewColumn]); + + useEffect(() => { + // Update selectedTemplateKeys based on config changes + setSelectedTemplateKeys(config?.templateView || ['eventTitle']); + }, [config.templateView]); const [ anchorDate, setAnchorDate, dateString, onRotatePreviousDate, - onRotateNextDate + onRotateNextDate, ] = useDataRotatorController(view.dateRotatorController); - const dateRange = useMemo( - () => view.dateRange(anchorDate, config), - [view, anchorDate] + const exportToExcelRef = useRef(null); + + const handleExportExcel = () => { + if (exportToExcelRef.current) { + exportToExcelRef.current.handleExportExcel(); + } + }; + + const downloadScreenshotAsImage = (dataUrl:string,file_Name:string) => { + + const a = document.createElement('a'); + a.href = dataUrl; + a.download = file_Name +'.png'; // Set the desired filename + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + console.log("done"); + }; + + const captureScreenshotAsPPT = async (screenshotDataUrl:string,file_Name:string) => { + try { + + if (screenshotDataUrl) { + const pptx = new pptxgen(); + const slide = pptx.addSlide(); + slide.addImage({ data: screenshotDataUrl, x: 0, y: 0, w: '100%', h: '100%' }); + pptx.writeFile({fileName: file_Name}); + } else { + console.error('Section not found.'); + } + } catch (error) { + console.error('Error capturing and saving screenshot as PPT:', error); + } + }; + + const printScreenshot = async (screenshotDataUrl:string,file_Name:string) => { + try { + + const printWindow = window.open('', '_blank', 'width=' + window.innerWidth + ',height=' + window.innerHeight); + printWindow.document.open(); + printWindow.document.write(''+ file_Name +''); + printWindow.document.write(''); + printWindow.document.write(''); + printWindow.document.close(); + + // Print the content in the new window or modal dialog + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 200); + + + } catch (error) { + console.error('Error printing screenshot:', error); + } + }; + + const captureScreenshot = async (downloadType:string) => { + try { + const button = document.querySelector('.btnDateLabel'); + let buttonText; + if (button) { + buttonText = button.textContent.trim(); + } else { + console.log("Button with class name 'btnDateLabel' not found."); + } + const eventsSection = eventsSectionRef.current; + let _settingsConfiguration: any; + let _addrefiner: any; + if(settingsConfigurationref && settingsConfigurationref.current){ + settingsConfigurationref.current.style.display = 'none'; + _settingsConfiguration= settingsConfigurationref.current; + } + if(add_refinerref && add_refinerref.current){ + add_refinerref.current.style.display = 'none'; + _addrefiner = add_refinerref.current; + } + setIsLoader(true); + + if (eventsSection) { + const screenshot = await html2canvas(eventsSection); + if(_settingsConfiguration) + _settingsConfiguration.style.display='block'; + if(_addrefiner) + _addrefiner.style.display = 'block'; + + const screenshotDataUrl = screenshot.toDataURL('image/png'); + if(!buttonText) + buttonText= "ListView" + if(downloadType === "ImageFile"){ + downloadScreenshotAsImage(screenshotDataUrl,buttonText); + } + else if(downloadType === "PdfFile"){ + + printScreenshot(screenshotDataUrl,buttonText); + } + else if(downloadType === "PPTFile"){ + captureScreenshotAsPPT(screenshotDataUrl, buttonText); + } + setIsLoader(false); + } + else { + console.error('Section not found.'); + } + } + catch (error) { + console.error('Error capturing screenshot:', error); + } + }; + + const comboBoxStyles: Partial = { 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 selectableOptions = options.filter( + option => + (option.itemType === SelectableOptionMenuItemType.Normal || option.itemType === undefined) && !option.disabled, ); - const { eventsAsync, refinersAsync, refinerValuesAsync, approversAsync } = useEventsService(); - const [asyncWatchers] = useState([eventsAsync, refinersAsync, refinerValuesAsync, approversAsync]); + const onChange = ( + event: React.FormEvent, + option?: IComboBoxOption, + index?: number, + value?: string, + ): void => { + const selected = option?.selected; + const currentSelectedOptionKeys = selectedKeys.filter(key => key !== 'selectAll'); + const selectAllState = currentSelectedOptionKeys.length === selectableOptions.length; + + if (option) { + if (option?.itemType === SelectableOptionMenuItemType.SelectAll) { + const nonDisabledKeys = selectableOptions + .filter(o => !o.disabled) + .map(o => o.key as string); + + selectAllState + ? setSelectedKeys([]) + : setSelectedKeys(['selectAll', ...selectableOptions.filter(o => !o.disabled).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'); + } + setSelectedKeys(updatedKeys); + } + } + }; + - const [ - hasRefiners, - selectedRefinerValues, - onSelectedRefinerValuesChanged - ] = useRefinerValues(); + // const dateRange = useMemo( + // () => view.dateRange(anchorDate, config), + // [view, anchorDate] + // ); + const dateRange = useMemo(() => { + if (view.id === 'list') { + setIsListView(true); + const start = moment(startDateList); + const end = moment(endDateList); + return { start, end }; + } else { + setIsListView(false); + return view.dateRange(anchorDate, config); + } + }, [view, anchorDate, startDateList, endDateList]); + + const { eventsAsync, refinersAsync, refinerValuesAsync, approversAsync } = + useEventsService(); + const [asyncWatchers] = useState([ + eventsAsync, + refinersAsync, + refinerValuesAsync, + approversAsync, + // channelsConfigurationsAsync + ]); + + const [hasRefiners, selectedRefinerValues, onSelectedRefinerValuesChanged] = + useRefinerValues(); const [ userIsAnApprover, @@ -68,40 +361,37 @@ const ViewRoute: FC = () => { openMyApprovalsPanel, , approveEvent, - rejectEvent + rejectEvent, ] = useApprovals(); - const [ - eventPanel, - newEvent, - displayEvent - ] = useEventPanel(anchorDate); + const [eventPanel, newEvent, displayEvent] = useEventPanel(anchorDate); - const [ - refinerPanel, - newRefiner, - editRefiner - ] = useRefinerPanel(); + const [refinerPanel, newRefiner, editRefiner] = useRefinerPanel(); const [ userCanManageSettings, settingsPanel, configureApproversPanel, - editSettings + // configureChannelsPanel, + userHasEditPermisison, + editSettings, ] = useSettings(); - const [ - copyLinkDialog, - getLink - ] = useCopyLinkDialog(); + const [copyLinkDialog, getLink] = useCopyLinkDialog(); const { width } = useWindowSize(); const useSwipeInRefiners = width <= RefinerRailPanelDisplayBreakpoint; const useRefinersRail = !useSwipeInRefiners; - const [isRefinerRailExpanded, { setTrue: expandRail, setFalse: collapseRail }] = useBoolean(false); - const backEventListener = useMemo(() => new BackEventListener(collapseRail), [collapseRail]); + const [ + isRefinerRailExpanded, + { setTrue: expandRail, setFalse: collapseRail }, + ] = useBoolean(false); + const backEventListener = useMemo( + () => new BackEventListener(collapseRail), + [collapseRail] + ); useEffect(() => { return () => backEventListener.cleanup(); }, [backEventListener]); @@ -116,42 +406,153 @@ const ViewRoute: FC = () => { collapseRail(); }, [backEventListener, collapseRail]); - const swipeHandler: SwipeEventListener = useCallback(({ detail }) => { - if (detail.dir === 'right') { - openRefinerRailPanel(); - } - }, [openRefinerRailPanel]); + const swipeHandler: SwipeEventListener = useCallback( + ({ detail }) => { + if (detail.dir === "right") { + openRefinerRailPanel(); + } + }, + [openRefinerRailPanel] + ); useExecuteEventDeepLink(displayEvent); - const useRefiners = (currentUserIsSiteAdmin || hasRefiners) && config.useRefiners; + const useRefiners = + (currentUserIsSiteAdmin || hasRefiners) && config.useRefiners; - const commandBarItems = useCallback((numberOfEventsNeedingApproval: number) => { - return ([ - { - key: 'new-event', - text: strings.Command_NewEvent.Text, - iconProps: { iconName: 'Add' }, - onClick: () => newEvent() - }, - userCanManageSettings && { - key: 'settings', - text: strings.Command_Settings.Text, - iconProps: { iconName: 'Settings' }, - onClick: () => editSettings() - }, - userIsAnApprover && { - key: 'approvals', - text: numberOfEventsNeedingApproval ? `${strings.Command_Approvals.Text} (${numberOfEventsNeedingApproval})` : strings.Command_Approvals.Text, - iconProps: { iconName: 'InboxCheck' }, - onClick: () => openMyApprovalsPanel() - } - ] as ICommandBarItemProps[]).filter(Boolean); - }, [userCanManageSettings, userIsAnApprover, newEvent, editSettings, openMyApprovalsPanel]); + const commandBarItems = useCallback( + (numberOfEventsNeedingApproval: number) => { + + const otherItems = [ + userHasEditPermisison && { + key: "new-event", + text: strings.Command_NewEvent.Text, + iconProps: { iconName: "Add" }, + onClick: () => newEvent(), + }, + userCanManageSettings && { + key: "settings", + text: strings.Command_Settings.Text, + iconProps: { iconName: "Settings" }, + onClick: () => editSettings(), + }, + userIsAnApprover && { + key: "approvals", + text: numberOfEventsNeedingApproval + ? `${strings.Command_Approvals.Text} (${numberOfEventsNeedingApproval})` + : strings.Command_Approvals.Text, + iconProps: { iconName: "InboxCheck" }, + onClick: () => openMyApprovalsPanel(), + }, + ]; + const dropdownMenuItems = [ + { + key: 'captureDropdown', + text: 'Export to', + iconProps: { iconName: 'Export' }, + subMenuProps: { + items: [ + { + key: "captureButtonImage", + text: "Image", + iconProps: { iconName: "FileImage" }, + onClick: () => captureScreenshot("ImageFile"), + }, + + { + key: "captureButtonPdf", + text: "PDF", + iconProps: { iconName: "PDF" }, + onClick: () => captureScreenshot("PdfFile"), + }, + { + key: "captureButtonPPT", + text: "PPT", + iconProps: { iconName: "PowerPointDocument" }, + onClick: () => captureScreenshot("PPTFile"), + }, + { + key: "captureButtonExcel", + text: "Excel", + iconProps: { iconName: "ExcelDocument" }, + onClick: handleExportExcel, + }, + + ], + }, + }, + ]; + const items = [...otherItems, ...dropdownMenuItems].filter(Boolean); + return items as ICommandBarItemProps[] + }, + [ + userCanManageSettings, + userIsAnApprover, + newEvent, + editSettings, + openMyApprovalsPanel, + ] + ); const events = useEventsService(); - const addEventToOutlook = (event: IEvent) => { events.addToOutlook(event.getExceptionOrEvent()); }; - const addEventSeriesToOutlook = (event: IEvent) => { events.addToOutlook(event.getSeriesMaster()); }; + const addEventToOutlook = (event: IEvent) => { + events.addToOutlook(event.getExceptionOrEvent()); + }; + // const sharedEventLink = async (event: IEvent, channelId: string, groupId: string) =>{ + // const itemUrl = events.createEventDeepLink(event.getExceptionOrEvent()); + // try{ + // await events.sendDetailinPost(event.getExceptionOrEvent(),itemUrl, channelId, groupId); + // return "Success"; + // } + // catch(ex){ + // return ex.message; + // } + // } + + const addEventSeriesToOutlook = (event: IEvent) => { + events.addToOutlook(event.getSeriesMaster(), isDifferenceInTimezoneVal); + }; + + const handleSearchChange = (event?: React.ChangeEvent, newValue?: string) => { + if(newValue == ""){ + setSelectedItem(undefined); + } + //setSearchText(newValue.toLowerCase()); + setSearchText(newValue); + + } + + const exactMatchSearch = () => { + setExactMatch(!exactMatch); + }; + + + const filterSearch = (event: React.FormEvent, item: IDropdownOption): void => { + setSelectedItem(item); + }; + + const handleStartDateChange = (date: Date) => { + // const originalStart = moment(date); + // const convertedStartMoment = originalStart && originalStart.clone().tz(siteTimeZone.momentId,true); + // convertedStartMoment.startOf('day'); + // const dateString = convertedStartMoment.toString(); + + // // Convert string to JavaScript Date object + // const convertedStartDate = new Date(dateString); + setStartDateList(date); + }; + + const handleEndDateChange = (date: Date) => { + // const originalEnd = moment(date); + // const convertedEndMoment = originalEnd && originalEnd.clone().tz(siteTimeZone.momentId,true); + // convertedEndMoment.endOf('day'); + // const dateString = convertedEndMoment.toString(); + + // // Convert string to JavaScript Date object + // const convertedEndDate = new Date(dateString); + setEndDateList(date); + + }; const eventCommands = useMemo(() => { return { @@ -160,164 +561,516 @@ const ViewRoute: FC = () => { reject: rejectEvent, addToOutlook: addEventToOutlook, addSeriesToOutlook: addEventSeriesToOutlook, - getLink - } as IEventCommands; + // sharedEventLink: sharedEventLink, + getLink, + configEnableOutlook: config.useAddToOutlook + } as IEventCommands ; }, [displayEvent, approveEvent, rejectEvent]); const viewCommands = useMemo(() => { return { setAnchorDate, newEvent, - activateEvent: displayEvent + activateEvent: displayEvent, } as IViewCommands; }, [setAnchorDate, newEvent]); + const searchBoxStyles = React.useMemo( + () => ({ + root: { + // width: 200, + border: `${searchText ? '4px solid #03787c' : '1px solid #010101'}`, // Border style changes dynamically based on searchText + borderRadius: `${searchText ? '4px' : '2px'}`, + boxSizing: 'border-box', + }, + }), + [searchText] + ); - return <> - - {refiners => - {() => - - - {currentUserIsSiteAdmin && - - {strings.Command_AddRefiner.Text} - - } - - } - } - - - - {useRefiners && useRefinersRail && - - {collapseRail => - {refiners => - {() => - - - {strings.RefinerRailLabel} - - - - - - {currentUserIsSiteAdmin && - - {strings.Command_AddRefiner.Text} - - } - - } - }} - - - } - - {approvers => - {refiners => - {events => - - {events => - - {useRefiners && useSwipeInRefiners && - - } - - - } - - - - - + + + {(refiners) => ( + + {() => ( + + - {cccurrences => - + selectedValues={selectedRefinerValues} + onSelectionChanged={ + onSelectedRefinerValuesChanged } - + onEditRefiner={editRefiner} + /> + {currentUserIsSiteAdmin && ( + + {strings.Command_AddRefiner.Text} + + )} - } - } - } - - - - - - - - - - - ; + )} + + )} + + + + {isLoader && <>
+
+ +
} + +
+ + + {useRefiners && useRefinersRail && ( + + + {(collapseRail) => ( + + {(refiners) => ( + + {() => ( + + + + { + strings.RefinerRailLabel + } + + + + + + + {currentUserIsSiteAdmin && (
+ + { + strings + .Command_AddRefiner + .Text + } + +
+ )} +
+ )} +
+ )} +
+ )} +
+
+ )} + + + {(approvers) => ( + + {(refiners) => ( + + {(events) => ( + +
+ + {(events) => ( + + + {useRefiners && + useSwipeInRefiners && ( + + )} + + + +
+
+
+ console.log('value is '+ newValue)} + onChange={handleSearchChange} + value={searchText} + onClear={() => setSearchText('')} + /> +
+ {/*
+ +
*/} +
+ {/*
+
+
+ + + +
+
*/} + {/*
+ console.log('value is '+ newValue)} + onChange={handleSearchChange} + /> + + + +
*/} +
+ )} +
+
+ + {!isListView && + } + + {isListView && +
+ + + + {!userCanManageSettings &&
+ +
} +
+ } + +
+ + + {(cccurrences) => ( + <> + + + + + )} + +
+ )} +
+ )} +
+ )} +
+
+ +
+
+ +
+ + + + + + + {/* */} + + + + ); }; -export default ViewRoute; \ No newline at end of file +export default ViewRoute; diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/Views.ts b/samples/react-rhythm-of-business-calendar/src/components/views/Views.ts index 107122db2..3b0ded2ec 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/Views.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/Views.ts @@ -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); \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/day/DayView.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/day/DayView.tsx index 4607f31fb..de2a4217a 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/day/DayView.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/day/DayView.tsx @@ -23,6 +23,7 @@ const eventCommandsStackItemStyles: IStackItemStyles = { interface IEventCardProps { occurrence: EventOccurrence; commands: IEventCommands, + //channels: readonly ChannelsConfigurations[]; } const EventCard: FC = ({ occurrence, commands }) => { @@ -54,7 +55,7 @@ const EventCard: FC = ({ occurrence, commands }) => { const DayView: FC = ({ cccurrences, - eventCommands, + eventCommands }) => { if (cccurrences.length === 0) { return {strings.DayView.NoEventsMessage} @@ -68,6 +69,7 @@ const DayView: FC = ({ key={`${occurrence.event.id}-${occurrence.start.format('L')}`} occurrence={occurrence} commands={eventCommands} + // channels ={channels} /> )}
diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/list/ExportToExcel.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/list/ExportToExcel.tsx new file mode 100644 index 000000000..be8c76f97 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/views/list/ExportToExcel.tsx @@ -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 = forwardRef(( props: IExportToExcelProps, ref ) => { + const { _refiners } = props; + const items = [...props.items].sort(EventOccurrence.StartAscComparer) + const tableRef = useRef(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 {matchingDisplayNames}; + }; + const uniqueEtags = getUniqueEtags(items); + + const formatDate = (date: any) => { + return moment(date).format('MM-DD-YYYY HH:mm'); + }; + + return ( +
+ + + + + + + + + + {_refiners.map((refiner, index) => ( + + ))} + + + + + + + + + + + + + + + + {items.map((item, index) => ( + + + + + + + + {_refiners.map((refiner, index) => ( + + ))} + + + + + + + + + + + + + + ))} + +
NameStart TimeEnd TimeDescriptionIs RecurringAll Day Event{refiner.displayName}LocationTagIs RejectedEvent ContactsIs ConfidentialIs ApprovedTitleRecurrenceCreatedCreated ByModifiedModified By
{item.title}{formatDate(item.start)}{formatDate(item.end)}{item.description}{item.isRecurring ? 'Yes' : 'No'}{item.isAllDay ? 'Yes' : 'No'}{renderRefinerValues(item, refiner)}{item.location}{item.tag}{item.isRejected ? 'Yes' : 'No'} + {item.contacts.map((contact:any, index:any) => ( + + {contact.title ? contact.title : contact.email} + {index < item.contacts.length - 1 ? '; ' : ''} + + ))} + {item.isConfidential ? 'Yes' : 'No'}{item.isApproved ? 'Yes' : 'No'}{item.title} + {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} + {item.created.format('MMM D, YYYY')}{item.createdBy ? (item.createdBy.title ? item.createdBy.title : (item.createdBy.email ? item.createdBy.email : '')) : ''}{item.modified.format('MMM D, YYYY')}{item.modifiedBy ? (item.modifiedBy.title ? item.modifiedBy.title : (item.modifiedBy.email ? item.modifiedBy.email : '')) : ''}
+
+ ); +}); + +export default ExportToExcel; + diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/list/ListView.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/list/ListView.tsx new file mode 100644 index 000000000..a7ad39365 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/components/views/list/ListView.tsx @@ -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 { + // 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(); + + // const onActivate = useCallback((cccurrence: EventOccurrence, target: HTMLElement) => { + // detailsCallout.current?.open(cccurrence, target); + // }, []); + + const data = this.state.items.map(item => ({ + title: item.title + })); + + return ( +
+
+
+ +
+
Count: {this.state.items.length}
+
+ + {items.length > 0 ? ( +
+ +
+ ) : ( +

No items found

+ )} +
+ ); + } + 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 {item.start.format('M/D/YYYY h:mm A')}; + }, + }); + 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 {item.end.format('M/D/YYYY h:mm A')}; + }, + }); + 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 {item.description}; + }, + isPadded: true, + }); + this.setState({ columns: newColumns }); + break; + case 'isRecurring': + newColumns.push({ + key: 'column5', + name: ' Is Recurring', + fieldName: 'isRecurring', + iconName: 'SyncOccurence', + iconClassName: classNames.fileIconHeaderIcon, + minWidth: 60, + maxWidth: 90, + isResizable: true, + isCollapsible: true, + data: 'string', + //onColumnClick: this._onColumnClick, + onRender: (item: any) => { + return
+ {/* */} + + {/* {item.isRecurring.toString()} */} + {/* {item.isRecurring ? 'Yes' : 'No'} */} +
+ }, + }); + 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 {item.isAllDay ? 'Yes' : 'No'}; + + }, + 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 {matchingDisplayName}; + }, + 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 {joinedValues}; + // }, + // 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 {item.location}; + }, + 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 {item.tag}; + }, + 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 {item.isRejected ? 'Yes' : 'No'}; + }, + 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 {renderedContacts}; + }, + 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 {item.isConfidential ? 'Yes' : 'No'}; + }, + 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 {item.isApproved ? 'Yes' : 'No'}; + }, + 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 {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}; + }, + 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 {item.created.format('MMM D, YYYY')}; + }, + }); + 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 {item.createdBy ? (item.createdBy.title ? item.createdBy.title : (item.createdBy.email ? item.createdBy.email : "")) : ""}; + }, + }); + 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 {item.modified.format('MMM D, YYYY')}; + }, + }); + 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 {item.modifiedBy ? (item.modifiedBy.title ? item.modifiedBy.title : (item.modifiedBy.email ? item.modifiedBy.email : "")) : ""}; + }, + }); + 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, checked: boolean): void => { + this.setState({ isCompactMode: checked }); + }; + + private _onChangeModalSelection = (ev: React.MouseEvent, checked: boolean): void => { + this.setState({ isModalSelection: checked }); + }; + + private _onChangeText = (ev: React.FormEvent, 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, 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(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(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') + }; +} +}; diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/month/ContentRow.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/month/ContentRow.tsx index dcfe386a8..3ba11e8b2 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/month/ContentRow.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/month/ContentRow.tsx @@ -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 = ({ row: { items }, onActivate }) => +export const ContentRow: FC = ({ row: { items }, onActivate, selectedTemplateKeys }) => {items.map((item, idx) => {item instanceof EventItemInfo - ? + ? : } diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/month/EventItem.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/month/EventItem.tsx index ce52fa963..0ed0463f7 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/month/EventItem.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/month/EventItem.tsx @@ -6,9 +6,10 @@ import { EventItemInfo } from "./Builder"; interface IProps { eventInfo: EventItemInfo; onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void; + selectedTemplateKeys?: string[]; } -export const EventItem: FC = ({ eventInfo, onActivate }) => { +export const EventItem: FC = ({ eventInfo, onActivate, selectedTemplateKeys }) => { const { cccurrence, startsInWeek, endsInWeek } = eventInfo; const root = useRef(); @@ -24,6 +25,7 @@ export const EventItem: FC = ({ eventInfo, onActivate }) => { startsIn={startsInWeek} endsIn={endsInWeek} size={EventBarSize.Compact} + selectedTemplateKeys={selectedTemplateKeys} />
); diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/month/MonthView.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/month/MonthView.tsx index 7db0160e4..397e999b0 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/month/MonthView.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/month/MonthView.tsx @@ -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 = ({ anchorDate, eventCommands, viewCommands, cccurrences }) => { - const weeks = Builder.build(cccurrences, anchorDate); +const MonthView: FC = ({ 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(); const onActivate = useCallback((cccurrence: EventOccurrence, target: HTMLElement) => { @@ -28,11 +32,13 @@ const MonthView: FC = ({ anchorDate, eventCommands, viewCommands, cc anchorDate={anchorDate} onActivate={onActivate} viewCommands={viewCommands} + selectedTemplateKeys={selectedTemplateKeys} /> )} ); diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/month/Week.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/month/Week.tsx index 533725e55..9ebfea16a 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/month/Week.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/month/Week.tsx @@ -14,9 +14,10 @@ interface IProps { week: WeekInfo; onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void; viewCommands: IViewCommands; + selectedTemplateKeys?: string[]; } -export const Week: FC = ({ anchorDate, week, onActivate, viewCommands }) => { +export const Week: FC = ({ anchorDate, week, onActivate, viewCommands, selectedTemplateKeys }) => { const { palette: { neutralTertiary } } = useTheme(); const style: CSSProperties = { @@ -27,7 +28,7 @@ export const Week: FC = ({ anchorDate, week, onActivate, viewCommands })
{week.contentRows.map((row, idx) => - + )}
); diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/month/WeekBackground.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/month/WeekBackground.tsx index 81e89cf81..2db3771ac 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/month/WeekBackground.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/month/WeekBackground.tsx @@ -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 = ({ 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 = ({ anchorDate, commands: { newEvent, s {width >= 640 - ? {strings.Command_NewEvent.Text} - : + ? userHasEditPermisison && {strings.Command_NewEvent.Text} + : userHasEditPermisison && } diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/EventItem.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/EventItem.tsx index ae1f1fbc6..65c1fd264 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/EventItem.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/EventItem.tsx @@ -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 = ({ eventInfos, onActivate }) => { +export const EventItem: FC = ({ 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 = ({ eventInfos, onActivate }) => { endsIn={endsInMonth} timeStringOverride={startTimeString} size={EventBarSize.Compact} + type= {_strings.Quarter} + selectedTemplateKeys={selectedTemplateKeys} /> ); diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Month.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Month.tsx index 7eeace60b..0166d6d71 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Month.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Month.tsx @@ -16,6 +16,7 @@ interface IProps { selectedRefinerValues: Set; onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void; viewCommands: IViewCommands; + selectedTemplateKeys?: string[]; } export const Month: FC = ({ @@ -23,7 +24,8 @@ export const Month: FC = ({ 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 = ({ } })} > - {start.format("MMMM")} + {start.format("MMMM")+ " " +start.format("YYYY")} {(!refiner || selectedRefinerValues.has(refiner.blankValue) || (refiner.required && blankValue.eventCount > 0)) && - + } {refiner && refiner.values.filter(Entity.NotDeletedFilter).filter(value => selectedRefinerValues.has(value)).map(value => refinerValues.get(value)).map((value, idx) => - + )} diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/QuarterView.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/QuarterView.tsx index b8f6f027b..14325d25c 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/QuarterView.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/QuarterView.tsx @@ -14,7 +14,7 @@ import { getFiscalQuarter, getFiscalYear } from './Utils'; import { ViewNames as strings } from 'ComponentStrings'; -const QuarterView: FC = ({ anchorDate, cccurrences, refiners, selectedRefinerValues, viewCommands, eventCommands }) => { +const QuarterView: FC = ({ 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 = ({ anchorDate, cccurrences, refiners, select selectedRefinerValues={selectedRefinerValues} onActivate={onActivate} viewCommands={viewCommands} + selectedTemplateKeys={selectedTemplateKeys} /> )} @@ -55,6 +56,7 @@ const QuarterView: FC = ({ anchorDate, cccurrences, refiners, select ); @@ -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}`; } diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/RefinerValueEvents.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/RefinerValueEvents.tsx index bc89c3c66..3612d5c6f 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/RefinerValueEvents.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/RefinerValueEvents.tsx @@ -11,9 +11,10 @@ interface IProps { showTitle?: boolean; refinerValue: RefinerValueInfo; onActivate: (cccurrence: EventOccurrence, target: HTMLElement) => void; + selectedTemplateKeys?: string[]; } -export const RefinerValueEvents: FC = ({ showTitle = false, refinerValue: { title, itemsByEvent }, onActivate }) => { +export const RefinerValueEvents: FC = ({ 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 = ({ showTitle = false, refinerValue } {[...itemsByEvent.values()].map((items, idx) => - + )} diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Utils.ts b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Utils.ts index 393389610..6ce80c362 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Utils.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/quarter/Utils.ts @@ -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 diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/week/Background.tsx b/samples/react-rhythm-of-business-calendar/src/components/views/week/Background.tsx index a67b8b8ff..fd7a77a65 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/week/Background.tsx +++ b/samples/react-rhythm-of-business-calendar/src/components/views/week/Background.tsx @@ -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 = ({ 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 = ({ anchorDate, commands: { newEvent, setAn {width >= 640 - ? {strings.Command_NewEvent.Text} - : + ? userHasEditPermisison && {strings.Command_NewEvent.Text} + : userHasEditPermisison && } diff --git a/samples/react-rhythm-of-business-calendar/src/components/views/week/Builder.ts b/samples/react-rhythm-of-business-calendar/src/components/views/week/Builder.ts index 4eaa94709..ac87a0b93 100644 --- a/samples/react-rhythm-of-business-calendar/src/components/views/week/Builder.ts +++ b/samples/react-rhythm-of-business-calendar/src/components/views/week/Builder.ts @@ -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)); diff --git a/samples/react-rhythm-of-business-calendar/src/model/Cadence.ts b/samples/react-rhythm-of-business-calendar/src/model/Cadence.ts index 893c46d14..11cce1bc0 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/Cadence.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/Cadence.ts @@ -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 { @@ -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 (date.isSameOrAfter(range.start, 'day')) + // 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); } diff --git a/samples/react-rhythm-of-business-calendar/src/model/ChannelsConfigurations.ts b/samples/react-rhythm-of-business-calendar/src/model/ChannelsConfigurations.ts new file mode 100644 index 000000000..e39f740ff --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/model/ChannelsConfigurations.ts @@ -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 { +// public static readonly ChannelNameValidations = [ +// new RequiredValidationRule(e => e.channelName), +// new MaxLengthValidationRule(e => e.channelName, 255) +// ]; +// public static readonly TeamsIdValidations = [ +// new RequiredValidationRule(e => e.teamsId), +// new MaxLengthValidationRule(e => e.teamsId, 255) +// ]; +// public static readonly ChannelIdValidations = [ +// new RequiredValidationRule(e => e.channelId), +// new MaxLengthValidationRule(e => e.channelId, 255) +// ]; + +// // public static appliesTo(channelsConfigurations: ChannelsConfigurations, eventValuesByRefiner: Map): 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): 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; + +// private _refinerValuesByRefiner: Map = 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[] { +// return [ +// ...ChannelsConfigurations.ChannelNameValidations, +// ...ChannelsConfigurations.TeamsIdValidations, +// ...ChannelsConfigurations.ChannelIdValidations +// ]; +// } +// } + +// export type ChannelsConfigurationsMap = Map; +// export type ReadonlyChannelsConfigurationsMap = ReadonlyMap; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/model/Event.ts b/samples/react-rhythm-of-business-calendar/src/model/Event.ts index 7f4cbbb8e..f6ba80838 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/Event.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/Event.ts @@ -31,6 +31,7 @@ interface IState { moderator: User | undefined; moderationTimestamp: Moment | undefined; moderationMessage: string; + teamsGroupChatId: string; } export class Event extends ListItemEntity implements IEvent { @@ -75,6 +76,10 @@ export class Event extends ListItemEntity implements IEvent { public static readonly Count_Until_Recurrence_Validations = [ new Count_Until_Recurrence_Required_ValidationRule() ]; + // public static readonly TeamsGroupChatId_Validations = [ + // new RequiredValidationRule(e => e.teamsGroupChatId), + // new MaxLengthValidationRule(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 implements IEvent { this.state.moderator = undefined; this.state.moderationTimestamp = undefined; this.state.moderationMessage = ""; + this.state.teamsGroupChatId = ""; this.refinerValues = ManyToManyRelationship.create(this, 'events', { comparer: Event.RefinerValueOrderAscComparer }); this.includeInBoundedContext(this.refinerValues); @@ -310,6 +316,9 @@ export class Event extends ListItemEntity 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 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 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 { - return (!range || MomentRange.overlaps(this, range)) ? [new EventOccurrence(this)] : []; + 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 implements IEvent { ...Event.Date_YearlyByDate_Recurrence_Validations, ...Event.EndDate_Until_Recurrence_Validations, ...Event.Count_Until_Recurrence_Validations + //...Event.TeamsGroupChatId_Validations ]; } } diff --git a/samples/react-rhythm-of-business-calendar/src/model/EventModerationStatus.ts b/samples/react-rhythm-of-business-calendar/src/model/EventModerationStatus.ts index d5e7dfedd..467d94b55 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/EventModerationStatus.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/EventModerationStatus.ts @@ -20,7 +20,7 @@ export class EventModerationStatus { ) { } - public clone(): this { + public clone(): EventModerationStatus { return this; } } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/model/EventOccurrence.ts b/samples/react-rhythm-of-business-calendar/src/model/EventOccurrence.ts index 763437dc2..ba79dcd7b 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/EventOccurrence.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/EventOccurrence.ts @@ -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 = (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); diff --git a/samples/react-rhythm-of-business-calendar/src/model/ListViewKeys.ts b/samples/react-rhythm-of-business-calendar/src/model/ListViewKeys.ts new file mode 100644 index 000000000..8c72ada32 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/model/ListViewKeys.ts @@ -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"]; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/model/TemplateViewKeys.ts b/samples/react-rhythm-of-business-calendar/src/model/TemplateViewKeys.ts new file mode 100644 index 000000000..4da9d4980 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/model/TemplateViewKeys.ts @@ -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"]; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/model/ViewKeys.ts b/samples/react-rhythm-of-business-calendar/src/model/ViewKeys.ts index 748a779f1..4f06e20bb 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/ViewKeys.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/ViewKeys.ts @@ -4,7 +4,8 @@ export const ViewKeys = stringToEnum([ "daily", "weekly", "monthly", - "quarter" + "quarter", + "list" ]); export type ViewKeys = keyof (typeof ViewKeys); diff --git a/samples/react-rhythm-of-business-calendar/src/model/ViewYearFYKeys.ts b/samples/react-rhythm-of-business-calendar/src/model/ViewYearFYKeys.ts new file mode 100644 index 000000000..3c7fa380e --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/model/ViewYearFYKeys.ts @@ -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"]; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/model/index.ts b/samples/react-rhythm-of-business-calendar/src/model/index.ts index cc0732939..5e322b890 100644 --- a/samples/react-rhythm-of-business-calendar/src/model/index.ts +++ b/samples/react-rhythm-of-business-calendar/src/model/index.ts @@ -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"; @@ -7,4 +8,6 @@ export * from './humanize'; export { Recurrence, RecurDay, RecurPattern, RecurPatternOption, RecurWeekOfMonth, RecurUntilType, DailyRecurrence, WeeklyRecurrence, MonthlyRecurrence, YearlyRecurrence, RecurUntil } from "./Recurrence"; export { Refiner, type RefinerMap, type ReadonlyRefinerMap } from "./Refiner"; export { RefinerValue, type RefinerValueMap, type ReadonlyRefinerValueMap } from "./RefinerValue"; -export { ViewKeys, DefaultViewKey } from './ViewKeys'; \ No newline at end of file +export { ViewKeys, DefaultViewKey } from './ViewKeys'; +export { ViewYearFYKeys, DefaultViewYearFYKey } from './ViewYearFYKeys'; +export {ListViewKeys, DefaultListViewKeys} from './ListViewKeys'; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/Configuration.ts b/samples/react-rhythm-of-business-calendar/src/schema/Configuration.ts index 9783bf0d0..35ab34c36 100644 --- a/samples/react-rhythm-of-business-calendar/src/schema/Configuration.ts +++ b/samples/react-rhythm-of-business-calendar/src/schema/Configuration.ts @@ -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 { @@ -32,6 +39,12 @@ export class Configuration extends ListItemEntity { 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 { 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; diff --git a/samples/react-rhythm-of-business-calendar/src/schema/Defaults.ts b/samples/react-rhythm-of-business-calendar/src/schema/Defaults.ts index 09505c31c..eb4de94d7 100644 --- a/samples/react-rhythm-of-business-calendar/src/schema/Defaults.ts +++ b/samples/react-rhythm-of-business-calendar/src/schema/Defaults.ts @@ -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') } }; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/RhythmOfBusinessCalendarSchema.ts b/samples/react-rhythm-of-business-calendar/src/schema/RhythmOfBusinessCalendarSchema.ts index 855cb3440..65e5abe12 100644 --- a/samples/react-rhythm-of-business-calendar/src/schema/RhythmOfBusinessCalendarSchema.ts +++ b/samples/react-rhythm-of-business-calendar/src/schema/RhythmOfBusinessCalendarSchema.ts @@ -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({ @@ -19,12 +22,13 @@ export const RhythmOfBusinessCalendarSchema = buildLiveSchema; + +} \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/index.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/index.ts new file mode 100644 index 000000000..f3b14d007 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/index.ts @@ -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"; diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/AddFYStartYearColumnToConfigutationList.tsx b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/AddFYStartYearColumnToConfigutationList.tsx new file mode 100644 index 000000000..aea87dad6 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/AddFYStartYearColumnToConfigutationList.tsx @@ -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 { + const provisioner: ElementProvisioner = new ElementProvisioner(); + await provisioner.ensureField(Field_FiscalYearStartYear, ConfigurationList); + await provisioner.ensureField(Field_UseApprovalsEmailNotification, ConfigurationList); + await provisioner.ensureField(Field_UseApprovalsTeamsNotification, ConfigurationList); + } +} diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/Definition.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/Definition.ts new file mode 100644 index 000000000..14728c3b3 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/Definition.ts @@ -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(),], +}; diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/UpdateAllConfigurationListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/UpdateAllConfigurationListView.ts new file mode 100644 index 000000000..2875ba320 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/UpdateAllConfigurationListView.ts @@ -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); + } + +} + + + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ApproversList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ApproversList.ts new file mode 100644 index 000000000..6210ea753 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ApproversList.ts @@ -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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ConfigurationList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ConfigurationList.ts new file mode 100644 index 000000000..99d8f6d99 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/ConfigurationList.ts @@ -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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/EventsList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/EventsList.ts new file mode 100644 index 000000000..305bf0f51 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/EventsList.ts @@ -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: ` + + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinerValuesList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinerValuesList.ts new file mode 100644 index 000000000..0ddb95b3f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinerValuesList.ts @@ -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: ` + + + + + + + + + + + 1 + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinersList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinersList.ts new file mode 100644 index 000000000..532954adf --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/RefinersList.ts @@ -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: ` + + + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/index.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/index.ts new file mode 100644 index 000000000..2eeae69b2 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v2.0.0/schemaSnapshot/index.ts @@ -0,0 +1,6 @@ +export { ApproversList, IApproversListDefinition } from './ApproversList'; +export { ConfigurationList, Field_FiscalYearStartYear, Field_UseApprovalsEmailNotification, Field_UseApprovalsTeamsNotification, View_AllItems } from './ConfigurationList'; +export { EventsList, IEventsListDefinition } from './EventsList'; +export { RefinersList, IRefinersListDefinition } from './RefinersList'; +export { RefinerValuesList, IRefinerValuesListDefinition } from './RefinerValuesList'; + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddEnableOutlookColumnToConfigutationList.tsx b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddEnableOutlookColumnToConfigutationList.tsx new file mode 100644 index 000000000..e24cfa3d8 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddEnableOutlookColumnToConfigutationList.tsx @@ -0,0 +1,16 @@ +import { ElementProvisioner } from "common/sharepoint"; +import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +import {ConfigurationList,Field_UseAddToOutlook} from "./schemaSnapshot/index"; + +export class AddEnableOutlookColumnToConfigutationList + implements IROBCalendarUpgradeAction +{ + public get description(): string { + return `Adding fields to list '${ConfigurationList.title}'`; + } + + public async execute(): Promise { + const provisioner: ElementProvisioner = new ElementProvisioner(); + await provisioner.ensureField(Field_UseAddToOutlook, ConfigurationList); + } +} diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddTeamsGroupChatIdToEventsList.tsx b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddTeamsGroupChatIdToEventsList.tsx new file mode 100644 index 000000000..c3a2f17b3 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/AddTeamsGroupChatIdToEventsList.tsx @@ -0,0 +1,16 @@ +import { ElementProvisioner } from "common/sharepoint"; +import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +import {EventsList, Field_TeamsGroupChatId} from "./schemaSnapshot/index"; + +export class AddTeamsGroupChatIdToEventsList + implements IROBCalendarUpgradeAction +{ + public get description(): string { + return `Adding fields to list '${EventsList.title}'`; + } + + public async execute(): Promise { + const provisioner: ElementProvisioner = new ElementProvisioner(); + await provisioner.ensureField(Field_TeamsGroupChatId, EventsList); + } +} \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/CreateChannelsConfigurationsListAction.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/CreateChannelsConfigurationsListAction.ts new file mode 100644 index 000000000..9587d738f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/CreateChannelsConfigurationsListAction.ts @@ -0,0 +1,9 @@ +// import { CreateListUpgradeAction } from "common/sharepoint"; +// import { ChannelsConfigurationsList } from "./schemaSnapshot"; + +// export class CreateChannelsConfigurationsListAction extends CreateListUpgradeAction { +// constructor( +// ) { +// super(ChannelsConfigurationsList); +// } +// } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/Definition.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/Definition.ts new file mode 100644 index 000000000..38fab36c4 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/Definition.ts @@ -0,0 +1,19 @@ +import { IROBCalendarUpgrade } from "../IROBCalendarUpgrade"; +//import { UpdateAllChannelsConfigurationsListView } from "./UpdateAllChannelsConfigurationsListView"; +import { UpdateAllEventsListView } from "./UpdateAllEventsListView"; +//import { CreateChannelsConfigurationsListAction } from "./CreateChannelsConfigurationsListAction"; +import { AddTeamsGroupChatIdToEventsList } from "./AddTeamsGroupChatIdToEventsList"; +import { AddEnableOutlookColumnToConfigutationList } from "./AddEnableOutlookColumnToConfigutationList"; +import { UpdateAllConfigurationListView } from "./UpdateAllConfigurationListView"; + +export const Definition: IROBCalendarUpgrade = { + fromVersion: 2.0, + toVersion: 3.0, + actions: [ + // new CreateChannelsConfigurationsListAction(), + // new UpdateAllChannelsConfigurationsListView(), + new AddTeamsGroupChatIdToEventsList(), + new UpdateAllEventsListView(), + new AddEnableOutlookColumnToConfigutationList(), + new UpdateAllConfigurationListView()], +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllChannelsConfigurationsListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllChannelsConfigurationsListView.ts new file mode 100644 index 000000000..870503564 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllChannelsConfigurationsListView.ts @@ -0,0 +1,15 @@ +// import { AddOrUpdateViewUpgradeAction } from "common/sharepoint"; +// import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +// import {ChannelsConfigurationsList, View_AllChannelsConfigurations} from "./schemaSnapshot/index"; + +// export class UpdateAllChannelsConfigurationsListView extends AddOrUpdateViewUpgradeAction implements IROBCalendarUpgradeAction { +// public readonly shared: boolean = false; + +// constructor() { +// super(ChannelsConfigurationsList, View_AllChannelsConfigurations); +// } + +// } + + + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllConfigurationListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllConfigurationListView.ts new file mode 100644 index 000000000..2875ba320 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllConfigurationListView.ts @@ -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); + } + +} + + + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllEventsListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllEventsListView.ts new file mode 100644 index 000000000..c0ed05348 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/UpdateAllEventsListView.ts @@ -0,0 +1,12 @@ +import { AddOrUpdateViewUpgradeAction } from "common/sharepoint"; +import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +import {EventsList, View_AllEvents } from "./schemaSnapshot/index"; + +export class UpdateAllEventsListView extends AddOrUpdateViewUpgradeAction implements IROBCalendarUpgradeAction { + public readonly shared: boolean = false; + + constructor() { + super(EventsList, View_AllEvents); + } + +} \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ApproversList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ApproversList.ts new file mode 100644 index 000000000..6210ea753 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ApproversList.ts @@ -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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ChannelsConfigurationsList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ChannelsConfigurationsList.ts new file mode 100644 index 000000000..9d5e04cb2 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ChannelsConfigurationsList.ts @@ -0,0 +1,78 @@ +// import { IListDefinition, FieldType, IViewDefinition, ITextFieldDefinition, includeStandardViewFields, ListTemplateType, RoleOperation, RoleType } from "common/sharepoint"; +// import { Defaults } from "schema/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' +// }; + +// export 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 +// }; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ConfigurationList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ConfigurationList.ts new file mode 100644 index 000000000..3ecf35bc7 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/ConfigurationList.ts @@ -0,0 +1,156 @@ +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 Field_UseAddToOutlook: IBooleanFieldDefinition = { + type: FieldType.Boolean, + name: 'UseAddToOutlook', + displayName: "Use Add To Outlook", + default: "Yes" +}; + +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, + Field_UseAddToOutlook + ) +}; + +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, + Field_UseAddToOutlook + ], + views: [View_AllItems], + view_AllItems: View_AllItems +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/EventsList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/EventsList.ts new file mode 100644 index 000000000..69ff8fb71 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/EventsList.ts @@ -0,0 +1,238 @@ +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 +}; + +export const Field_TeamsGroupChatId: ITextFieldDefinition = { + type: FieldType.Text, + name: 'TeamsGroupChatId', + displayName: 'Teams Group Chat Id', + required: false +}; + +export 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, + Field_TeamsGroupChatId + ), + // need to sort by ID ascending in order to ensure the series master is loaded before any exceptions to the series + query: ` + + + + ` +}; + +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, + Field_TeamsGroupChatId + ], + views: [ + View_AllEvents + ], + view_AllEvents: View_AllEvents +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinerValuesList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinerValuesList.ts new file mode 100644 index 000000000..0ddb95b3f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinerValuesList.ts @@ -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: ` + + + + + + + + + + + 1 + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinersList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinersList.ts new file mode 100644 index 000000000..532954adf --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/RefinersList.ts @@ -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: ` + + + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/index.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/index.ts new file mode 100644 index 000000000..ebe83bf83 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v3.0.0/schemaSnapshot/index.ts @@ -0,0 +1,6 @@ +export { ApproversList, IApproversListDefinition } from './ApproversList'; +export { ConfigurationList, Field_FiscalYearStartYear, Field_UseApprovalsEmailNotification, Field_UseApprovalsTeamsNotification,Field_UseAddToOutlook, View_AllItems } from './ConfigurationList'; +export { EventsList, IEventsListDefinition, Field_TeamsGroupChatId, View_AllEvents } from './EventsList'; +export { RefinersList, IRefinersListDefinition } from './RefinersList'; +export { RefinerValuesList, IRefinerValuesListDefinition } from './RefinerValuesList'; +//export { ChannelsConfigurationsList, IChannelsConfigurationsListDefinition, View_AllChannelsConfigurations } from './ChannelsConfigurationsList'; diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/AddListViewColumnToConfigutationList.tsx b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/AddListViewColumnToConfigutationList.tsx new file mode 100644 index 000000000..d6371bb4c --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/AddListViewColumnToConfigutationList.tsx @@ -0,0 +1,16 @@ +import { ElementProvisioner } from "common/sharepoint"; +import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +import {ConfigurationList, Field_ListViewColumn} from "./schemaSnapshot/index"; + +export class AddListViewColumnToConfigutationList + implements IROBCalendarUpgradeAction +{ + public get description(): string { + return `Adding fields to list '${ConfigurationList.title}'`; + } + + public async execute(): Promise { + const provisioner: ElementProvisioner = new ElementProvisioner(); + await provisioner.ensureField(Field_ListViewColumn, ConfigurationList); + } +} diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/Definition.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/Definition.ts new file mode 100644 index 000000000..60ddfab2d --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/Definition.ts @@ -0,0 +1,11 @@ +import { IROBCalendarUpgrade } from "../IROBCalendarUpgrade"; +import { AddListViewColumnToConfigutationList } from "./AddListViewColumnToConfigutationList"; +import { UpdateAllConfigurationListView } from "./UpdateAllConfigurationListView"; + +export const Definition: IROBCalendarUpgrade = { + fromVersion: 3.0, + toVersion: 4.0, + actions: [ + new AddListViewColumnToConfigutationList(), + new UpdateAllConfigurationListView()], +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/UpdateAllConfigurationListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/UpdateAllConfigurationListView.ts new file mode 100644 index 000000000..2875ba320 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/UpdateAllConfigurationListView.ts @@ -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); + } + +} + + + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ApproversList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ApproversList.ts new file mode 100644 index 000000000..6210ea753 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ApproversList.ts @@ -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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ConfigurationList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ConfigurationList.ts new file mode 100644 index 000000000..9a05263ac --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/ConfigurationList.ts @@ -0,0 +1,168 @@ +import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, INumberFieldDefinition, ListTemplateType, IChoiceFieldDefinition, IBooleanFieldDefinition, RoleOperation, RoleType } from "common/sharepoint"; +import { ListViewKeys, 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 Field_UseAddToOutlook: IBooleanFieldDefinition = { + type: FieldType.Boolean, + name: 'UseAddToOutlook', + displayName: "Use Add To Outlook", + default: "Yes" +}; + +export const Field_ListViewColumn: IChoiceFieldDefinition = { + type: FieldType.Choice, + name: 'ListViewColumn', + displayName: "List View Column", + choices: Object.keys(ListViewKeys), + default: ListViewKeys["displayName"], + multi: true +}; + +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, + Field_UseAddToOutlook, + Field_ListViewColumn + + ) +}; + +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, + Field_UseAddToOutlook, + Field_ListViewColumn + ], + views: [View_AllItems], + view_AllItems: View_AllItems +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/EventsList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/EventsList.ts new file mode 100644 index 000000000..69ff8fb71 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/EventsList.ts @@ -0,0 +1,238 @@ +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 +}; + +export const Field_TeamsGroupChatId: ITextFieldDefinition = { + type: FieldType.Text, + name: 'TeamsGroupChatId', + displayName: 'Teams Group Chat Id', + required: false +}; + +export 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, + Field_TeamsGroupChatId + ), + // need to sort by ID ascending in order to ensure the series master is loaded before any exceptions to the series + query: ` + + + + ` +}; + +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, + Field_TeamsGroupChatId + ], + views: [ + View_AllEvents + ], + view_AllEvents: View_AllEvents +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinerValuesList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinerValuesList.ts new file mode 100644 index 000000000..0ddb95b3f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinerValuesList.ts @@ -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: ` + + + + + + + + + + + 1 + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinersList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinersList.ts new file mode 100644 index 000000000..532954adf --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/RefinersList.ts @@ -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: ` + + + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/index.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/index.ts new file mode 100644 index 000000000..be1640ef6 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v4.0.0/schemaSnapshot/index.ts @@ -0,0 +1,6 @@ +export { ApproversList, IApproversListDefinition } from './ApproversList'; +export { ConfigurationList, Field_FiscalYearStartYear, Field_UseApprovalsEmailNotification, Field_UseApprovalsTeamsNotification,Field_UseAddToOutlook, Field_ListViewColumn, View_AllItems } from './ConfigurationList'; +export { EventsList, IEventsListDefinition, Field_TeamsGroupChatId, View_AllEvents } from './EventsList'; +export { RefinersList, IRefinersListDefinition } from './RefinersList'; +export { RefinerValuesList, IRefinerValuesListDefinition } from './RefinerValuesList'; +//export { ChannelsConfigurationsList, IChannelsConfigurationsListDefinition, View_AllChannelsConfigurations } from './ChannelsConfigurationsList'; diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/AddTemplateViewColumnToConfigutationList.tsx b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/AddTemplateViewColumnToConfigutationList.tsx new file mode 100644 index 000000000..1e253ba22 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/AddTemplateViewColumnToConfigutationList.tsx @@ -0,0 +1,16 @@ +import { ElementProvisioner } from "common/sharepoint"; +import { IROBCalendarUpgradeAction } from "../IROBCalendarUpgradeAction"; +import {ConfigurationList, Field_TemplateView} from "./schemaSnapshot/index"; + +export class AddTemplateViewColumnToConfigutationList + implements IROBCalendarUpgradeAction +{ + public get description(): string { + return `Adding fields to list '${ConfigurationList.title}'`; + } + + public async execute(): Promise { + const provisioner: ElementProvisioner = new ElementProvisioner(); + await provisioner.ensureField(Field_TemplateView, ConfigurationList); + } +} diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/Definition.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/Definition.ts new file mode 100644 index 000000000..bae80b276 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/Definition.ts @@ -0,0 +1,11 @@ +import { IROBCalendarUpgrade } from "../IROBCalendarUpgrade"; +import { AddTemplateViewColumnToConfigutationList } from "./AddTemplateViewColumnToConfigutationList"; +import { UpdateAllConfigurationListView } from "./UpdateAllConfigurationListView"; + +export const Definition: IROBCalendarUpgrade = { + fromVersion: 4.0, + toVersion: 5.0, + actions: [ + new AddTemplateViewColumnToConfigutationList(), + new UpdateAllConfigurationListView()], +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/UpdateAllConfigurationListView.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/UpdateAllConfigurationListView.ts new file mode 100644 index 000000000..2875ba320 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/UpdateAllConfigurationListView.ts @@ -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); + } + +} + + + diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ApproversList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ApproversList.ts new file mode 100644 index 000000000..6210ea753 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ApproversList.ts @@ -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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ConfigurationList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ConfigurationList.ts new file mode 100644 index 000000000..03e2267e2 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/ConfigurationList.ts @@ -0,0 +1,180 @@ +import { IListDefinition, FieldType, IViewDefinition, includeStandardViewFields, INumberFieldDefinition, ListTemplateType, IChoiceFieldDefinition, IBooleanFieldDefinition, RoleOperation, RoleType } from "common/sharepoint"; +import { ListViewKeys, ViewKeys, ViewYearFYKeys } from "model"; +import { TemplateViewKeys } from "model/TemplateViewKeys"; +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 Field_TemplateView: IChoiceFieldDefinition = { + type: FieldType.Choice, + name: 'TemplateView', + displayName: "Template View", + choices: Object.keys(TemplateViewKeys), + default: TemplateViewKeys["eventTitle"], + multi: true +}; + +export const Field_UseAddToOutlook: IBooleanFieldDefinition = { + type: FieldType.Boolean, + name: 'UseAddToOutlook', + displayName: "Use Add To Outlook", + default: "Yes" +}; + +export const Field_ListViewColumn: IChoiceFieldDefinition = { + type: FieldType.Choice, + name: 'ListViewColumn', + displayName: "List View Column", + choices: Object.keys(ListViewKeys), + default: ListViewKeys["displayName"], + multi: true +}; + +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, + Field_UseAddToOutlook, + Field_ListViewColumn, + Field_TemplateView + + ) +}; + +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, + Field_UseAddToOutlook, + Field_ListViewColumn, + Field_TemplateView + ], + views: [View_AllItems], + view_AllItems: View_AllItems +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/EventsList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/EventsList.ts new file mode 100644 index 000000000..69ff8fb71 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/EventsList.ts @@ -0,0 +1,238 @@ +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 +}; + +export const Field_TeamsGroupChatId: ITextFieldDefinition = { + type: FieldType.Text, + name: 'TeamsGroupChatId', + displayName: 'Teams Group Chat Id', + required: false +}; + +export 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, + Field_TeamsGroupChatId + ), + // need to sort by ID ascending in order to ensure the series master is loaded before any exceptions to the series + query: ` + + + + ` +}; + +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, + Field_TeamsGroupChatId + ], + views: [ + View_AllEvents + ], + view_AllEvents: View_AllEvents +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinerValuesList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinerValuesList.ts new file mode 100644 index 000000000..0ddb95b3f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinerValuesList.ts @@ -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: ` + + + + + + + + + + + 1 + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinersList.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinersList.ts new file mode 100644 index 000000000..532954adf --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/RefinersList.ts @@ -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: ` + + + + + ` +}; + +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 +}; \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/index.ts b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/index.ts new file mode 100644 index 000000000..f18bc553f --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/schema/upgrades/v5.0.0/schemaSnapshot/index.ts @@ -0,0 +1,6 @@ +export { ApproversList, IApproversListDefinition } from './ApproversList'; +export { ConfigurationList, Field_FiscalYearStartYear, Field_UseApprovalsEmailNotification, Field_UseApprovalsTeamsNotification,Field_UseAddToOutlook, Field_ListViewColumn, Field_TemplateView, View_AllItems } from './ConfigurationList'; +export { EventsList, IEventsListDefinition, Field_TeamsGroupChatId, View_AllEvents } from './EventsList'; +export { RefinersList, IRefinersListDefinition } from './RefinersList'; +export { RefinerValuesList, IRefinerValuesListDefinition } from './RefinerValuesList'; +//export { ChannelsConfigurationsList, IChannelsConfigurationsListDefinition, View_AllChannelsConfigurations } from './ChannelsConfigurationsList'; diff --git a/samples/react-rhythm-of-business-calendar/src/services/configuration/ConfigurationLoader.ts b/samples/react-rhythm-of-business-calendar/src/services/configuration/ConfigurationLoader.ts index f25701209..b56672c41 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/configuration/ConfigurationLoader.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/configuration/ConfigurationLoader.ts @@ -1,8 +1,9 @@ import { ErrorHandler } from "common"; import { PagedViewLoader, IListItemResult, SPField, buildLiveList, IUpdateListItem, ErrorDiagnosis } from "common/sharepoint"; import { ILiveUpdateService, ISharePointService, ITimeZoneService } from "common/services"; -import { ViewKeys } from "model"; +import { ViewKeys, ViewYearFYKeys, ListViewKeys } from "model"; import { Configuration, ConfigurationList } from 'schema'; +import { TemplateViewKeys } from "model/TemplateViewKeys"; interface IConfigurationListItemResult extends IListItemResult { readonly SchemaVersion: SPField.Query_Number; @@ -14,6 +15,12 @@ interface IConfigurationListItemResult extends IListItemResult { readonly QuarterViewGroupByRefinerId: SPField.Query_Number; readonly UseApprovals: SPField.Query_Boolean; readonly AllowConfidentialEvents: SPField.Query_Boolean; + readonly UseApprovalsTeamsNotification: SPField.Query_Boolean; + readonly UseApprovalsEmailNotification: SPField.Query_Boolean; + readonly FiscalYearStartYear: SPField.Query_Choice; + readonly ListViewColumn: SPField.Query_ChoiceMulti; + readonly UseAddToOutlook: SPField.Query_Boolean; + readonly TemplateView: SPField.Query_ChoiceMulti; } interface IConfigurationUpdateListItem extends IUpdateListItem { @@ -27,6 +34,12 @@ interface IConfigurationUpdateListItem extends IUpdateListItem { readonly QuarterViewGroupByRefinerId: SPField.Update_Number; readonly UseApprovals: SPField.Update_Boolean; readonly AllowConfidentialEvents: SPField.Update_Boolean; + readonly UseApprovalsTeamsNotification: SPField.Update_Boolean; + readonly UseApprovalsEmailNotification: SPField.Update_Boolean; + readonly FiscalYearStartYear: SPField.Update_Choice; + readonly ListViewColumn: SPField.Update_ChoiceMulti; + readonly UseAddToOutlook: SPField.Update_Boolean; + readonly TemplateView: SPField.Update_ChoiceMulti; } const toConfiguration = async (row: IConfigurationListItemResult, config: Configuration): Promise => { @@ -40,6 +53,12 @@ const toConfiguration = async (row: IConfigurationListItemResult, config: Config config.quarterViewGroupByRefinerId = SPField.fromInt(row, 'QuarterViewGroupByRefinerId', undefined); config.useApprovals = SPField.fromYesNo(row, 'UseApprovals', false); config.allowConfidentialEvents = SPField.fromYesNo(row, 'AllowConfidentialEvents', false); + config.useApprovalsTeamsNotification = SPField.fromYesNo(row, 'UseApprovalsTeamsNotification', false); + config.useApprovalsEmailNotification = SPField.fromYesNo(row, 'UseApprovalsEmailNotification', false); + config.fiscalYearStartYear = row.FiscalYearStartYear as ViewYearFYKeys || ViewYearFYKeys["Next Year"]; + config.listViewColumn = row.ListViewColumn as ListViewKeys[] || [ListViewKeys["displayName"]]; + config.useAddToOutlook = SPField.fromYesNo(row, 'UseAddToOutlook', false); + config.templateView = row.TemplateView as TemplateViewKeys[] || [TemplateViewKeys["eventTitle"]]; }; const toUpdateListItem = (config: Configuration): IConfigurationUpdateListItem => { @@ -55,7 +74,13 @@ const toUpdateListItem = (config: Configuration): IConfigurationUpdateListItem = RefinerRailInitiallyExpanded: config.refinerRailInitiallyExpanded, QuarterViewGroupByRefinerId: config.quarterViewGroupByRefinerId || 0, UseApprovals: config.useApprovals, - AllowConfidentialEvents: config.allowConfidentialEvents + AllowConfidentialEvents: config.allowConfidentialEvents, + UseApprovalsTeamsNotification: config.useApprovalsTeamsNotification, + UseApprovalsEmailNotification: config.useApprovalsEmailNotification, + FiscalYearStartYear : config.fiscalYearStartYear, + ListViewColumn: SPField.toChoiceMulti(config.listViewColumn as any[]), + UseAddToOutlook: config.useAddToOutlook, + TemplateView: SPField.toChoiceMulti(config.templateView as any[]), }, // 1.1 fields ...(config.schemaVersion >= 1.1 && { diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/ChannelsConfigurationsLoader.ts b/samples/react-rhythm-of-business-calendar/src/services/events/ChannelsConfigurationsLoader.ts new file mode 100644 index 000000000..6464ffa13 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/src/services/events/ChannelsConfigurationsLoader.ts @@ -0,0 +1,57 @@ +// import { ErrorHandler } from "common"; +// import { PagedViewLoader, IListItemResult, SPField, IUpdateListItem, ErrorDiagnosis } from "common/sharepoint"; +// import { ISharePointService, ILiveUpdateService, ITimeZoneService } from "common/services"; +// import {ChannelsConfigurations } from "model"; +// import { IRhythmOfBusinessCalendarSchema } from "schema"; +// import { RefinerValueLoader } from "./RefinerValueLoader"; +// import { decode } from "he"; + +// interface IChannelsConfigurationsListItemResult extends IListItemResult { +// ChannelName: SPField.Query_Text; +// TeamsId: SPField.Query_TextMultiLine; +// ChannelId: SPField.Query_TextMultiLine; +// TeamsName:SPField.Query_Text; +// ActualChannelName:SPField.Query_Text; +// } + +// interface IChannelsConfigurationsUpdateListItem extends IUpdateListItem { +// ChannelName: SPField.Update_Text; +// TeamsId: SPField.Update_TextMultiLine; +// ChannelId: SPField.Update_TextMultiLine; +// TeamsName: SPField.Update_Text; +// ActualChannelName:SPField.Update_Text; + +// } + +// const toChannelsConfigurations = async (row: IChannelsConfigurationsListItemResult, channelsConfigurations: ChannelsConfigurations, refinerValueLoader: RefinerValueLoader): Promise => { +// channelsConfigurations.title = row.Title; +// channelsConfigurations.channelName = decode(row.ChannelName); +// channelsConfigurations.teamsId = decode(row.TeamsId); +// channelsConfigurations.channelId= decode(row.ChannelId); +// channelsConfigurations.teamsName = decode(row.TeamsName); +// channelsConfigurations.actualChannelName = decode(row.ActualChannelName); +// }; + +// const toUpdateListItem = (channelsConfigurations: ChannelsConfigurations): IChannelsConfigurationsUpdateListItem => { +// return { +// Title: channelsConfigurations.title, +// ChannelName: channelsConfigurations.channelName, +// TeamsId: channelsConfigurations.teamsId, +// ChannelId: channelsConfigurations.channelId, +// TeamsName : channelsConfigurations.teamsName, +// ActualChannelName: channelsConfigurations.actualChannelName +// }; +// }; + +// export class ChannelsConfigurationsLoader extends PagedViewLoader { +// constructor(schema: IRhythmOfBusinessCalendarSchema, timezones: ITimeZoneService, spo: ISharePointService, liveUpdate: ILiveUpdateService, private readonly _refinerValueLoader: RefinerValueLoader) { +// super({ ctor: ChannelsConfigurations, view: schema.channelsConfigurationsList.view_AllChannelsConfigurations, timezones, spo, liveUpdate, fastLoad: { useCache: true } }); + +// _refinerValueLoader.registerDependency(this); +// } + +// // protected readonly extractReferencedUsers = (approver: Approvers) => [...approver.users]; +// protected readonly toEntity = (row: IChannelsConfigurationsListItemResult, entity: ChannelsConfigurations) => toChannelsConfigurations(row, entity, this._refinerValueLoader); +// protected readonly updateListItem = toUpdateListItem; +// protected readonly diagnosePersistError = (error: any) => ErrorHandler.is_412_PRECONDITION_FAILED(error) ? ErrorDiagnosis.Propogate : ErrorDiagnosis.Critical; +// } \ No newline at end of file diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/EventLoader.ts b/samples/react-rhythm-of-business-calendar/src/services/events/EventLoader.ts index 91237335e..a8e09ad89 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/events/EventLoader.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/events/EventLoader.ts @@ -1,8 +1,19 @@ import { decode } from "he"; import { ErrorHandler } from "common"; -import { PagedViewLoader, IListItemResult, SPField, IUpdateListItem, ErrorDiagnosis } from "common/sharepoint"; -import { ISharePointService, ILiveUpdateService, ITimeZoneService, ITimeZone } from "common/services"; -import { Event, EventModerationStatus, ReadonlyEventMap } from "model"; +import { + PagedViewLoader, + IListItemResult, + SPField, + IUpdateListItem, + ErrorDiagnosis, +} from "common/sharepoint"; +import { + ISharePointService, + ILiveUpdateService, + ITimeZoneService, + ITimeZone, +} from "common/services"; +import { Event, EventModerationStatus, ReadonlyEventMap, RecurUntilType } from "model"; import { IRhythmOfBusinessCalendarSchema } from "schema"; import { RefinerValueLoader } from "./RefinerValueLoader"; import { RecurrenceData } from "./RecurrenceData"; @@ -28,6 +39,7 @@ interface IEventListItemResult extends IListItemResult { RecurrenceID: SPField.Query_DateTime; UID: SPField.Query_Guid; Duration: SPField.Query_Integer; + TeamsGroupChatId: SPField.Query_Text; } interface IEventUpdateListItem extends IUpdateListItem { @@ -51,47 +63,83 @@ interface IEventUpdateListItem extends IUpdateListItem { RecurrenceID: SPField.Update_DateTime; UID: SPField.Update_Guid; Duration: SPField.Update_Integer; + TeamsGroupChatId: SPField.Update_Text } -const toEvent = async (row: IEventListItemResult, event: Event, siteTimeZone: ITimeZone, refinerValueLoader: RefinerValueLoader, eventsById: ReadonlyEventMap): Promise => { +const toEvent = async ( + row: IEventListItemResult, + event: Event, + siteTimeZone: ITimeZone, + refinerValueLoader: RefinerValueLoader, + eventsById: ReadonlyEventMap +): Promise => { event.title = decode(row.Title); event.description = decode(row.Description); event.location = decode(row.Location); event.contacts = SPField.toUsers(row.Contacts); - event.refinerValues.set(await SPField.fromLookupMultiAsync(row.RefinerValues, refinerValueLoader.getById)); + event.refinerValues.set( + await SPField.fromLookupMultiAsync( + row.RefinerValues, + refinerValueLoader.getById + ) + ); - const isAllDay = SPField.fromYesNo(row, 'fAllDayEvent'); - const start = SPField.fromDateTime(row, 'EventDate', siteTimeZone); - const end = SPField.fromDateTime(row, 'EndDate', siteTimeZone); + const isAllDay = SPField.fromYesNo(row, "fAllDayEvent"); + const start = isAllDay + ? SPField.fromDate(row, "EventDate", siteTimeZone) + : SPField.fromDateTime(row, "EventDate", siteTimeZone); + const end = isAllDay + ? SPField.fromDate(row, "EndDate", siteTimeZone) + : SPField.fromDateTime(row, "EndDate", siteTimeZone); if (isAllDay) { - start.utc().tz(siteTimeZone.momentId, true); - end.utc().tz(siteTimeZone.momentId, true); + start.tz(siteTimeZone.momentId, true); + end.tz(siteTimeZone.momentId, true); } event.start = start; event.end = end; event.isAllDay = isAllDay; - event.isConfidential = SPField.fromYesNo(row, 'IsConfidential'); + event.isConfidential = SPField.fromYesNo(row, "IsConfidential"); event.restrictedToAccounts = SPField.toUsers(row.RestrictedToAccounts); - event.moderationStatus = EventModerationStatus.fromName(row.ModerationStatus); + event.moderationStatus = EventModerationStatus.fromName( + row.ModerationStatus + ); event.moderator = SPField.toUser(row.Moderator); - event.moderationTimestamp = SPField.fromDateTime(row, 'ModerationTimestamp', siteTimeZone); + event.moderationTimestamp = SPField.fromDateTime( + row, + "ModerationTimestamp", + siteTimeZone + ); event.moderationMessage = decode(row.ModerationMessage); - event.isRecurring = SPField.fromRecurrence(row, 'fRecurrence'); - event.recurrenceUID = SPField.fromGuid(row, 'UID'); + event.isRecurring = SPField.fromRecurrence(row, "fRecurrence"); + event.recurrenceUID = SPField.fromGuid(row, "UID"); if (event.isRecurring) { - const seriesMasterId = SPField.fromInteger(row, 'MasterSeriesItemID'); - if (seriesMasterId) { // this is an exception occurrence to the series + const seriesMasterId = SPField.fromInteger(row, "MasterSeriesItemID"); + if (seriesMasterId) { + // this is an exception occurrence to the series event.seriesMaster.set(eventsById.get(seriesMasterId)); - event.recurrenceExceptionInstanceDate = SPField.fromDateTime(row, 'RecurrenceID', siteTimeZone); - event.recurrenceInstanceCancelled = (SPField.fromInteger(row, 'EventType') === 3); - } else { // this is the series master - const duration = SPField.fromInteger(row, 'Duration'); - event.end = event.start.clone().add(duration, 'seconds'); - event.recurrence = RecurrenceData.deserialize(decode(row.RecurrenceData || '')); + event.recurrenceExceptionInstanceDate = SPField.fromDateTime( + row, + "RecurrenceID", + siteTimeZone + ); + event.recurrenceInstanceCancelled = + SPField.fromInteger(row, "EventType") === 3; + } else { + // this is the series master + const duration = SPField.fromInteger(row, "Duration"); + event.end = isAllDay ? event.start.clone().set({hours: 23, minutes: 59}) : event.start.clone().add(duration, "seconds"); + event.recurrence = RecurrenceData.deserialize( + decode(row.RecurrenceData || "") + ); + if (event.recurrence && event.recurrence.until && event.recurrence.until.date) { + event.recurrence.until.date = event.recurrence.until.date.tz(siteTimeZone.momentId); + } + console.log("event", event); } } + event.teamsGroupChatId = decode(row.TeamsGroupChatId); }; const getEventTypeValue = (event: Event) => { @@ -99,44 +147,97 @@ const getEventTypeValue = (event: Event) => { if (!event.isSeriesException) return 1; // 1 = series master if (!event.recurrenceInstanceCancelled) return 4; // 4 = this one occurrence of the series is an exception (date/time change) return 3; // 3 = cancelled this one occurrence of a series -} +}; -const toUpdateListItem = (event: Event, siteTimeZone: ITimeZone): IEventUpdateListItem => { - const { isNew, isAllDay, isRecurring, isSeriesMaster, isSeriesException } = event; +const toUpdateListItem = ( + event: Event, + siteTimeZone: ITimeZone +): IEventUpdateListItem => { + const { isNew, isAllDay, isRecurring, isSeriesMaster, isSeriesException } = + event; + if(isRecurring && event.recurrence.until.type === RecurUntilType.date){ + event.recurrence.until.date.set({hours:23,minutes:59}); + } return { Title: event.title, Description: event.description, Location: event.location, ContactsId: SPField.fromUsers(event.contacts), RefinerValuesId: SPField.toLookupMulti(event.refinerValues.get()), - EventDate: isAllDay ? SPField.toDateOnly(event.start) : SPField.toDateTime(event.start, siteTimeZone), - EndDate: isAllDay ? SPField.toDateOnly(event.end) : SPField.toDateTime(event.end, siteTimeZone), + EventDate: isAllDay + ? SPField.toDateOnly(event.start) + : SPField.toDateTime(event.start, siteTimeZone), + EndDate: isAllDay + ? SPField.toDateOnly(event.end) + : SPField.toDateTime(event.end, siteTimeZone), fAllDayEvent: event.isAllDay, IsConfidential: event.isConfidential, RestrictedToAccountsId: SPField.fromUsers(event.restrictedToAccounts), - ModerationStatus: event.moderationStatus?.name || EventModerationStatus.Pending.name, + ModerationStatus: + event.moderationStatus?.name || EventModerationStatus.Pending.name, ModeratorId: SPField.fromUser(event.moderator), - ModerationTimestamp: SPField.toDateTime(event.moderationTimestamp, siteTimeZone), + ModerationTimestamp: SPField.toDateTime( + event.moderationTimestamp, + siteTimeZone + ), ModerationMessage: event.moderationMessage, fRecurrence: SPField.tofRecurrence(isRecurring), EventType: getEventTypeValue(event), - RecurrenceData: isSeriesMaster ? RecurrenceData.serialize(event.recurrence) : undefined, - MasterSeriesItemID: isSeriesException ? event.seriesMaster.get()?.id : undefined, - RecurrenceID: isSeriesException ? SPField.toDateTime(event.recurrenceExceptionInstanceDate, siteTimeZone) : undefined, + RecurrenceData: isSeriesMaster + ? RecurrenceData.serialize(event.recurrence) + : undefined, + MasterSeriesItemID: isSeriesException + ? event.seriesMaster.get()?.id + : undefined, + RecurrenceID: isSeriesException + ? SPField.toDateTime( + event.recurrenceExceptionInstanceDate, + siteTimeZone + ) + : undefined, UID: isRecurring && isNew ? event.recurrenceUID?.toString() : undefined, - Duration: event.duration.asSeconds() + Duration: event.duration.asSeconds(), + TeamsGroupChatId: event.teamsGroupChatId }; }; export class EventLoader extends PagedViewLoader { - constructor(schema: IRhythmOfBusinessCalendarSchema, timezones: ITimeZoneService, spo: ISharePointService, liveUpdate: ILiveUpdateService, private readonly _refinerValueLoader: RefinerValueLoader) { - super({ ctor: Event, view: schema.eventsList.view_AllEvents, timezones, spo, liveUpdate, fastLoad: { useCache: true } }); + constructor( + schema: IRhythmOfBusinessCalendarSchema, + timezones: ITimeZoneService, + spo: ISharePointService, + liveUpdate: ILiveUpdateService, + private readonly _refinerValueLoader: RefinerValueLoader + ) { + super({ + ctor: Event, + view: schema.eventsList.view_AllEvents, + timezones, + spo, + liveUpdate, + fastLoad: { useCache: true }, + }); this.registerDependency(_refinerValueLoader); } - protected readonly extractReferencedUsers = (event: Event) => [...event.contacts, ...event.restrictedToAccounts, event.moderator]; - protected readonly toEntity = (row: IEventListItemResult, event: Event) => toEvent(row, event, this.timezones.siteTimeZone, this._refinerValueLoader, this._entitiesById); - protected readonly updateListItem = (event: Event) => toUpdateListItem(event, this.timezones.siteTimeZone); - protected readonly diagnosePersistError = (error: any) => ErrorHandler.is_412_PRECONDITION_FAILED(error) ? ErrorDiagnosis.Propogate : ErrorDiagnosis.Critical; -} \ No newline at end of file + protected readonly extractReferencedUsers = (event: Event) => [ + ...event.contacts, + ...event.restrictedToAccounts, + event.moderator, + ]; + protected readonly toEntity = (row: IEventListItemResult, event: Event) => + toEvent( + row, + event, + this.timezones.siteTimeZone, + this._refinerValueLoader, + this._entitiesById + ); + protected readonly updateListItem = (event: Event) => + toUpdateListItem(event, this.timezones.siteTimeZone); + protected readonly diagnosePersistError = (error: any) => + ErrorHandler.is_412_PRECONDITION_FAILED(error) + ? ErrorDiagnosis.Propogate + : ErrorDiagnosis.Critical; +} diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/EventsServiceDescriptor.ts b/samples/react-rhythm-of-business-calendar/src/services/events/EventsServiceDescriptor.ts index 3112728de..866de17f0 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/events/EventsServiceDescriptor.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/events/EventsServiceDescriptor.ts @@ -14,17 +14,24 @@ export interface IEventsService extends IService { readonly refinerValuesAsync: IAsyncData; readonly approversAsync: IAsyncData; + //readonly channelsConfigurationsAsync: IAsyncData; track(event: Event): void; track(refiner: Refiner): void; track(refinerValue: RefinerValue): void; track(approver: Approvers): void; + // track(channelsConfiguration: ChannelsConfigurations): void; persist(): Promise; - addToOutlook(event: Event): void; + addToOutlook(event: Event, isDifferenceInTimezoneVal?: boolean): void; createEventDeepLink(event: Event): string; + // sendDetailinPost(event: Event, itemUrl: string, channelId: string, groupId: string): Promise; + sendNotification_EventApproved(chatId: { chat_Id: string, rxUser: any }[], event: any, itemUrl: string): any; + sendNotification_EventRejected(chatId: { chat_Id: string, rxUser: any }[], event: any, itemUrl: string): any; + getTeamsNameById(teamsId: string): Promise; + getActualChannelNameById(teamsId: string, channelId: string): Promise; } export type EventsServiceProp = { diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/OnlineEventsService.ts b/samples/react-rhythm-of-business-calendar/src/services/events/OnlineEventsService.ts index 68e8a37e0..0a40a70bf 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/events/OnlineEventsService.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/events/OnlineEventsService.ts @@ -1,9 +1,10 @@ import { sp } from '@pnp/sp'; import { IEmailProperties } from '@pnp/sp/sputilities'; import { IMicrosoftTeams } from '@microsoft/sp-webpart-base'; +import { MSGraphClientV3, SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { format } from '@fluentui/react'; import { Color, Entity, humanizeFixedList, IAsyncData, multifilter, now, User } from 'common'; -import { ServiceContext, DeveloperService, DeveloperServiceProp, SharePointServiceProp, SharePointService, ISharePointService, TimeZoneServiceProp, TimeZoneService, ITimeZoneService, LiveUpdateServiceProp, LiveUpdateService, ILiveUpdateService, DirectoryService, DirectoryServiceProp, IDirectoryService, TeamsJs } from 'common/services'; +import { ServiceContext, DeveloperService, DeveloperServiceProp, SharePointServiceProp, SharePointService, ISharePointService, TimeZoneServiceProp, TimeZoneService, ITimeZoneService, LiveUpdateServiceProp, LiveUpdateService, ILiveUpdateService, DirectoryService, DirectoryServiceProp, IDirectoryService, SpfxContext, TeamsJs } from 'common/services'; import { RoleType } from 'common/sharepoint'; import { Approvers, Event, EventModerationStatus, humanizeDateRange, humanizeRecurrencePattern, ReadonlyEventMap, Refiner, RefinerValue } from 'model'; import { ConfigurationService, IConfigurationService, ConfigurationServiceProp } from '../configuration'; @@ -16,21 +17,26 @@ import { ApproversLoader } from './ApproversLoader'; import { Defaults } from './Defaults'; import { AppName, ApprovalEmails as strings } from 'ComponentStrings'; +//import { ChannelsConfigurationsLoader } from './ChannelsConfigurationsLoader'; export class OnlineEventsService implements IEventsService { + private readonly _context: SpfxContext; private readonly _teams: IMicrosoftTeams; private readonly _timezones: ITimeZoneService; private readonly _liveUpdate: ILiveUpdateService; private readonly _directory: IDirectoryService; private readonly _spo: ISharePointService; private readonly _configurations: IConfigurationService; + private _msGraphClient: MSGraphClientV3; private _eventLoader: EventLoader; private _refinerLoader: RefinerLoader; private _refinerValueLoader: RefinerValueLoader; private _approversLoader: ApproversLoader; + //private _channelsConfigurationsLoader: ChannelsConfigurationsLoader; constructor({ + [SpfxContext]: context, [TeamsJs]: teams, [DeveloperService]: dev, [TimeZoneService]: timezones, @@ -39,6 +45,7 @@ export class OnlineEventsService implements IEventsService { [SharePointService]: spo, [ConfigurationService]: configurations }: ServiceContext) { + this._context = context; this._teams = teams; this._timezones = timezones; this._liveUpdate = liveUpdate; @@ -59,7 +66,9 @@ export class OnlineEventsService implements IEventsService { this._refinerValueLoader = new RefinerValueLoader(schema, this._timezones, this._spo, this._liveUpdate, this._refinerLoader); this._eventLoader = new EventLoader(schema, this._timezones, this._spo, this._liveUpdate, this._refinerValueLoader); this._approversLoader = new ApproversLoader(schema, this._timezones, this._spo, this._liveUpdate, this._refinerValueLoader); + // this._channelsConfigurationsLoader = new ChannelsConfigurationsLoader(schema, this._timezones, this._spo, this._liveUpdate, this._refinerValueLoader); } + this._msGraphClient = await this._context.msGraphClientFactory.getClient("3"); } public get eventsAsync(): IAsyncData { @@ -82,11 +91,16 @@ export class OnlineEventsService implements IEventsService { return this._approversLoader.asyncData(); } + // public get channelsConfigurationsAsync(): IAsyncData { + // return this._channelsConfigurationsLoader.asyncData(); + // } + public track(event: Event): void; public track(refiner: Refiner): void; public track(refinerValue: RefinerValue): void; public track(approvers: Approvers): void; - public track(entity: Event | Refiner | RefinerValue | Approvers): void { + // public track(channelsConfiguration: ChannelsConfigurations): void; + public track(entity: Event | Refiner | RefinerValue | Approvers ): void { if (entity instanceof Event) { this._eventLoader.track(entity); } else if (entity instanceof Refiner) { @@ -96,21 +110,25 @@ export class OnlineEventsService implements IEventsService { this._refinerValueLoader.track(entity); } else if (entity instanceof Approvers) { this._approversLoader.track(entity); - } + } + // else if (entity instanceof ChannelsConfigurations) { + // this._channelsConfigurationsLoader.track(entity); + // } } public async persist(): Promise { await this._refinerLoader.persist(); await this._refinerValueLoader.persist(); await this._approversLoader.persist(); + // await this._channelsConfigurationsLoader.persist(); await this._eventLoader.persist(); await this._handleRestrictedPermissionsEvents(); await this._handleEventApprovals(); } - public addToOutlook(event: Event): void { + public addToOutlook(event: Event, timeZoneDiff?: any): void { const builder = new iCalendarFileBuilder(); - const ics = builder.build(event); + const ics = builder.build(event, timeZoneDiff); const filename = event.title; const blob = new Blob([ics], { type: "text/html" }); @@ -197,26 +215,616 @@ export class OnlineEventsService implements IEventsService { } } + public getTeamsNameById = async (teamsId: string) => { + const resultGraph = await this._msGraphClient.api(`teams/${teamsId}`).version("v1.0").select('displayName').get(); + return resultGraph.displayName; + } + + public getActualChannelNameById = async (teamsId: string, channelId: string) => { + const resultGraph = await this._msGraphClient.api(`teams/${teamsId}/channels/${channelId}`).version("v1.0").select('displayName').get(); + return resultGraph.displayName; + } + + private getUserId = async (userEmail: string) => { + let userObject: { displayName: string, id: any } = null; + const accountName = `'i:0#.f|membership|${userEmail}'`; + const url = `${this._context.pageContext.web.absoluteUrl}/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v=${encodeURIComponent(accountName)}`; + await this._context.spHttpClient.get(url, SPHttpClient.configurations.v1) + .then(async (response: SPHttpClientResponse) => { + if (response.ok) { + await response.json().then((data) => { + console.log("data response", data); + const selectedItem = data.UserProfileProperties.find((item: { Key: string; })=> item.Key === "msOnline-ObjectId"); + userObject = { + displayName: data.DisplayName, + id: selectedItem?selectedItem.Value:null + }; + }); + } + else { + console.error(`Failed to load user profile. Error: ${response.statusText}`); + } + }).catch((error: any) => { + console.error(`Error: ${error}`); + }); + return userObject; + } + + private createUsersGroupChat = async (requesterId: string, rxUsers: any[], eventDisplayName: string) => { + const groupUsers = []; + let temprxUsers: any[] = []; + if (rxUsers && rxUsers.length > 0) { + rxUsers.filter((userItem) => { + var i = temprxUsers.findIndex(x => (x.id === userItem.id)); + if (i <= -1) { + temprxUsers.push(userItem); + } + return null; + }); + } + const isRequesterIdPresent = temprxUsers.some(user => user.id === requesterId); + if (!isRequesterIdPresent) { + groupUsers.push({ + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "user@odata.bind": `https://graph.microsoft.com/beta/users('${requesterId}')` + }); + } + + if(temprxUsers && temprxUsers.length > 0) + temprxUsers.map(user => { + groupUsers.push({ + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "user@odata.bind": `https://graph.microsoft.com/beta/users('${user.id}')` + }) + }); + + const body: any = { + "chatType": "group", + "topic": eventDisplayName, + "members": groupUsers + }; + const resultGraph = await this._msGraphClient.api(`chats`).version("beta").post(body); + return { + chat_Id: resultGraph.id, + rxUser: temprxUsers + }; + } + + private sendMessage = async (chatId: { chat_Id: string, rxUser: any }[], event: any, itemUrl: string) => { + const { displayName, start, end, isAllDay, location, description, isRecurring, recurrence } = event; + const dateAndTimeString = isRecurring ? humanizeRecurrencePattern(start, recurrence) : humanizeDateRange(start, end, isAllDay); + const strippedHtmlDescription = description.replace(/<[^>]+>/g, ''); + const imageDataUrl="data:image/jpeg;base64,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"; + const adaptiveCard = { + "type": "AdaptiveCard", + "body": [ + isRecurring && { + type: 'Image', + url: imageDataUrl, + altText: 'Description of the image', + "size": "Small", + "width": "15px", + "height": "15px" + }, + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": strings.RequestEmail.Subject + }, + { + "type": "TextBlock", + "text": "An event requiring your approval has been submitted to the Rhythm of Business Calendar.", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Event Details :", + "wrap": true, + "weight": "Bolder" + }, + { + "type": "FactSet", + "spacing": "large", + "facts": [ + { + "title": "Event :", + "value": displayName + }, + { + "title": "Location :", + "value": location + }, + { + "title": "Date and Time :", + "value": dateAndTimeString + }, + { + "title": "Description :", + "value": strippedHtmlDescription, + "wrap": true + } + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Please approve or decline this event", + "url": itemUrl + } + ] + } + ].filter(Boolean), + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" + }; + + await Promise.all( + chatId.map(async (cidInFocus: { chat_Id: string, rxUser: any[] }) => { + const groupUsers: any[] = []; + if(cidInFocus.rxUser.length > 0) + cidInFocus.rxUser.map((user, index) => { + groupUsers.push({ + "id": index, //To get id dynamicaly + "mentionText": user.displayName, + "mentioned": { + "user": { + "displayName": user.displayName, + "id": user.id, + "userIdentityType": "aadUser" + } + } + }) + }); + const body = { + "body": { + "contentType": 'html', + "content": `Hi ${cidInFocus.rxUser.map((user, index) => `${user.displayName}`).join(", ")}` //Mention approvers + }, + "mentions": groupUsers, + "attachments": [ + { + "id": "74d20c7f34aa4a7fb74e2b30004247c5", + "contentType": "application/vnd.microsoft.card.adaptive", + "content": JSON.stringify(adaptiveCard) + } + ] + + }; + await this._msGraphClient.api(`chats/${cidInFocus.chat_Id}/messages`).version("beta").post(body); + }) + ); + } + + public sendNotification_EventApproved = async (chatId: { chat_Id: string, rxUser: any }[], event: any, itemUrl: string) => { + const { moderator, moderationMessage } = event; + // const dateAndTimeString = isRecurring ? humanizeRecurrencePattern(start, recurrence) : humanizeDateRange(start, end, isAllDay); + // const strippedHtmlDescription = description.replace(/<[^>]+>/g, ''); + + const { author } = event; + const approvers = (await this._approversLoader.all()).filter(Entity.NotDeletedFilter); + const selectedValuesByRefiner = event.valuesByRefiner(); + const approversForEvent = approvers.filter(a => Approvers.appliesTo(a, selectedValuesByRefiner)); + const approversUsers = approversForEvent.flatMap(a => a.users); + + const toAddresses = [ + ...approversUsers + ].map(user => user.email); + + const ccAddresses = [ + author + ].map(user => user.email); + + const rxEmailColl: string[] = [...toAddresses, ...ccAddresses]; + let rxUserIDColl: any[] = []; + let filteredrxUserIDColl: any[] = []; + + const currentUser: { displayName: string, id: any } = await this.getUserId(moderator.email); + await Promise.all(rxEmailColl.map(async (email: string) => { + rxUserIDColl.push(await this.getUserId(email)); + })); + if(rxUserIDColl && rxUserIDColl.length>0) + rxUserIDColl = rxUserIDColl.filter(rxUserID => rxUserID.id !== currentUser.id); + + if (rxUserIDColl && rxUserIDColl.length > 0) { + rxUserIDColl.filter((userItem) => { + var i = filteredrxUserIDColl.findIndex(x => (x.id === userItem.id)); + if (i <= -1) { + filteredrxUserIDColl.push(userItem); + } + return null; + }); + } + const adaptiveCard = { + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": strings.ApprovedEmail.Subject + }, + { + "type": "TextBlock", + "text": "This event was approved by " + event.moderator.title + " on " + event.moderationTimestamp.format('DD/MM/yyyy'), + "wrap": true + }, + { + "type": "FactSet", + "spacing": "large", + "facts": [ + { + "title": "Comments :", + "value": moderationMessage + }, + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.OpenUrl", + "title": "See Details", + "float": "right", + "url": itemUrl + } + ] + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" + }; + + await Promise.all( + chatId.map(async (cidInFocus: { chat_Id: string, rxUser: any[] }) => { + const groupUsers: any[] = []; + if(filteredrxUserIDColl && filteredrxUserIDColl.length > 0) + filteredrxUserIDColl.map((user, index) => { + groupUsers.push({ + "id": index, + "mentionText": user.displayName, + "mentioned": { + "user": { + "displayName": user.displayName, + "id": user.id, + "userIdentityType": "aadUser" + } + } + }) + }); + const body = { + "body": { + "contentType": 'html', + "content":`Hi ${filteredrxUserIDColl.map((user, index) => `${user.displayName}`).join(", ")}` + }, + "mentions": groupUsers, + "attachments": [ + { + "id": "74d20c7f34aa4a7fb74e2b30004247c5", + "contentType": "application/vnd.microsoft.card.adaptive", + "content": JSON.stringify(adaptiveCard) + } + ] + }; + try{ + await this._msGraphClient.api(`chats/${cidInFocus.chat_Id}/messages`).version("beta").post(body); + }catch (e) { + // send an email + console.log(e); + } + + }) + ); + } + + public sendNotification_EventRejected = async (chatId: { chat_Id: string, rxUser: any }[], event: any, itemUrl: string) => { + const { start, end, isAllDay, description, isRecurring, recurrence, moderator, moderationMessage } = event; + // const dateAndTimeString = isRecurring ? humanizeRecurrencePattern(start, recurrence) : humanizeDateRange(start, end, isAllDay); + // const strippedHtmlDescription = description.replace(/<[^>]+>/g, ''); + + const { author } = event; + const approvers = (await this._approversLoader.all()).filter(Entity.NotDeletedFilter); + const selectedValuesByRefiner = event.valuesByRefiner(); + const approversForEvent = approvers.filter(a => Approvers.appliesTo(a, selectedValuesByRefiner)); + const approversUsers = approversForEvent.flatMap(a => a.users); + + const toAddresses = [ + ...approversUsers + ].map(user => user.email); + + const ccAddresses = [ + author + ].map(user => user.email); + + const rxEmailColl: string[] = [...toAddresses, ...ccAddresses]; + let rxUserIDColl: any[] = []; + let filteredrxUserIDColl: any[] = []; + const currentUser: { displayName: string, id: any } = await this.getUserId(moderator.email); + await Promise.all(rxEmailColl.map(async (email: string) => { + rxUserIDColl.push(await this.getUserId(email)); + })); + if(rxUserIDColl && rxUserIDColl.length>0) + rxUserIDColl = rxUserIDColl.filter(rxUserID => rxUserID.id !== currentUser.id); + + // + if (rxUserIDColl && rxUserIDColl.length > 0) { + rxUserIDColl.filter((userItem) => { + var i = filteredrxUserIDColl.findIndex(x => (x.id === userItem.id)); + if (i <= -1) { + filteredrxUserIDColl.push(userItem); + } + return null; + }); + } + // + const adaptiveCard = { + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": strings.RejectedEmail.Subject + }, + { + "type": "TextBlock", + "text": "This event was declined by " + event.moderator.title + " on " + event.moderationTimestamp.format('DD/MM/yyyy') , + "wrap": true + }, + { + "type": "FactSet", + "spacing": "large", + "facts": [ + { + "title": "Reason :", + "value": moderationMessage + }, + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.OpenUrl", + "title": "See Details", + "url": itemUrl + } + ] + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" + }; + + await Promise.all( + chatId.map(async (cidInFocus: { chat_Id: string, rxUser: any[] }) => { + const groupUsers: any[] = []; + if(filteredrxUserIDColl && filteredrxUserIDColl.length > 0) + filteredrxUserIDColl.map((user, index) => { + groupUsers.push({ + "id": index, + "mentionText": user.displayName, + "mentioned": { + "user": { + "displayName": user.displayName, + "id": user.id, + "userIdentityType": "aadUser" + } + } + }) + }); + + const body = { + "body": { + "contentType": 'html', + "content":`Hi ${filteredrxUserIDColl.map((user, index) => `${user.displayName}`).join(", ")}` + }, + "mentions": groupUsers, + "attachments": [ + { + "id": "74d20c7f34aa4a7fb74e2b30004247c5", + "contentType": "application/vnd.microsoft.card.adaptive", + "content": JSON.stringify(adaptiveCard) + } + ] + + }; + try{ + await this._msGraphClient.api(`chats/${cidInFocus.chat_Id}/messages`).version("beta").post(body); + }catch (e) { + console.log(e); + } + }) + ); + } + + // public sendDetailinPost = async (event: Event, itemUrl: string, channelId: string, groupId: string) => { + // const { displayName, start, end, isAllDay, location, description, isRecurring, recurrence } = event; + // const dateAndTimeString = isRecurring ? humanizeRecurrencePattern(start, recurrence) : humanizeDateRange(start, end, isAllDay); + + // const strippedHtmlDescription = description.replace(/<[^>]+>/g, ''); + // const imageDataUrl="data:image/jpeg;base64,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"; + // //if (true) { + // //const { channelId, groupId } = this._teams.context; + // const adaptiveCard = { + // "type": "AdaptiveCard", + // "body": [ + // isRecurring && { + // type: 'Image', + // url: imageDataUrl, + // altText: 'Description of the image', + // "size": "Small", + // "width": "15px", + // "height": "15px" + // }, + // { + // "type": "TextBlock", + // "size": "Medium", + // "weight": "Bolder", + // "text": displayName + // }, + // { + // "type": "TextBlock", + // "text": "This event is shared from the Rhythm of Business Calendar.", + // "wrap": true + // }, + // { + // "type": "TextBlock", + // "text": "Event Details :", + // "wrap": true, + // "weight": "Bolder" + // }, + // { + // "type": "FactSet", + // "spacing": "large", + // "facts": [ + // { + // "title": "Event:", + // "value": displayName + // }, + // { + // "title": "Location:", + // "value": location + // }, + // { + // "title": "Date and Time:", + // "value": dateAndTimeString + // }, + // { + // "title": "Description:", + // "value": strippedHtmlDescription, + // "wrap": true + // } + // ] + // }, + // { + // "type": "ActionSet", + // "actions": [ + // { + // "type": "Action.OpenUrl", + // "title": "Click to navigate to this event", + // "url": itemUrl + // } + // ] + // } + // ].filter(Boolean), + // "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + // "version": "1.5" + // }; + // const body = { + // "body": { + // "contentType": 'html', + // "content": "" + // }, + // // "body": { + // // "contentType": 'html', + // // "content": `Hi MOT` + // // }, + // // "mentions": [ + // // { + // // "id": 0, + // // "mentionText": "MOT", + // // "mentioned": { + // // "channel": { + // // "displayName": mot, + // // "id": cidInFocus.rxUser.id, + // // "userIdentityType": "aadUser" + // // } + // // } + // // } + // // ], + // "attachments": [ + // { + // "id": "74d20c7f34aa4a7fb74e2b30004247c5", + // "contentType": "application/vnd.microsoft.card.adaptive", + // "content": JSON.stringify(adaptiveCard) + // } + // ] + // }; + // await this._msGraphClient.api(`teams/${groupId}/channels/${channelId}/messages`).version("beta").post(body); + // } + // } + + private constructUserTeamsMessage = async (event: Event, refiners: Refiner[], approvers: Approvers[]) => { + const { author } = event; + + const selectedValuesByRefiner = event.valuesByRefiner(); + const approversForEvent = approvers.filter(a => Approvers.appliesTo(a, selectedValuesByRefiner)); + const approversUsers = approversForEvent.flatMap(a => a.users); + + if(approversUsers && approversUsers.length > 0){ + const toAddresses = [ + ...approversUsers + ].map(user => user.email); + + const ccAddresses = [ + author + ].map(user => user.email); + + const itemUrl = this.createEventDeepLink(event); + + const rxEmailColl: string[] = [...toAddresses, ...ccAddresses]; + let rxUserIDColl: any[] = []; + const currentUser: { displayName: string, id: any } = await this.getUserId(author.email); + await Promise.all(rxEmailColl.map(async (email: string) => { + rxUserIDColl.push(await this.getUserId(email)); + })); + if(rxUserIDColl && rxUserIDColl.length>0) + rxUserIDColl = rxUserIDColl.filter(rxUserID => rxUserID.id !== currentUser.id); + const chatID: { chat_Id: string, rxUser: any }[] = []; + const eventDisplayName = "Approval for - " + event.displayName; + chatID.push(await this.createUsersGroupChat(currentUser.id, rxUserIDColl, eventDisplayName)); + event.teamsGroupChatId = chatID[0].chat_Id.replace("@", "#"); + await this._eventLoader.persist(); + await this.sendMessage(chatID, event, itemUrl); + } + } + private async _handleEventApprovals(): Promise { const events = multifilter(this._eventLoader.entitiesWithChanges, Entity.NotDeletedFilter, e => e.hasSnapshot); const refiners = (await this._refinerLoader.all()).filter(Entity.NotDeletedFilter); const approvers = (await this._approversLoader.all()).filter(Entity.NotDeletedFilter); + const isTeamsNotification = this._configurations.active.useApprovalsTeamsNotification; + const isEmailNotification = this._configurations.active.useApprovalsEmailNotification; for (const event of events) { - const { isPendingApproval, isRejected } = event; + const { isPendingApproval, isRejected, isApproved } = event; const isNew = event.snapshotValue('isNew'); const moderationStatusChanged = event.hasChanges('moderationStatus'); try { if (isNew && isPendingApproval) { const email = this._constructEmail_EventApprovalRequest(event, refiners, approvers); + try{ + if(isTeamsNotification){ + await this.constructUserTeamsMessage(event, refiners, approvers); + } + } + catch (ex) { + console.error('Failed to send teams event approval post', ex); + } + try{ + if(isEmailNotification){ + await sp.utility.sendEmail(email); + } + }catch (ex) { + console.error('Failed to send event approval e-mail', ex); + } + + } else if (moderationStatusChanged && isRejected && isEmailNotification) { + const email = this._constructEmail_EventRejected(event, refiners, approvers); await sp.utility.sendEmail(email); - } else if (moderationStatusChanged && isRejected) { - const email = this._constructEmail_EventRejected(event, refiners); + } + else if (moderationStatusChanged && isApproved && isEmailNotification) { + const email = this._constructEmail_EventApproved(event, refiners, approvers); await sp.utility.sendEmail(email); } } catch (ex) { - console.error('Failed to send event approval e-mail', ex); + console.error('Failed to send event approval e-mail or teams notification', ex); } } } @@ -254,13 +862,55 @@ export class OnlineEventsService implements IEventsService { }; } - private _constructEmail_EventRejected(event: Event, refiners: Refiner[]): IEmailProperties { + private _constructEmail_EventApproved(event: Event, refiners: Refiner[], approvers: Approvers[]): IEmailProperties { const { author, moderator, moderationMessage } = event; + const selectedValuesByRefiner = event.valuesByRefiner(); + const approversForEvent = approvers.filter(a => Approvers.appliesTo(a, selectedValuesByRefiner)); + const approversUsers = approversForEvent.flatMap(a => a.users); + const toAddresses = [ author ].map(user => user.email); + const ccAddresses = [ + ...approversUsers + ].map(user => user.email); + + const itemUrl = this.createEventDeepLink(event); + + const eventDetailsHtml = this._constructEventDetailsHtml(event, refiners); + + const body = + `

${format(strings.ApprovedEmail.Intro, AppName, `${moderator.title}`)}

` + + `

Comments: ${moderationMessage || strings.ApprovedEmail.CommentGiven}

` + + `
` + + `

${strings.ApprovedEmail.EventLinkText}

` + + `

${strings.ApprovedEmail.EventDetailsHeading}

` + + eventDetailsHtml; + + return { + To: toAddresses, + CC: ccAddresses, + Subject: strings.ApprovedEmail.Subject, + Body: body + }; + } + + private _constructEmail_EventRejected(event: Event, refiners: Refiner[], approvers: Approvers[]): IEmailProperties { + const { author, moderator, moderationMessage } = event; + const selectedValuesByRefiner = event.valuesByRefiner(); + const approversForEvent = approvers.filter(a => Approvers.appliesTo(a, selectedValuesByRefiner)); + const approversUsers = approversForEvent.flatMap(a => a.users); + + const toAddresses = [ + author + ].map(user => user.email); + + const ccAddresses = [ + ...approversUsers + ].map(user => user.email); + const itemUrl = this.createEventDeepLink(event); const eventDetailsHtml = this._constructEventDetailsHtml(event, refiners); @@ -275,6 +925,7 @@ export class OnlineEventsService implements IEventsService { return { To: toAddresses, + CC: ccAddresses, Subject: strings.RejectedEmail.Subject, Body: body }; diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/RecurrenceData.ts b/samples/react-rhythm-of-business-calendar/src/services/events/RecurrenceData.ts index 4f50b6354..86f10adc9 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/events/RecurrenceData.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/events/RecurrenceData.ts @@ -356,7 +356,7 @@ export class RecurrenceData { } case RecurUntilType.date: { const windowEndNode = doc.createElement('windowEnd'); - windowEndNode.textContent = until.date?.clone().utc().format(); + windowEndNode.textContent = until.date?.clone().format(); ruleNode.appendChild(windowEndNode); break; } diff --git a/samples/react-rhythm-of-business-calendar/src/services/events/iCalendarFileBuilder.ts b/samples/react-rhythm-of-business-calendar/src/services/events/iCalendarFileBuilder.ts index 17ec8d0d9..b4c051d3a 100644 --- a/samples/react-rhythm-of-business-calendar/src/services/events/iCalendarFileBuilder.ts +++ b/samples/react-rhythm-of-business-calendar/src/services/events/iCalendarFileBuilder.ts @@ -2,25 +2,29 @@ import { Event, RecurDay, RecurPattern, RecurPatternOption, Recurrence, RecurUnt import { Cadence } from "model/Cadence"; export class iCalendarFileBuilder { - public build(event: Event) { - const { start, startTime, end, duration, isAllDay, isSeriesMaster, recurrence, location, title, description } = event; + public build(event: Event, timeZoneDiff?: any) { + const { start, startTime, end, duration, isAllDay, isSeriesMaster, recurrence, location, title, description, isRecurring } = event; let adjustedStart = start; let adjustedEnd = end; - // iCalendar format requires the DTSTART/DTEND date to be a valid meeting occurence date or it will not load in Outlook if (isSeriesMaster) { - const cadence = new Cadence(start, recurrence); + const isDifferenceInTimezone = timeZoneDiff; + const cadence = new Cadence(start, recurrence, isDifferenceInTimezone); const dates = cadence.generate({ start, end: start.clone().add(3, 'years') }); const firstDate = dates.next().value; if (firstDate) { adjustedStart = firstDate.clone().startOf('day').add(startTime); - adjustedEnd = firstDate.clone().startOf('day').add(duration); + adjustedEnd = adjustedStart.clone().add(duration, 'seconds'); } } const dtstart = adjustedStart.format(isAllDay ? "YYYYMMDD" : "YYYYMMDD[T]HHmmss"); - const dtend = adjustedEnd.format(isAllDay ? "YYYYMMDD" : "YYYYMMDD[T]HHmmss"); + let dtend = adjustedEnd.format(isAllDay ? "YYYYMMDD" : "YYYYMMDD[T]HHmmss"); + + if(isAllDay){ + dtend = end.clone().add(1, "day").format("YYYYMMDD"); + } const ics = `BEGIN:VCALENDAR PRODID:-//Microsoft Corporation//SharePoint MIMEDIR//EN @@ -30,7 +34,7 @@ BEGIN:VEVENT UID;TYPE=SharePoint:321 DTSTART:${dtstart} DTEND:${dtend} -${isSeriesMaster ? this._buildRRule(recurrence) : ''} +${isSeriesMaster ? this._buildRRule(recurrence, isAllDay, isRecurring) : ''} LOCATION;ENCODING=8BIT;CHARSET=utf-8:${location} TRANSP:OPAQUE SEQUENCE:1 @@ -44,7 +48,7 @@ END:VCALENDAR`; return ics; } - private _buildRRule(recurrence: Recurrence): string { + private _buildRRule(recurrence: Recurrence, isAllDay:boolean, isRecurring:boolean): string { const rrule = 'RRULE:' + [ this._freq(recurrence), this._interval(recurrence), @@ -53,7 +57,7 @@ END:VCALENDAR`; this._byMonth(recurrence), this._byMonthDay(recurrence), this._untilCount(recurrence), - this._untilDate(recurrence) + this._untilDate(recurrence, isAllDay, isRecurring) ].filter(Boolean).join(';'); return rrule; @@ -155,7 +159,7 @@ END:VCALENDAR`; return type === RecurUntilType.count ? "COUNT=" + count : ''; } - private _untilDate({ until: { type, date } }: Recurrence): string { - return type === RecurUntilType.date ? "UNTIL=" + date.clone().add(1, 'day').format('YYYYMMDD[T]HHmmss') : ''; + private _untilDate({ until: { type, date } }: Recurrence, isAllDay:boolean, isRecurring:boolean): string { + return type === RecurUntilType.date ? "UNTIL=" + (isAllDay && isRecurring? date.clone().set({hours: 0, minutes: 0, seconds: 0}).format('YYYYMMDD[T]HHmmss') : date.clone().format('YYYYMMDD[T]HHmmss')) : ''; } } \ No newline at end of file From 99d62786888db82bdcc20375c32edcb29538fd45 Mon Sep 17 00:00:00 2001 From: Mrigango Deb Date: Fri, 13 Sep 2024 20:37:35 +0530 Subject: [PATCH 2/5] pushing the enhancement versions --- .../react-rhythm-of-business-calendar/.nvmrc | 1 + .../README.md | 77 +- .../config/config.json | 33 +- .../config/package-solution.json | 22 +- .../package-lock.json | 119674 ++++++++------- .../package.json | 159 +- .../src/services/events/Defaults.ts | 2 +- ...thmOfBusinessCalendarWebPart.manifest.json | 6 +- 8 files changed, 65876 insertions(+), 54098 deletions(-) create mode 100644 samples/react-rhythm-of-business-calendar/.nvmrc diff --git a/samples/react-rhythm-of-business-calendar/.nvmrc b/samples/react-rhythm-of-business-calendar/.nvmrc new file mode 100644 index 000000000..dac255d28 --- /dev/null +++ b/samples/react-rhythm-of-business-calendar/.nvmrc @@ -0,0 +1 @@ +v16.15.1 diff --git a/samples/react-rhythm-of-business-calendar/README.md b/samples/react-rhythm-of-business-calendar/README.md index e9ff36aab..d34f7be7a 100644 --- a/samples/react-rhythm-of-business-calendar/README.md +++ b/samples/react-rhythm-of-business-calendar/README.md @@ -4,11 +4,31 @@ 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 + -Rhythm of Business (RoB) Calendar keeps you on top of your business goals by managing all team and organizational events seamlessly. Simplify and expedite the coordination and planning process for your team and subgroups with the help of color-coded events, approval workflow, refiners and confidential events. Ideal for Chiefs of Staff, Executive Assistants, or anyone who manages a team calendar, you can empower your teams by enabling better insights on your business goals and team events. +Rhythm of Business (RoB) Calendar keeps you on top of your business goals by managing all team and organizational events seamlessly. Simplify and expedite the coordination and planning process for your team and subgroups with the help of color-coded events, approval workflow, refiners and confidential events. Ideal for Chiefs of Staff, Executive Assistants, or anyone who manages a team calendar, you can empower your teams by enabling better insights on your business goals and team events. Month view ![Screenshot of month view](./assets/screenshot-month-view.png) @@ -21,16 +41,16 @@ 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 for more information on SPFx 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 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)]( "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- author [Mrigango Deb] ## 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`