tp-react-elements 3.0.15 → 3.0.16

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.
@@ -1,3 +1,3 @@
1
- var Month = {};
1
+ var InputRangeField = {};
2
2
 
3
- export { Month as __exports };
3
+ export { InputRangeField as __exports };
@@ -1,3 +1,3 @@
1
- var DateInput = {};
1
+ var Month = {};
2
2
 
3
- export { DateInput as __exports };
3
+ export { Month as __exports };
@@ -1,3 +1,3 @@
1
- var InputRangeField = {};
1
+ var DateInput = {};
2
2
 
3
- export { InputRangeField as __exports };
3
+ export { DateInput as __exports };
@@ -1,3 +1,3 @@
1
- var Calendar = {};
1
+ var DateRange = {};
2
2
 
3
- export { Calendar as __exports };
3
+ export { DateRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRangePicker = {};
1
+ var DefinedRange = {};
2
2
 
3
- export { DateRangePicker as __exports };
3
+ export { DefinedRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRange = {};
1
+ var DateRangePicker = {};
2
2
 
3
- export { DateRange as __exports };
3
+ export { DateRangePicker as __exports };
@@ -1,3 +1,3 @@
1
- var DefinedRange = {};
1
+ var Calendar = {};
2
2
 
3
- export { DefinedRange as __exports };
3
+ export { Calendar as __exports };
@@ -1,6 +1,13 @@
1
1
  import { jsx, Fragment } from 'react/jsx-runtime';
2
2
  import require$$0__default, { lazy, Suspense } from 'react';
3
3
  import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
4
+ import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
5
+ import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
6
+ import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
7
+ import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
8
+ import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
9
+ import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
10
+ import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
4
11
  import SingleSelect from '../FormComponents/Select/SingleSelect.esm.js';
5
12
  import MultiSelectAutocomplete from '../FormComponents/Select/MultiSelectAutocomplete.esm.js';
6
13
  import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
@@ -11,13 +18,6 @@ import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js'
11
18
  import RichTextEditorWrapper from '../FormComponents/RichTextEditor/RichTextEditorWrapper.esm.js';
12
19
  import SingleSelectSearchApi from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
13
20
  import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWrapper.esm.js';
14
- import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
15
- import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
16
- import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
17
- import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
18
- import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
19
- import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
20
- import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
21
21
 
22
22
  // Lazy-loaded heavy components
23
23
  // const RichTextEditorWrapper = lazy(() =>
@@ -39,10 +39,34 @@ const FormRadioGroup = ({ props, variant }) => {
39
39
  fontSize: 16, // controls the radio icon size
40
40
  },
41
41
  }), [checkedColor]);
42
+ const isPill = props.item.radioVariant === 'pill';
42
43
  /**
43
44
  * Renders a group of radio buttons with labels and validation support
44
45
  */
45
- return (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(FormControl, { component: "fieldset", children: jsx(Controller, { name: props.item.name, control: props.control, render: ({ field }) => (jsx(RadioGroup, { value: field.value || '', onChange: (e) => {
46
+ return (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(FormControl, { component: "fieldset", children: jsx(Controller, { name: props.item.name, control: props.control, render: ({ field }) => isPill ? (jsx("div", { role: "radiogroup", "aria-label": props.item.label, style: { display: 'flex', gap: 7, flexWrap: 'wrap' }, children: props.item?.settings?.map((option, i) => {
47
+ const on = (field.value || '') === option.name;
48
+ const long = String(option.label).length > 26;
49
+ return (jsx("button", { type: "button", role: "radio", "aria-checked": on, onClick: () => {
50
+ field.onChange(option.name); // update form state
51
+ props.item.onChangeFn && props.item.onChangeFn(option.name); // optional handler
52
+ }, style: {
53
+ flex: long ? '1 1 100%' : '1 1 auto',
54
+ width: long ? '100%' : undefined,
55
+ textAlign: long ? 'left' : 'center',
56
+ minWidth: long ? undefined : 120,
57
+ padding: long ? '10px 13px' : '9px 12px',
58
+ borderRadius: 7,
59
+ fontFamily: 'inherit',
60
+ fontSize: '12.5px',
61
+ fontWeight: on ? 700 : 600,
62
+ cursor: 'pointer',
63
+ whiteSpace: long ? 'normal' : 'nowrap',
64
+ lineHeight: 1.4,
65
+ border: `1px solid ${on ? checkedColor : '#DDE4ED'}`,
66
+ background: on ? `${checkedColor}14` : '#fff',
67
+ color: on ? checkedColor : '#42536B',
68
+ }, children: option.label }, i));
69
+ }) })) : (jsx(RadioGroup, { value: field.value || '', onChange: (e) => {
46
70
  field.onChange(e.target.value); // update form state
47
71
  props.item.onChangeFn && props.item.onChangeFn(e.target.value); // optional handler
48
72
  }, row: true, sx: RADIO_GROUP_SX, children: props.item?.settings?.map((option, i) => (jsx(FormControlLabel, { value: option.name, control: jsx(Radio, { sx: radioSx }), label: jsx(Typography, { fontSize: '11px', fontWeight: 'normal !important', children: option.label }) }, i))) })) }) }), jsx(FormBottomField, { ...props })] }));
@@ -1,3 +1,8 @@
1
+ export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
2
+ export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
3
+ export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
4
+ export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
5
+ export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
1
6
  export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
2
7
  export { default as SingleSelect } from './Select/SingleSelect.esm.js';
3
8
  export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
@@ -5,21 +10,33 @@ export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi
5
10
  export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
6
11
  export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
7
12
  export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
8
- export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
9
- export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
10
13
  export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
11
- export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
12
- export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
13
- export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
14
14
  export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
15
+ export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
16
+ export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
17
+ import 'react/jsx-runtime';
18
+ import 'react';
19
+ import '@mui/material';
20
+ import '@mui/icons-material/CalendarToday';
21
+ import '@mui/icons-material/ChevronLeft';
22
+ import '@mui/icons-material/ChevronRight';
23
+ import 'react-hook-form';
24
+ export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
25
+ import 'dayjs';
26
+ export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
27
+ export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
15
28
  export { default as FormRenderFileUpload } from './FileUpload/FormRenderFileUpload.esm.js';
16
29
  export { default as FormRenderMultiFileUpload } from './FileUpload/FormRenderMultiFileUpload.esm.js';
17
- export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
18
- export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
30
+ import '@mui/icons-material/Visibility';
31
+ import '@mui/icons-material/Close';
32
+ import '@mui/icons-material/NavigateBefore';
33
+ import '@mui/icons-material/NavigateNext';
34
+ import '@mui/icons-material/Delete';
35
+ import '@mui/icons-material/UploadFile';
36
+ import '@mui/icons-material/PictureAsPdf';
37
+ import '@mui/icons-material/TableChart';
38
+ import '@mui/icons-material/Description';
39
+ export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
19
40
  export { default as HelperText } from './HelperText/HelperText.esm.js';
20
- export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
21
- export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
22
41
  export { default as RichTextEditor } from './RichTextEditor/RichTextEditor.esm.js';
23
42
  export { default as RichTextEditorWrapper } from './RichTextEditor/RichTextEditorWrapper.esm.js';
24
- export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
25
- export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as Calendar } from '../../../../../_virtual/index.esm4.js';
1
+ import { __exports as Calendar } from '../../../../../_virtual/index.esm7.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateInput } from '../../../../../_virtual/index.esm13.js';
1
+ import { __exports as DateInput } from '../../../../../_virtual/index.esm14.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRange } from '../../../../../_virtual/index.esm6.js';
1
+ import { __exports as DateRange } from '../../../../../_virtual/index.esm4.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireCalendar } from '../Calendar/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm5.js';
1
+ import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm6.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDateRange } from '../DateRange/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DefinedRange } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as DefinedRange } from '../../../../../_virtual/index.esm5.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireStyles } from '../../styles.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as InputRangeField } from '../../../../../_virtual/index.esm14.js';
1
+ import { __exports as InputRangeField } from '../../../../../_virtual/index.esm12.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { __exports as Month } from '../../../../../_virtual/index.esm12.js';
1
+ import { __exports as Month } from '../../../../../_virtual/index.esm13.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -97,6 +97,13 @@ export interface FormSectionPropsItem {
97
97
  labelVariant?: 'floating' | 'static';
98
98
  /** Style overrides for the label span when `labelVariant: 'static'` is used. */
99
99
  labelSx?: SxProps<Theme>;
100
+ /**
101
+ * Visual style for `radio-group` fields. `'dot'` (default when omitted) keeps every existing
102
+ * consumer's current look — a stock MUI `Radio` dot per option. `'pill'` renders each option
103
+ * as a bordered, labelled button instead (no dot) that fills with the theme's
104
+ * `TPRadioGroup.checkedColor` when selected — no visual change unless a field opts in.
105
+ */
106
+ radioVariant?: 'dot' | 'pill';
100
107
  }
101
108
  export type VariantProps = 'standard' | 'outlined' | '';
102
109
  export interface FormRenderProps {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "3.0.15",
3
+ "version": "3.0.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",
@@ -66,6 +66,8 @@
66
66
  "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
67
67
  "storybook": "storybook dev -p 6006",
68
68
  "build-storybook": "storybook build",
69
+ "deploy-storybook": "powershell -ExecutionPolicy Bypass -File scripts/deploy-storybook.ps1",
70
+ "deploy-storybook:ftp": "powershell -ExecutionPolicy Bypass -File scripts/deploy-storybook-ftp.ps1",
69
71
  "version:patch": "npm version patch",
70
72
  "version:minor": "npm version minor",
71
73
  "version:major": "npm version major",
@@ -0,0 +1,173 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Builds Storybook and publishes storybook-static/ to the SmarterASP.NET site
4
+ "tpproduction-001-site41" over FTP, using the same credentials as
5
+ site41.PublishSettings' FTP profile. Use this if Web Deploy
6
+ (deploy-storybook.ps1) keeps failing with ERROR_CONNECTION_TERMINATED --
7
+ shared hosts often cap the size/duration of a single Web Deploy sync, and
8
+ this build's minified JS chunks exceed that. FTP uploads file-by-file, so a
9
+ large build doesn't hit the same limit.
10
+
11
+ The password is read from .storybook-deploy.local.json in the repo root if
12
+ present (shared with deploy-storybook.ps1, gitignored). Otherwise you're
13
+ prompted once and it's saved there for next time.
14
+
15
+ .EXAMPLE
16
+ ./scripts/deploy-storybook-ftp.ps1
17
+
18
+ .EXAMPLE
19
+ ./scripts/deploy-storybook-ftp.ps1 -SkipBuild
20
+ #>
21
+
22
+ param(
23
+ [string]$FtpHost = "win8095.site4now.net",
24
+ [int]$FtpPort = 21,
25
+ [string]$RemoteRoot = "/tp-react-elements",
26
+ [string]$UserName = "tpproduction-001",
27
+ [SecureString]$Password,
28
+ [switch]$SkipBuild
29
+ )
30
+
31
+ $ErrorActionPreference = "Stop"
32
+ $repoRoot = Split-Path -Parent $PSScriptRoot
33
+ $sourcePath = Join-Path $repoRoot "storybook-static"
34
+ $credentialFile = Join-Path $repoRoot ".storybook-deploy.local.json"
35
+
36
+ if (-not $SkipBuild) {
37
+ Write-Host "Building Storybook..." -ForegroundColor Cyan
38
+ Push-Location $repoRoot
39
+ try {
40
+ npm run build-storybook
41
+ if ($LASTEXITCODE -ne 0) { throw "build-storybook failed with exit code $LASTEXITCODE" }
42
+ } finally {
43
+ Pop-Location
44
+ }
45
+ }
46
+
47
+ if (-not (Test-Path $sourcePath)) {
48
+ throw "Build output not found at $sourcePath"
49
+ }
50
+
51
+ $plainPassword = $null
52
+ $passwordFromFile = $false
53
+
54
+ if ($Password) {
55
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
56
+ try { $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
57
+ finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
58
+ } elseif (Test-Path $credentialFile) {
59
+ $plainPassword = (Get-Content $credentialFile -Raw | ConvertFrom-Json).password
60
+ $passwordFromFile = $true
61
+ Write-Host "Using saved deployment password from .storybook-deploy.local.json" -ForegroundColor DarkGray
62
+ }
63
+
64
+ if (-not $plainPassword) {
65
+ $securePassword = Read-Host -Prompt "FTP password for $UserName" -AsSecureString
66
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
67
+ try { $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
68
+ finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
69
+ }
70
+
71
+ if (-not $passwordFromFile) {
72
+ @{ password = $plainPassword } | ConvertTo-Json | Set-Content -Path $credentialFile -Encoding utf8
73
+ Write-Host "Saved password to .storybook-deploy.local.json for next time (gitignored)." -ForegroundColor DarkGray
74
+ }
75
+
76
+ $credential = New-Object System.Net.NetworkCredential($UserName, $plainPassword)
77
+
78
+ function New-RemoteDirectory([string]$remotePath) {
79
+ $uri = "ftp://$FtpHost`:$FtpPort$remotePath"
80
+ $request = [System.Net.FtpWebRequest]::Create($uri)
81
+ $request.Credentials = $credential
82
+ $request.UsePassive = $true
83
+ $request.UseBinary = $true
84
+ $request.KeepAlive = $false
85
+ $request.Method = [System.Net.WebRequestMethods+Ftp]::MakeDirectory
86
+ try {
87
+ $response = $request.GetResponse()
88
+ $response.Close()
89
+ } catch [System.Net.WebException] {
90
+ # 550 = already exists, which is fine
91
+ $resp = $_.Exception.Response
92
+ if (-not ($resp -and $resp.StatusDescription -match "550")) {
93
+ throw
94
+ }
95
+ }
96
+ }
97
+
98
+ function Send-RemoteFile([string]$localFile, [string]$remotePath) {
99
+ $uri = "ftp://$FtpHost`:$FtpPort$remotePath"
100
+ $request = [System.Net.FtpWebRequest]::Create($uri)
101
+ $request.Credentials = $credential
102
+ $request.UsePassive = $true
103
+ $request.UseBinary = $true
104
+ $request.KeepAlive = $false
105
+ $request.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
106
+
107
+ $bytes = [System.IO.File]::ReadAllBytes($localFile)
108
+ $request.ContentLength = $bytes.Length
109
+ $stream = $request.GetRequestStream()
110
+ try {
111
+ $stream.Write($bytes, 0, $bytes.Length)
112
+ } finally {
113
+ $stream.Close()
114
+ }
115
+ $response = $request.GetResponse()
116
+ $response.Close()
117
+ }
118
+
119
+ Write-Host "Publishing $sourcePath -> ftp://$FtpHost$RemoteRoot ..." -ForegroundColor Cyan
120
+
121
+ $allFiles = Get-ChildItem -Path $sourcePath -Recurse -File
122
+ $createdDirs = New-Object System.Collections.Generic.HashSet[string]
123
+ $total = $allFiles.Count
124
+ $count = 0
125
+ $failed = @()
126
+
127
+ foreach ($file in $allFiles) {
128
+ $count++
129
+ $relative = $file.FullName.Substring($sourcePath.Length).TrimStart("\", "/").Replace("\", "/")
130
+ $remoteFilePath = "$RemoteRoot/$relative"
131
+ $remoteDir = [System.IO.Path]::GetDirectoryName($remoteFilePath).Replace("\", "/")
132
+
133
+ if ($remoteDir -and -not $createdDirs.Contains($remoteDir)) {
134
+ $segments = $remoteDir.Trim("/").Split("/")
135
+ $accum = ""
136
+ foreach ($segment in $segments) {
137
+ $accum = "$accum/$segment"
138
+ if (-not $createdDirs.Contains($accum)) {
139
+ New-RemoteDirectory $accum
140
+ $createdDirs.Add($accum) | Out-Null
141
+ }
142
+ }
143
+ }
144
+
145
+ $attempt = 0
146
+ $ok = $false
147
+ while (-not $ok -and $attempt -lt 3) {
148
+ $attempt++
149
+ try {
150
+ Send-RemoteFile $file.FullName $remoteFilePath
151
+ $ok = $true
152
+ } catch {
153
+ if ($attempt -ge 3) {
154
+ $failed += $relative
155
+ Write-Host "FAILED ($attempt/3): $relative - $($_.Exception.Message)" -ForegroundColor Red
156
+ } else {
157
+ Start-Sleep -Seconds 2
158
+ }
159
+ }
160
+ }
161
+
162
+ if ($ok) {
163
+ Write-Host "[$count/$total] $relative" -ForegroundColor DarkGray
164
+ }
165
+ }
166
+
167
+ if ($failed.Count -gt 0) {
168
+ Write-Host "`n$($failed.Count) file(s) failed to upload:" -ForegroundColor Red
169
+ $failed | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
170
+ throw "FTP deploy finished with $($failed.Count) failure(s)."
171
+ }
172
+
173
+ Write-Host "`nDeployed successfully: $total files." -ForegroundColor Green
@@ -0,0 +1,135 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Builds Storybook and publishes storybook-static/ to the SmarterASP.NET site
4
+ "tpproduction-001-site41" via Web Deploy (MSDeploy), using the same Web Deploy
5
+ mechanism as site41.PublishSettings.
6
+
7
+ The deployment password is read from .storybook-deploy.local.json in the repo
8
+ root if that file exists (it is gitignored, so it never leaves this machine).
9
+ Otherwise you are prompted for it once, and it is saved to that file so future
10
+ runs don't ask again. On a fresh clone the file won't exist yet, so the first
11
+ run there will prompt.
12
+
13
+ .EXAMPLE
14
+ ./scripts/deploy-storybook.ps1
15
+ (uses the saved password, or prompts and saves it on first run)
16
+
17
+ .EXAMPLE
18
+ ./scripts/deploy-storybook.ps1 -SkipBuild
19
+ (redeploys the existing storybook-static/ folder without rebuilding)
20
+ #>
21
+
22
+ param(
23
+ [string]$SiteName = "tpproduction-001-site41",
24
+ [string]$ComputerName = "https://win8095.site4now.net:8172/msdeploy.axd?site=tpproduction-001-site41",
25
+ [string]$UserName = "tpproduction-001",
26
+ [SecureString]$Password,
27
+ [switch]$SkipBuild
28
+ )
29
+
30
+ $ErrorActionPreference = "Stop"
31
+ $repoRoot = Split-Path -Parent $PSScriptRoot
32
+ $sourcePath = Join-Path $repoRoot "storybook-static"
33
+ $credentialFile = Join-Path $repoRoot ".storybook-deploy.local.json"
34
+
35
+ $msdeployCandidates = @(
36
+ "$env:ProgramFiles\IIS\Microsoft Web Deploy V3\msdeploy.exe",
37
+ "${env:ProgramFiles(x86)}\IIS\Microsoft Web Deploy V3\msdeploy.exe"
38
+ )
39
+ $msdeploy = $msdeployCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
40
+ if (-not $msdeploy) {
41
+ throw "msdeploy.exe not found. Install 'Web Deploy 3.6' from Microsoft and re-run."
42
+ }
43
+
44
+ if (-not $SkipBuild) {
45
+ Write-Host "Building Storybook..." -ForegroundColor Cyan
46
+ Push-Location $repoRoot
47
+ try {
48
+ npm run build-storybook
49
+ if ($LASTEXITCODE -ne 0) { throw "build-storybook failed with exit code $LASTEXITCODE" }
50
+ } finally {
51
+ Pop-Location
52
+ }
53
+ }
54
+
55
+ if (-not (Test-Path $sourcePath)) {
56
+ throw "Build output not found at $sourcePath"
57
+ }
58
+
59
+ $plainPassword = $null
60
+ $passwordFromFile = $false
61
+
62
+ if ($Password) {
63
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
64
+ try {
65
+ $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
66
+ } finally {
67
+ [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
68
+ }
69
+ } elseif (Test-Path $credentialFile) {
70
+ $plainPassword = (Get-Content $credentialFile -Raw | ConvertFrom-Json).password
71
+ $passwordFromFile = $true
72
+ Write-Host "Using saved deployment password from .storybook-deploy.local.json" -ForegroundColor DarkGray
73
+ }
74
+
75
+ if (-not $plainPassword) {
76
+ $securePassword = Read-Host -Prompt "Web Deploy password for $UserName" -AsSecureString
77
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
78
+ try {
79
+ $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
80
+ } finally {
81
+ [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
82
+ }
83
+ }
84
+
85
+ if (-not $passwordFromFile) {
86
+ @{ password = $plainPassword } | ConvertTo-Json | Set-Content -Path $credentialFile -Encoding utf8
87
+ Write-Host "Saved password to .storybook-deploy.local.json for next time (gitignored)." -ForegroundColor DarkGray
88
+ }
89
+
90
+ Write-Host "Publishing $sourcePath -> $SiteName via Web Deploy..." -ForegroundColor Cyan
91
+
92
+ # Synced one file at a time (instead of the whole storybook-static folder in a
93
+ # single sync) because this build is ~10MB across 150+ files, and one big sync
94
+ # kept dying with ERROR_CONNECTION_TERMINATED partway through -- SmarterASP's
95
+ # shared-hosting Web Deploy endpoint appears to cap how long/large a single
96
+ # request can be. Each file here is well under 1.5MB, small enough to finish
97
+ # before any such limit kicks in. contentPath's provider auto-creates any
98
+ # destination folders that don't exist yet, so no separate mkdir pass is needed.
99
+ $allFiles = Get-ChildItem -Path $sourcePath -Recurse -File
100
+ $total = $allFiles.Count
101
+ $count = 0
102
+ $failed = @()
103
+
104
+ foreach ($file in $allFiles) {
105
+ $count++
106
+ $relative = $file.FullName.Substring($sourcePath.Length).TrimStart("\", "/").Replace("\", "/")
107
+ $remoteFilePath = "$SiteName/$relative"
108
+
109
+ # Pre-quoted as strings (not inline) so PowerShell's native-argument
110
+ # marshalling preserves the embedded quotes around paths that contain
111
+ # spaces (e.g. "D:\TP Projects\..."). Writing the quotes inline in the
112
+ # call below causes PowerShell to strip them before msdeploy ever sees
113
+ # them, which then misparses the whole descriptor as an unknown provider.
114
+ $sourceArg = "-source:contentPath=`"$($file.FullName)`""
115
+ $destArg = "-dest:contentPath=`"$remoteFilePath`",ComputerName=`"$ComputerName`",UserName=`"$UserName`",Password=`"$plainPassword`",AuthType=`"Basic`""
116
+
117
+ $output = & $msdeploy -verb:sync $sourceArg $destArg -allowUntrusted -retryAttempts:2 -retryInterval:5000 2>&1
118
+ if ($LASTEXITCODE -eq 0) {
119
+ Write-Host "[$count/$total] $relative" -ForegroundColor DarkGray
120
+ } else {
121
+ $failed += $relative
122
+ Write-Host "[$count/$total] FAILED: $relative" -ForegroundColor Red
123
+ $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
124
+ }
125
+ }
126
+
127
+ $plainPassword = $null
128
+
129
+ if ($failed.Count -gt 0) {
130
+ Write-Host "`n$($failed.Count) file(s) failed to deploy:" -ForegroundColor Red
131
+ $failed | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
132
+ throw "Web Deploy finished with $($failed.Count) failure(s)."
133
+ }
134
+
135
+ Write-Host "`nDeployed successfully: $total files." -ForegroundColor Green