tp-react-elements 3.0.15 → 3.0.17
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/DateRangePicker/DateRangePicker.esm.js +1 -1
- package/dist/components/Form/FormRender.esm.js +7 -7
- package/dist/components/FormComponents/FormRadioGroup/FormRadioGroup.esm.js +25 -1
- package/dist/components/FormComponents/index.esm.js +28 -11
- 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 DateInput = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateInput as __exports };
|
|
@@ -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 DefinedRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DefinedRange 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 Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Calendar as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker as __exports };
|
|
@@ -83,7 +83,7 @@ const DateRangePickerComponent = ({ item, control, watch, setValue, getValues, c
|
|
|
83
83
|
const displayValue = state[0].startDate && state[0].endDate
|
|
84
84
|
? `${format(state[0].startDate, 'dd/MM/yyyy')} - ${format(state[0].endDate, 'dd/MM/yyyy')}`
|
|
85
85
|
: '';
|
|
86
|
-
const label = variant !== 'standard' ? `${item.label}${item.required ? ' *' : ''}` : '';
|
|
86
|
+
const label = variant !== 'standard' && item.labelVariant !== 'static' ? `${item.label}${item.required ? ' *' : ''}` : '';
|
|
87
87
|
return (jsxs(Fragment, { children: [jsx(Box, { onClick: handleClick, sx: { width: '100%' }, children: jsx(Controller, { name: fromDateName, control: control, render: ({ fieldState }) => (jsxs(Fragment, { children: [renderLabel(variant, {
|
|
88
88
|
item,
|
|
89
89
|
...rest,
|
|
@@ -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(
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
1
|
+
import { __exports as Calendar } 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 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.esm12.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.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 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.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 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.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 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.esm13.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.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 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.17",
|
|
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
|