From 4921e3b02816b7f0ec600d362bebe13cb22c2136 Mon Sep 17 00:00:00 2001 From: AriGunawan Date: Fri, 2 Dec 2022 01:59:43 +1000 Subject: [PATCH] Extract functions --- .../react-birthdays-per-month/.eslintrc.js | 2 +- .../config/package-solution.json | 6 +- .../react-birthdays-per-month/package.json | 2 + .../react-birthdays-per-month/pnpm-lock.yaml | 60 +++++++++--- .../src/models/BirthdaysInMonth.ts | 6 ++ .../src/models/User.ts | 8 ++ .../src/utils/SharePointService.ts | 90 ++++++++++++++++++ .../BirthdaysPerMonthWebPart.manifest.json | 7 +- .../BirthdaysPerMonthWebPart.ts | 23 +++-- .../components/BirthdaysPerMonth.tsx | 90 ++---------------- .../components/MonthSection.tsx | 91 +++++++++++++++++++ 11 files changed, 276 insertions(+), 109 deletions(-) create mode 100644 samples/react-birthdays-per-month/src/models/BirthdaysInMonth.ts create mode 100644 samples/react-birthdays-per-month/src/models/User.ts create mode 100644 samples/react-birthdays-per-month/src/utils/SharePointService.ts create mode 100644 samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/MonthSection.tsx diff --git a/samples/react-birthdays-per-month/.eslintrc.js b/samples/react-birthdays-per-month/.eslintrc.js index 6ebb2a10f..31364d534 100644 --- a/samples/react-birthdays-per-month/.eslintrc.js +++ b/samples/react-birthdays-per-month/.eslintrc.js @@ -79,7 +79,7 @@ module.exports = { // This rule should be suppressed only in very special cases such as JSON.stringify() // where the type really can be anything. Even if the type is flexible, another type // may be more appropriate such as "unknown", "{}", or "Record". - '@typescript-eslint/no-explicit-any': 1, + '@typescript-eslint/no-explicit-any': 0, // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() // handler. Thus wherever a Promise arises, the code must either append a catch handler, // or else return the object to a caller (who assumes this responsibility). Unterminated diff --git a/samples/react-birthdays-per-month/config/package-solution.json b/samples/react-birthdays-per-month/config/package-solution.json index 2b167122d..686edd49d 100644 --- a/samples/react-birthdays-per-month/config/package-solution.json +++ b/samples/react-birthdays-per-month/config/package-solution.json @@ -1,9 +1,9 @@ { "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", "solution": { - "name": "react-birthdays-per-month-client-side-solution", + "name": "React Birthdays Per Month", "id": "e4cd97a3-4515-42cd-8a12-f765eaf60caa", - "version": "1.0.0.0", + "version": "1.1.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": true, "isDomainIsolated": false, @@ -35,6 +35,6 @@ ] }, "paths": { - "zippedPackage": "solution/react-birthdays-per-month.sppkg" + "zippedPackage": "solution/React Birthdays Per Month.sppkg" } } diff --git a/samples/react-birthdays-per-month/package.json b/samples/react-birthdays-per-month/package.json index 4c5301f4f..4820dca40 100644 --- a/samples/react-birthdays-per-month/package.json +++ b/samples/react-birthdays-per-month/package.json @@ -15,6 +15,8 @@ "@microsoft/sp-office-ui-fabric-core": "1.15.2", "@microsoft/sp-property-pane": "1.15.2", "@microsoft/sp-webpart-base": "1.15.2", + "@pnp/sp": "^3.8.0", + "date-fns": "^2.29.3", "office-ui-fabric-react": "7.185.7", "react": "16.13.1", "react-dom": "16.13.1", diff --git a/samples/react-birthdays-per-month/pnpm-lock.yaml b/samples/react-birthdays-per-month/pnpm-lock.yaml index 7337905a9..bb266d998 100644 --- a/samples/react-birthdays-per-month/pnpm-lock.yaml +++ b/samples/react-birthdays-per-month/pnpm-lock.yaml @@ -11,12 +11,14 @@ specifiers: '@microsoft/sp-office-ui-fabric-core': 1.15.2 '@microsoft/sp-property-pane': 1.15.2 '@microsoft/sp-webpart-base': 1.15.2 + '@pnp/sp': ^3.8.0 '@rushstack/eslint-config': 2.5.1 '@types/react': 16.9.51 '@types/react-dom': 16.9.8 '@types/webpack-env': ~1.15.2 ajv: ^6.12.5 autoprefixer: ^10.4.13 + date-fns: ^2.29.3 eslint-plugin-react-hooks: 4.3.0 gulp: 4.0.2 gulp-postcss: ^9.0.1 @@ -36,6 +38,8 @@ dependencies: '@microsoft/sp-office-ui-fabric-core': 1.15.2 '@microsoft/sp-property-pane': 1.15.2_7ombvvupg4tnmt4iqt5m47i6cu '@microsoft/sp-webpart-base': 1.15.2_7ombvvupg4tnmt4iqt5m47i6cu + '@pnp/sp': 3.8.0 + date-fns: 2.29.3 office-ui-fabric-react: 7.185.7_24igt2r6uynb67fv3burekl4py react: 16.13.1 react-dom: 16.13.1_react@16.13.1 @@ -76,7 +80,7 @@ packages: resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} engines: {node: '>=12.0.0'} dependencies: - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/core-asynciterator-polyfill/1.0.2: @@ -89,7 +93,7 @@ packages: engines: {node: '>=12.0.0'} dependencies: '@azure/abort-controller': 1.1.0 - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/core-http/1.2.6: @@ -107,7 +111,7 @@ packages: node-fetch: 2.6.7 process: 0.11.10 tough-cookie: 4.1.2 - tslib: 2.3.1 + tslib: 2.4.0 tunnel: 0.0.6 uuid: 8.3.2 xml2js: 0.4.23 @@ -123,7 +127,7 @@ packages: '@azure/core-http': 1.2.6 '@azure/core-tracing': 1.0.0-preview.11 events: 3.3.0 - tslib: 2.3.1 + tslib: 2.4.0 transitivePeerDependencies: - encoding dev: true @@ -132,7 +136,7 @@ packages: resolution: {integrity: sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==} engines: {node: '>=12.0.0'} dependencies: - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/core-tracing/1.0.0-preview.11: @@ -141,7 +145,7 @@ packages: dependencies: '@opencensus/web-types': 0.0.7 '@opentelemetry/api': 1.0.0-rc.0 - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/core-tracing/1.0.0-preview.7: @@ -158,7 +162,7 @@ packages: dependencies: '@opencensus/web-types': 0.0.7 '@opentelemetry/api': 0.10.2 - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/identity/1.0.3: @@ -182,7 +186,7 @@ packages: resolution: {integrity: sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==} engines: {node: '>=12.0.0'} dependencies: - tslib: 2.3.1 + tslib: 2.4.0 dev: true /@azure/msal-browser/2.22.0: @@ -208,7 +212,7 @@ packages: '@azure/logger': 1.0.3 '@opentelemetry/api': 0.10.2 events: 3.3.0 - tslib: 2.3.1 + tslib: 2.4.0 transitivePeerDependencies: - encoding dev: true @@ -225,7 +229,7 @@ packages: '@azure/logger': 1.0.3 '@opentelemetry/api': 0.10.2 events: 3.3.0 - tslib: 2.3.1 + tslib: 2.4.0 transitivePeerDependencies: - encoding dev: true @@ -1126,7 +1130,7 @@ packages: dependencies: '@azure/msal-browser': 2.22.0 '@babel/runtime': 7.20.0 - tslib: 2.3.1 + tslib: 2.4.0 dev: false /@microsoft/office-ui-fabric-react-bundle/1.15.2_7ombvvupg4tnmt4iqt5m47i6cu: @@ -1790,6 +1794,31 @@ packages: webpack-dev-server: 3.11.2_jtacdaqalodjhudd6qhs6dld5q dev: true + /@pnp/core/3.8.0: + resolution: {integrity: sha512-xPUohlAxbLate6yjR0+ajE29PJouhLqKW6iO37Xl3npT5IdC3C8k/W/RTvXEu1xegHAM+LVYQ3/OhKr+zxf6tw==} + engines: {node: '>=14.15.1'} + dependencies: + tslib: 2.4.0 + dev: false + + /@pnp/queryable/3.8.0: + resolution: {integrity: sha512-W2pX05FmIuFaQTNqcbrblgFjd9WVc0W4ElKAf5u4vMp0zlPidtsybkME6YC0o4k9+4LgTT9HZzCEY3ovuxhZ4g==} + engines: {node: '>=14.15.1'} + dependencies: + '@pnp/core': 3.8.0 + tslib: 2.4.0 + dev: false + + /@pnp/sp/3.8.0: + resolution: {integrity: sha512-8mfvaCODbdfud3lJVRWbVWIQ4SlbN3sNBhSEDq4fHnC/SNSn21V2ADOR//LqYcovdPQU3AKTlqrorItoSnK8/g==} + engines: {node: '>=14.15.1'} + requiresBuild: true + dependencies: + '@pnp/core': 3.8.0 + '@pnp/queryable': 3.8.0 + tslib: 2.4.0 + dev: false + /@pnpm/error/1.4.0: resolution: {integrity: sha512-vxkRrkneBPVmP23kyjnYwVOtipwlSl6UfL+h+Xa3TrABJTz5rYBXemlTsU5BzST8U4pD7YDkTb3SQu+MMuIDKA==} engines: {node: '>=10.16'} @@ -5490,6 +5519,11 @@ packages: whatwg-url: 7.1.0 dev: true + /date-fns/2.29.3: + resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} + engines: {node: '>=0.11'} + dev: false + /dateformat/1.0.12: resolution: {integrity: sha512-5sFRfAAmbHdIts+eKjR9kYJoF0ViCMVX9yqLu5A7S/v+nd077KgCITOMiirmyCBiZpKLDXbBOkYm6tu7rX/TKg==} hasBin: true @@ -14185,6 +14219,10 @@ packages: /tslib/2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + dev: false + + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} /tsutils/3.21.0_typescript@4.5.5: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} diff --git a/samples/react-birthdays-per-month/src/models/BirthdaysInMonth.ts b/samples/react-birthdays-per-month/src/models/BirthdaysInMonth.ts new file mode 100644 index 000000000..dfcffd265 --- /dev/null +++ b/samples/react-birthdays-per-month/src/models/BirthdaysInMonth.ts @@ -0,0 +1,6 @@ +import { User } from "./User"; + +export interface BirthdaysInMonth { + title: string; + users: Array; +} \ No newline at end of file diff --git a/samples/react-birthdays-per-month/src/models/User.ts b/samples/react-birthdays-per-month/src/models/User.ts new file mode 100644 index 000000000..99f59c6a8 --- /dev/null +++ b/samples/react-birthdays-per-month/src/models/User.ts @@ -0,0 +1,8 @@ +export interface User { + id: number; + name: string; + email: string; + month: string; + monthIndex: number; + date: number; +} \ No newline at end of file diff --git a/samples/react-birthdays-per-month/src/utils/SharePointService.ts b/samples/react-birthdays-per-month/src/utils/SharePointService.ts new file mode 100644 index 000000000..eb18caf78 --- /dev/null +++ b/samples/react-birthdays-per-month/src/utils/SharePointService.ts @@ -0,0 +1,90 @@ +import { SPFI } from "@pnp/sp"; +import "@pnp/sp/webs"; +import "@pnp/sp/lists"; +import "@pnp/sp/items"; +import { BirthdaysInMonth } from "../models/BirthdaysInMonth"; +import { User } from "../models/User"; +import { sortBy } from "@microsoft/sp-lodash-subset"; + +export class SharePointService { + private readonly _spfi: SPFI; + + constructor(spfi: SPFI) { + this._spfi = spfi; + } + + public async GetBirthdays(): Promise> { + const items: Array = await this._spfi.web.lists + .getByTitle("Birthdays") + .items.expand("Employee") + .select("ID,Month,Date,Employee/Title,Employee/UserName")(); + + return this.ProcessData(items); + } + + private GenerateMonths(): Array { + const months: Array = []; + for (let i = 0; i < 12; i++) { + const today = new Date(); + today.setMonth(today.getMonth() + i); + months.push({ + title: today.toLocaleString("en-AU", { month: "long" }), + users: [], + }); + } + + return months; + } + + private GetMonthIndex(month: string): number { + switch (month) { + case "January": + return 0; + case "February": + return 1; + case "March": + return 2; + case "April": + return 3; + case "May": + return 4; + case "June": + return 5; + case "July": + return 6; + case "August": + return 7; + case "September": + return 8; + case "October": + return 9; + case "November": + return 10; + case "December": + return 11; + } + } + + private ProcessData(items: any): Array { + const months = this.GenerateMonths(); + for (let i = 0; i < months.length; i++) { + const month = months[i]; + month.users = sortBy( + items + .filter((item: any) => item.Month === month.title) + .map( + (item: any): User => ({ + id: item.ID, + name: item.Employee.Title, + email: item.Employee.UserName, + date: item.Date, + month: item.Month, + monthIndex: this.GetMonthIndex(item.Month), + }) + ), + "date" + ); + } + return months; + } +} diff --git a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.manifest.json b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.manifest.json index 742dcc20d..9af520d6d 100644 --- a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.manifest.json +++ b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.manifest.json @@ -18,11 +18,10 @@ "preconfiguredEntries": [{ "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced "group": { "default": "Advanced" }, - "title": { "default": "BirthdaysPerMonth" }, - "description": { "default": "BirthdaysPerMonth description" }, - "officeFabricIconFontName": "Page", + "title": { "default": "Birthdays Per Month" }, + "description": { "default": "" }, + "officeFabricIconFontName": "BirthdayCake", "properties": { - "description": "BirthdaysPerMonth" } }] } diff --git a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.ts b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.ts index 68b283233..5b070068f 100644 --- a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.ts +++ b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/BirthdaysPerMonthWebPart.ts @@ -6,6 +6,7 @@ import { PropertyPaneTextField, } from "@microsoft/sp-property-pane"; import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base"; +import { spfi, SPFx, SPFI } from "@pnp/sp"; import * as strings from "BirthdaysPerMonthWebPartStrings"; import "../../../assets/dist/tailwind.css"; @@ -13,29 +14,31 @@ import { BirthdaysPerMonth, IBirthdaysPerMonthProps, } from "./components/BirthdaysPerMonth"; +import { BirthdaysInMonth } from "../../models/BirthdaysInMonth"; +import { SharePointService } from "../../utils/SharePointService"; export interface IBirthdaysPerMonthWebPartProps { description: string; } export default class BirthdaysPerMonthWebPart extends BaseClientSideWebPart { - private _isDarkTheme: boolean = false; - private _environmentMessage: string = ""; + private _spfi: SPFI; - public render(): void { + public async render(): Promise { + const sharePointService = new SharePointService(this._spfi); + const birthdays: Array = + await sharePointService.GetBirthdays(); + const elementProps: IBirthdaysPerMonthProps = { + data: birthdays, + }; const element: React.ReactElement = - React.createElement(BirthdaysPerMonth, { - description: this.properties.description, - isDarkTheme: this._isDarkTheme, - environmentMessage: this._environmentMessage, - hasTeamsContext: !!this.context.sdks.microsoftTeams, - userDisplayName: this.context.pageContext.user.displayName, - }); + React.createElement(BirthdaysPerMonth, elementProps); ReactDom.render(element, this.domElement); } protected onInit(): Promise { + this._spfi = spfi().using(SPFx(this.context)); return super.onInit(); } diff --git a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/BirthdaysPerMonth.tsx b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/BirthdaysPerMonth.tsx index e330d8f4b..6bfa467d1 100644 --- a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/BirthdaysPerMonth.tsx +++ b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/BirthdaysPerMonth.tsx @@ -1,87 +1,17 @@ import * as React from "react"; -import styles from "./BirthdaysPerMonth.module.scss"; -import { escape } from "@microsoft/sp-lodash-subset"; +import { BirthdaysInMonth } from "../../../models/BirthdaysInMonth"; +import { MonthSection } from "./MonthSection"; -interface IBirthdaysPerMonthProps {} +interface IBirthdaysPerMonthProps { + data: Array; +} -const BirthdaysPerMonth = () => { +const BirthdaysPerMonth = (props: IBirthdaysPerMonthProps): JSX.Element => { return ( -
-
- -

Well done, Ari!

-
-
-

Welcome to SharePoint Framework!

-

- The SharePoint Framework (SPFx) is a extensibility model for Microsoft - Viva, Microsoft Teams and SharePoint. It's the easiest way to - extend Microsoft 365 with automatic Single Sign On, automatic hosting - and industry standard tooling. -

-

Learn more about SPFx development:

- -
+
+ {props.data.map((month, index) => ( + + ))}
); }; diff --git a/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/MonthSection.tsx b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/MonthSection.tsx new file mode 100644 index 000000000..9ffa28fcd --- /dev/null +++ b/samples/react-birthdays-per-month/src/webparts/birthdaysPerMonth/components/MonthSection.tsx @@ -0,0 +1,91 @@ +import { addYears, format } from "date-fns/esm"; +import formatDistance from "date-fns/formatDistance"; +import set from "date-fns/set"; +import { Icon, IconButton } from "office-ui-fabric-react"; +import * as React from "react"; +import { BirthdaysInMonth } from "../../../models/BirthdaysInMonth"; + +interface IMonthSectionProps { + data: BirthdaysInMonth; + index: number; +} + +const MonthSection = (props: IMonthSectionProps): JSX.Element => { + const [isExpand, toggleExpand] = React.useReducer( + (previous) => !previous, + props.index === 0 || props.index === 1 + ); + + const generateYearLabel = (): string => { + if (props.data.title !== "January" || props.index === 0) return ""; + + const today = new Date(); + today.setFullYear(today.getFullYear() + 1); + return " " + today.getFullYear(); + }; + + const getDate = (date: number, month: number): Date => { + let birthdateDate = set(new Date(), { month, date }); + if (birthdateDate.getMonth() < new Date().getMonth()) { + birthdateDate = addYears(birthdateDate, 1); + } + return birthdateDate; + }; + + const generateDateLabel = (date: number, month: number): string => { + const birthdateDate = getDate(date, month); + return format(birthdateDate, "E, d MMM"); + }; + + const generateDistanceLabel = (date: number, month: number): string => { + const birthdateDate = getDate(date, month); + return formatDistance(birthdateDate, new Date(), { addSuffix: true }); + }; + + return ( +
+
+ +
+ {props.data.title} + {generateYearLabel()} +
+
+ {isExpand && ( +
+ {props.data.users.length === 0 &&
No birthdays this month.
} + {props.data.users.map((user) => ( +
+
+ +
+
+
+
{user.name}
+ { + event.stopPropagation(); + window.open(`mailto:${user.email}`); + }} + /> +
+
{generateDateLabel(user.date, user.monthIndex)}
+
{generateDistanceLabel(user.date, user.monthIndex)}
+
+
+ ))} +
+ )} +
+ ); +}; + +export { MonthSection };