tp-react-elements 3.0.14 → 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.
- package/dist/_virtual/index.esm12.js +2 -2
- package/dist/_virtual/index.esm13.js +2 -2
- package/dist/_virtual/index.esm14.js +2 -2
- package/dist/_virtual/index.esm4.js +2 -2
- package/dist/_virtual/index.esm5.js +2 -2
- package/dist/_virtual/index.esm6.js +2 -2
- package/dist/_virtual/index.esm7.js +2 -2
- package/dist/components/FormComponents/FormRadioGroup/FormRadioGroup.esm.js +25 -1
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +17 -9
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +2 -2
- package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DefinedRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/InputRangeField/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
- package/dist/utils/Interface/FormInterface.d.ts +7 -0
- package/package.json +3 -1
- package/scripts/deploy-storybook-ftp.ps1 +173 -0
- package/scripts/deploy-storybook.ps1 +135 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var InputRangeField = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { InputRangeField as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Month = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Month as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateInput = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateInput as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRange as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DefinedRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DefinedRange as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Calendar as __exports };
|
|
@@ -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(
|
|
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 })] }));
|
|
@@ -37,10 +37,15 @@ export interface GenericPaginatedGridProps<TData extends object> {
|
|
|
37
37
|
*/
|
|
38
38
|
columns: TPColumnDef<TData>[];
|
|
39
39
|
/**
|
|
40
|
-
* Async function called on every page/filter change.
|
|
41
|
-
* Receives the current flat filter map
|
|
40
|
+
* Async function called on every page/filter/page-size change.
|
|
41
|
+
* Receives the current flat filter map, the 0-based page index, and the
|
|
42
|
+
* current `pageSize` (whatever `pageSize`/`pageSizeOptions`/`batchSize`
|
|
43
|
+
* resolved to, including the user's latest "Rows per page" selection) so
|
|
44
|
+
* the request can ask the backend for exactly that many rows. Existing
|
|
45
|
+
* implementations that only declare the first two parameters keep working
|
|
46
|
+
* unchanged — the third argument is simply ignored by them, same as before.
|
|
42
47
|
*/
|
|
43
|
-
fetchPage: (filters: Record<string, string>, pageIndex: number) => Promise<PagedResult<TData>>;
|
|
48
|
+
fetchPage: (filters: Record<string, string>, pageIndex: number, pageSize: number) => Promise<PagedResult<TData>>;
|
|
44
49
|
/** Base react-query cache key. */
|
|
45
50
|
queryKey: unknown[];
|
|
46
51
|
/** Server-side filter chip definitions. */
|
|
@@ -123,12 +128,15 @@ export interface GenericPaginatedGridProps<TData extends object> {
|
|
|
123
128
|
* Initial "Rows per page" value, shown in the footer before the user touches the dropdown.
|
|
124
129
|
*
|
|
125
130
|
* The grid never slices rows client-side (`manualPagination: true`, no row model) — whatever
|
|
126
|
-
* `fetchPage` resolves with for a page **is** that page, shown in full.
|
|
127
|
-
*
|
|
128
|
-
* `fetchPage`
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
131
|
+
* `fetchPage` resolves with for a page **is** that page, shown in full. The current value
|
|
132
|
+
* (initial `pageSize`, or whatever the user picks from the footer dropdown afterward) is
|
|
133
|
+
* passed to `fetchPage` as its third argument and is part of the react-query cache key, so
|
|
134
|
+
* picking a new value re-fetches that page at the new size — `fetchPage` should ask the
|
|
135
|
+
* backend for that many rows per call. Implementations written before this argument existed
|
|
136
|
+
* simply ignore it and keep behaving exactly as before. The two exist separately only because
|
|
137
|
+
* `batchSize` doubles as "rows per fetch" documentation elsewhere; use `pageSize` when you
|
|
138
|
+
* want the initial dropdown value to be authoritative without reinterpreting `batchSize`'s
|
|
139
|
+
* meaning at every call site.
|
|
132
140
|
*
|
|
133
141
|
* Default: `batchSize` (unchanged behaviour — identical to how the initial page size worked
|
|
134
142
|
* before this prop existed). If this value isn't already in `pageSizeOptions`, it's folded in
|
|
@@ -466,8 +466,8 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
466
466
|
const resolvedHeight = tableHeight || (shouldAutoFill ? autoHeight : undefined) || tableMinHeight;
|
|
467
467
|
// ── Data fetching ──────────────────────────────────────────────────────────
|
|
468
468
|
const { data, isLoading, isError, isFetching, refetch } = useQuery({
|
|
469
|
-
queryKey: [...queryKey, filterValues, pagination.pageIndex],
|
|
470
|
-
queryFn: () => fetchPage(filterValues, pagination.pageIndex),
|
|
469
|
+
queryKey: [...queryKey, filterValues, pagination.pageIndex, pagination.pageSize],
|
|
470
|
+
queryFn: () => fetchPage(filterValues, pagination.pageIndex, pagination.pageSize),
|
|
471
471
|
placeholderData: keepPreviousData,
|
|
472
472
|
staleTime: 30000,
|
|
473
473
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as Calendar } from '../../../../../_virtual/index.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|