create-spfx-tailwind-app 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,9 @@
8
8
  A modern CLI tool to scaffold **SharePoint Framework (SPFx)** projects
9
9
  with **Tailwind CSS pre-configured** --- no manual setup required.
10
10
 
11
+ - 🚀 **Updated to SPFx 1.24 & React 18**
12
+ - 🎨 **Tailwind CSS optimized for SharePoint Modern Pages**
13
+
11
14
  ------------------------------------------------------------------------
12
15
 
13
16
  ## ✨ Features
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "template-spfx",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "private": true,
5
5
  "engines": {
6
- "node": ">=22.14.0 < 23.0.0"
6
+ "node": ">=22.14.0"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "heft test --clean --production && heft package-solution --production",
@@ -17,36 +17,36 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@fluentui/react": "^8.106.4",
20
- "@microsoft/sp-component-base": "1.22.2",
21
- "@microsoft/sp-core-library": "1.22.2",
22
- "@microsoft/sp-lodash-subset": "1.22.2",
23
- "@microsoft/sp-office-ui-fabric-core": "1.22.2",
24
- "@microsoft/sp-property-pane": "1.22.2",
25
- "@microsoft/sp-webpart-base": "1.22.2",
20
+ "@microsoft/sp-component-base": "1.24.0-beta.5",
21
+ "@microsoft/sp-core-library": "1.24.0-beta.5",
22
+ "@microsoft/sp-lodash-subset": "1.24.0-beta.5",
23
+ "@microsoft/sp-office-ui-fabric-core": "1.24.0-beta.5",
24
+ "@microsoft/sp-property-pane": "1.24.0-beta.5",
25
+ "@microsoft/sp-webpart-base": "1.24.0-beta.5",
26
26
  "jquery": "^4.0.0",
27
- "react": "17.0.1",
28
- "react-dom": "17.0.1",
29
- "tslib": "2.3.1",
27
+ "react": "^18.3.1",
28
+ "react-dom": "^18.3.1",
29
+ "tslib": "^2.6.2",
30
30
  "uuid": "^14.0.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@microsoft/eslint-config-spfx": "1.22.2",
34
- "@microsoft/eslint-plugin-spfx": "1.22.2",
35
- "@microsoft/sp-module-interfaces": "1.22.2",
36
- "@microsoft/spfx-heft-plugins": "1.22.2",
37
- "@microsoft/spfx-web-build-rig": "1.22.2",
33
+ "@microsoft/eslint-config-spfx": "1.24.0-beta.5",
34
+ "@microsoft/eslint-plugin-spfx": "1.24.0-beta.5",
35
+ "@microsoft/sp-module-interfaces": "1.24.0-beta.5",
36
+ "@microsoft/spfx-heft-plugins": "1.24.0-beta.5",
37
+ "@microsoft/spfx-web-build-rig": "1.24.0-beta.5",
38
38
  "@rushstack/eslint-config": "4.5.2",
39
39
  "@rushstack/heft": "1.1.2",
40
40
  "@types/heft-jest": "1.0.2",
41
41
  "@types/jquery": "^4.0.0",
42
- "@types/react": "17.0.45",
43
- "@types/react-dom": "17.0.17",
42
+ "@types/react": "^18.3.18",
43
+ "@types/react-dom": "^18.3.5",
44
44
  "@types/webpack-env": "~1.15.2",
45
45
  "@typescript-eslint/parser": "8.46.2",
46
46
  "autoprefixer": "^10.4.27",
47
47
  "css-loader": "~7.1.2",
48
48
  "eslint": "8.57.1",
49
- "eslint-plugin-react-hooks": "4.3.0",
49
+ "eslint-plugin-react-hooks": "5.0.0",
50
50
  "postcss": "^8.5.8",
51
51
  "spfx-fast-serve": "^4.0.2",
52
52
  "tailwindcss": "^3.4.19",
@@ -56,6 +56,7 @@
56
56
  "@rushstack/heft": "1.1.2"
57
57
  },
58
58
  "resolutions": {
59
- "@types/react": "17.0.45"
59
+ "@types/react": "^18.3.18",
60
+ "@types/react-dom": "^18.3.5"
60
61
  }
61
62
  }
@@ -1,130 +1,176 @@
1
- import * as React from 'react';
2
- import * as ReactDom from 'react-dom';
3
- import { Version } from '@microsoft/sp-core-library';
1
+ import * as React from "react";
2
+ import * as ReactDom from "react-dom/client";
3
+ import { Version } from "@microsoft/sp-core-library";
4
4
  import {
5
5
  type IPropertyPaneConfiguration,
6
- PropertyPaneTextField
7
- } from '@microsoft/sp-property-pane';
8
- import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
9
- import { IReadonlyTheme } from '@microsoft/sp-component-base';
6
+ PropertyPaneTextField,
7
+ } from "@microsoft/sp-property-pane";
8
+ import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
9
+ import { IReadonlyTheme } from "@microsoft/sp-component-base";
10
10
  import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
11
11
  import "../../tailwind.generated.css";
12
- import * as strings from 'TemplateSpfxWebPartStrings';
12
+ import * as strings from "TemplateSpfxWebPartStrings";
13
13
  import __PROJECT_NAME__ from "./components/__PROJECT_NAME__";
14
- import { I__PROJECT_NAME__Props } from './components/I__PROJECT_NAME__Props';
14
+ import { I__PROJECT_NAME__Props } from "./components/I__PROJECT_NAME__Props";
15
15
  import $ from "jquery";
16
+
16
17
  export interface I__PROJECT_NAME__WebPartProps {
17
18
  description: string;
18
19
  }
19
20
 
21
+ interface ISharePointUserGroup {
22
+ Id: number;
23
+ Title: string;
24
+ }
25
+
26
+ interface IUserData {
27
+ id: number;
28
+ title: string;
29
+ email: string;
30
+ groups: ISharePointUserGroup[];
31
+ }
32
+
20
33
  export default class __PROJECT_NAME__WebPart extends BaseClientSideWebPart<I__PROJECT_NAME__WebPartProps> {
21
34
  private _isDarkTheme: boolean = false;
22
- private _environmentMessage: string = "";
23
- public getCurrentUserGroups(requester: SPHttpClient, siteUrl: string): any {
35
+ private _root: ReactDom.Root | null = null;
36
+ private _userData: IUserData = {
37
+ id: -1,
38
+ title: "",
39
+ email: "",
40
+ groups: [],
41
+ };
42
+ private _domObserver: MutationObserver | null = null;
43
+ private _injectedStyleElement: HTMLStyleElement | null = null;
44
+
45
+ public async getCurrentUserGroups(
46
+ requester: SPHttpClient,
47
+ siteUrl: string,
48
+ ): Promise<IUserData> {
24
49
  try {
25
- return requester
26
- .get(
27
- `${siteUrl}/_api/web/currentuser/?$select=Title,Email,LoginName,Id&$expand=groups`,
28
- SPHttpClient.configurations.v1,
29
- {
30
- headers: {
31
- Accept: "application/json;odata=verbose",
32
- "odata-version": "",
33
- },
50
+ const response: SPHttpClientResponse = await requester.get(
51
+ `${siteUrl}/_api/web/currentuser/?$select=Title,Email,LoginName,Id&$expand=groups`,
52
+ SPHttpClient.configurations.v1,
53
+ {
54
+ headers: {
55
+ Accept: "application/json;odata=verbose",
56
+ "odata-version": "",
34
57
  },
35
- )
36
- .then((response: SPHttpClientResponse) => {
37
- return response.json();
38
- })
39
- .then((json: any) => {
40
- return json;
41
- });
42
- } catch (error: any) {
43
- console.log(
44
- `Error occured in getEntity method in factory.ts. Error message: ${error.message}`,
58
+ },
59
+ );
60
+
61
+ if (!response.ok) {
62
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
63
+ }
64
+
65
+ const json = await response.json();
66
+ const user = json?.d;
67
+
68
+ return {
69
+ id: user?.Id ?? -1,
70
+ title: user?.Title ?? "",
71
+ email: this.context.pageContext.user.email || user?.Email || "",
72
+ groups: user?.Groups?.results ?? [],
73
+ };
74
+ } catch (error: unknown) {
75
+ console.error(
76
+ "[SPFx] Error fetching current user groups:",
77
+ error instanceof Error ? error.message : error,
45
78
  );
46
- throw error;
79
+ return {
80
+ id: -1,
81
+ title: "",
82
+ email: this.context.pageContext.user.email || "",
83
+ groups: [],
84
+ };
47
85
  }
48
86
  }
49
87
 
50
- public render(): void {
51
- this.getCurrentUserGroups(
88
+ protected async onInit(): Promise<void> {
89
+ await super.onInit();
90
+
91
+ // 1. Fetch user data before rendering component
92
+ this._userData = await this.getCurrentUserGroups(
52
93
  this.context.spHttpClient,
53
94
  this.context.pageContext.web.absoluteUrl,
54
- ).then((currentUser: any) => {
55
- const element: React.ReactElement<I__PROJECT_NAME__Props> =
56
- React.createElement(__PROJECT_NAME__, {
57
- newcontext: this.context,
58
- newsiteUrl: this.context.pageContext.web.absoluteUrl,
59
- newlistName: "",
60
- newspHttpClient: this.context.spHttpClient,
61
- newrelativeUrl: this.context.pageContext.web.serverRelativeUrl,
62
- newcurrentUserId: currentUser?.d?.Id ? currentUser.d.Id : -1,
63
- newcurrentUserName: currentUser?.d?.Title ? currentUser.d.Title : "",
64
- newgroups: currentUser?.d?.Groups?.results || [],
65
- newcurrentUserEmail: this.context.pageContext.user.email,
66
- });
67
-
68
- ReactDom.render(element, this.domElement);
69
- });
95
+ );
96
+
97
+ document.title = "MIS Repository";
98
+ $("body").addClass("ms-backgroundImage");
99
+
100
+ // 2. Inject CSS rules once
101
+ this._injectHideStyles();
102
+
103
+ // 3. Setup dynamic chrome hiding using MutationObserver
104
+ this._setupDomObserver();
70
105
  }
71
106
 
72
- protected onInit(): Promise<void> {
73
- return super.onInit().then(() => {
74
- document.title = "MIS Repository";
75
-
76
- // Add body class safely
77
- $("body").addClass("ms-backgroundImage");
78
-
79
- // Inject CSS (FIXED - all rules together)
80
- const style = document.createElement("style");
81
- style.innerHTML = `
82
- #spLeftNav,
83
- #SuiteNavWrapper,
84
- #CommentsWrapper,
85
- #sp-appBar,
86
- .headerRow-107,
87
- .commandBarButtonHeightAndColor,
88
- .fui-Toolbar,
89
- .simpleFooterContainer-196,
90
- .p_i-Dej_1x34n,
91
- div[data-automation-id="pageHeader"],
92
- div[data-automation-id="SiteHeader"],
93
- div[data-automation-id="MegaFooter"],
94
- div[data-automation-id="gradientBox"],
95
- div[data-automation-id="LeftNavGroups"] {
107
+ public render(): void {
108
+ const element: React.ReactElement<I__PROJECT_NAME__Props> =
109
+ React.createElement(__PROJECT_NAME__, {
110
+ newcontext: this.context,
111
+ newsiteUrl: this.context.pageContext.web.absoluteUrl,
112
+ newlistName: "",
113
+ newspHttpClient: this.context.spHttpClient,
114
+ newrelativeUrl: this.context.pageContext.web.serverRelativeUrl,
115
+ newcurrentUserId: this._userData.id,
116
+ newcurrentUserName: this._userData.title,
117
+ newgroups: this._userData.groups,
118
+ newcurrentUserEmail: this._userData.email,
119
+ });
120
+
121
+ // React 18 createRoot
122
+ if (!this._root) {
123
+ this._root = ReactDom.createRoot(this.domElement);
124
+ }
125
+ this._root.render(element);
126
+ }
127
+
128
+ private _injectHideStyles(): void {
129
+ const selectors = [
130
+ "#spLeftNav",
131
+ "#SuiteNavWrapper",
132
+ "#CommentsWrapper",
133
+ "#sp-appBar",
134
+ ".headerRow-107",
135
+ ".commandBarButtonHeightAndColor",
136
+ ".fui-Toolbar",
137
+ ".simpleFooterContainer-196",
138
+ ".p_i-Dej_1x34n",
139
+ 'div[data-automation-id="pageHeader"]',
140
+ 'div[data-automation-id="SiteHeader"]',
141
+ 'div[data-automation-id="MegaFooter"]',
142
+ 'div[data-automation-id="gradientBox"]',
143
+ 'div[data-automation-id="LeftNavGroups"]',
144
+ ];
145
+
146
+ this._injectedStyleElement = document.createElement("style");
147
+ this._injectedStyleElement.innerHTML = `
148
+ ${selectors.join(", ")} {
96
149
  display: none !important;
97
150
  }
98
151
  `;
99
- document.head.appendChild(style);
100
-
101
- // Delay + repeat (IMPORTANT for SPFx dynamic DOM)
102
- const hideElements = () => {
103
- $("div[data-automation-id='pageHeader']").hide();
104
- $("div[data-automation-id='SiteHeader']").hide();
105
- $("div[data-automation-id='SimpleFooter']").parent().hide();
106
- $("div[data-automation-id='SiteHeaderOverlay']").parent().hide();
107
- $("div[data-automation-id='MegaFooter']").hide();
108
- $("div[data-automation-id='gradientBox']").parent().hide();
109
- $("div[data-automation-id='LeftNavGroups']").parent().hide();
110
-
111
- $("#spLeftNav").hide();
112
- $("#SuiteNavWrapper").hide();
113
- $("#CommentsWrapper").hide();
114
- $("#sp-appBar").hide();
115
-
116
- $(".headerRow-107").hide();
117
- $(".commandBarButtonHeightAndColor").hide();
118
- $(".fui-Toolbar").hide();
119
- };
152
+ document.head.appendChild(this._injectedStyleElement);
153
+ }
120
154
 
121
- // Run multiple times (handles re-render)
122
- const interval = setInterval(hideElements, 1000);
155
+ private _setupDomObserver(): void {
156
+ const hideExtraNodes = (): void => {
157
+ $(
158
+ "div[data-automation-id='SimpleFooter'], div[data-automation-id='SiteHeaderOverlay'], div[data-automation-id='gradientBox'], div[data-automation-id='LeftNavGroups']",
159
+ )
160
+ .parent()
161
+ .hide();
162
+ };
163
+
164
+ hideExtraNodes();
123
165
 
124
- // Stop after 10 seconds (optional cleanup)
125
- setTimeout(() => clearInterval(interval), 10000);
166
+ // Observe changes instead of polling via setInterval
167
+ this._domObserver = new MutationObserver(() => {
168
+ hideExtraNodes();
169
+ });
126
170
 
127
-
171
+ this._domObserver.observe(document.body, {
172
+ childList: true,
173
+ subtree: true,
128
174
  });
129
175
  }
130
176
 
@@ -150,7 +196,25 @@ export default class __PROJECT_NAME__WebPart extends BaseClientSideWebPart<I__PR
150
196
  }
151
197
 
152
198
  protected onDispose(): void {
153
- ReactDom.unmountComponentAtNode(this.domElement);
199
+ // 1. Disconnect DOM observer
200
+ if (this._domObserver) {
201
+ this._domObserver.disconnect();
202
+ this._domObserver = null;
203
+ }
204
+
205
+ // 2. Remove dynamically injected CSS tag
206
+ if (this._injectedStyleElement && this._injectedStyleElement.parentNode) {
207
+ this._injectedStyleElement.parentNode.removeChild(
208
+ this._injectedStyleElement,
209
+ );
210
+ this._injectedStyleElement = null;
211
+ }
212
+
213
+ // 3. Unmount React 18 component tree
214
+ if (this._root) {
215
+ this._root.unmount();
216
+ this._root = null;
217
+ }
154
218
  }
155
219
 
156
220
  protected get dataVersion(): Version {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=22"
5
5
  },
6
- "version": "1.0.17",
6
+ "version": "1.0.19",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "echo \"Error: no test specified\" && exit 1"