wcz-test 2.2.2 → 2.3.1
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/index.d.ts +56 -32
- package/dist/index.js +131 -108
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/index.cjs +0 -2606
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -216
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { LinkComponent, ErrorComponentProps } from '@tanstack/react-router';
|
|
|
5
5
|
import * as react from 'react';
|
|
6
6
|
import react__default, { FC } from 'react';
|
|
7
7
|
import { Navigation } from '@toolpad/core/AppProvider';
|
|
8
|
-
export {
|
|
8
|
+
export { Navigation } from '@toolpad/core/AppProvider';
|
|
9
9
|
import { TFunction } from 'i18next';
|
|
10
10
|
export { uuidv7 } from 'uuidv7';
|
|
11
11
|
import * as axios from 'axios';
|
|
@@ -16,36 +16,6 @@ import * as _tanstack_form_core from '@tanstack/form-core';
|
|
|
16
16
|
export { useDialogs } from '@toolpad/core/useDialogs';
|
|
17
17
|
export { useNotifications } from '@toolpad/core/useNotifications';
|
|
18
18
|
export { useLocalStorageState } from '@toolpad/core/useLocalStorageState';
|
|
19
|
-
export { useTranslation } from 'react-i18next';
|
|
20
|
-
|
|
21
|
-
var Layout = {
|
|
22
|
-
Logout: "Logout",
|
|
23
|
-
LogIn: "Log In",
|
|
24
|
-
Language: "Language",
|
|
25
|
-
Appearance: "Appearance",
|
|
26
|
-
Light: "Light",
|
|
27
|
-
Dark: "Dark",
|
|
28
|
-
System: "System",
|
|
29
|
-
Homepage: "Homepage",
|
|
30
|
-
ThisPageCouldNotBeFound: "This page could not be found.",
|
|
31
|
-
Settings: "Settings",
|
|
32
|
-
Unauthorized: "Unauthorized",
|
|
33
|
-
DevelopmentDialogTitle: "Testing environment",
|
|
34
|
-
DevelopmentDialogContent: "You are in a test environment. Data integrity is not guaranteed and application behavior may be unpredictable. Please proceed with caution and report any unexpected behavior to the development team."
|
|
35
|
-
};
|
|
36
|
-
var en = {
|
|
37
|
-
Layout: Layout
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type TranslationKeys = typeof en;
|
|
41
|
-
|
|
42
|
-
declare module 'i18next' {
|
|
43
|
-
interface CustomTypeOptions {
|
|
44
|
-
resources: {
|
|
45
|
-
translation: TranslationKeys;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
19
|
|
|
50
20
|
interface TypographyWithIconProps extends TypographyProps {
|
|
51
21
|
startIcon?: React.ReactNode;
|
|
@@ -114,6 +84,60 @@ declare class Platform {
|
|
|
114
84
|
}
|
|
115
85
|
declare const getContrastTextColor: (color: string) => "black" | "white";
|
|
116
86
|
declare const wczApiClient: axios.AxiosInstance;
|
|
87
|
+
declare const WISTRON_PRIMARY_COLOR = "#00506E";
|
|
88
|
+
declare const WISTRON_SECONDARY_COLOR = "#64DC00";
|
|
89
|
+
|
|
90
|
+
interface HeadProps {
|
|
91
|
+
name: string;
|
|
92
|
+
themeColor: string;
|
|
93
|
+
}
|
|
94
|
+
declare const defaultHead: ({ name, themeColor }: HeadProps) => () => {
|
|
95
|
+
meta: ({
|
|
96
|
+
charSet: string;
|
|
97
|
+
name?: undefined;
|
|
98
|
+
content?: undefined;
|
|
99
|
+
title?: undefined;
|
|
100
|
+
} | {
|
|
101
|
+
name: string;
|
|
102
|
+
content: string;
|
|
103
|
+
charSet?: undefined;
|
|
104
|
+
title?: undefined;
|
|
105
|
+
} | {
|
|
106
|
+
title: string;
|
|
107
|
+
charSet?: undefined;
|
|
108
|
+
name?: undefined;
|
|
109
|
+
content?: undefined;
|
|
110
|
+
})[];
|
|
111
|
+
links: ({
|
|
112
|
+
rel: string;
|
|
113
|
+
sizes: string;
|
|
114
|
+
href: string;
|
|
115
|
+
type?: undefined;
|
|
116
|
+
name?: undefined;
|
|
117
|
+
theme_color?: undefined;
|
|
118
|
+
} | {
|
|
119
|
+
rel: string;
|
|
120
|
+
type: string;
|
|
121
|
+
sizes: string;
|
|
122
|
+
href: string;
|
|
123
|
+
name?: undefined;
|
|
124
|
+
theme_color?: undefined;
|
|
125
|
+
} | {
|
|
126
|
+
rel: string;
|
|
127
|
+
href: string;
|
|
128
|
+
name: string;
|
|
129
|
+
theme_color: string;
|
|
130
|
+
sizes?: undefined;
|
|
131
|
+
type?: undefined;
|
|
132
|
+
} | {
|
|
133
|
+
rel: string;
|
|
134
|
+
href: string;
|
|
135
|
+
sizes?: undefined;
|
|
136
|
+
type?: undefined;
|
|
137
|
+
name?: undefined;
|
|
138
|
+
theme_color?: undefined;
|
|
139
|
+
})[];
|
|
140
|
+
};
|
|
117
141
|
|
|
118
142
|
type FormOmittedProps = "name" | "value" | "onChange" | "onBlur" | "error" | "helperText" | "renderInput" | "type" | "aria-label";
|
|
119
143
|
|
|
@@ -213,4 +237,4 @@ declare const withLayoutForm: <TFormData, TOnMount extends _tanstack_form_core.F
|
|
|
213
237
|
};
|
|
214
238
|
}>) => react.JSX.Element;
|
|
215
239
|
|
|
216
|
-
export { ChipInputCell, EditableColumnHeader, LayoutProvider, type NavigationParams, Platform, RouterButton, RouterError, RouterLink, RouterNotFound, RouterTab, TypographyWithIcon, type User, getContrastTextColor, useFieldContext, useFormContext, useLayoutForm, wczApiClient, withLayoutForm };
|
|
240
|
+
export { ChipInputCell, EditableColumnHeader, LayoutProvider, type NavigationParams, Platform, RouterButton, RouterError, RouterLink, RouterNotFound, RouterTab, TypographyWithIcon, type User, WISTRON_PRIMARY_COLOR, WISTRON_SECONDARY_COLOR, defaultHead, getContrastTextColor, useFieldContext, useFormContext, useLayoutForm, wczApiClient, withLayoutForm };
|
package/dist/index.js
CHANGED
|
@@ -1032,9 +1032,9 @@ var TypographyWithIcon = ({ startIcon, endIcon, children, sx, gutterBottom, ...p
|
|
|
1032
1032
|
return acc;
|
|
1033
1033
|
}, {});
|
|
1034
1034
|
return /* @__PURE__ */ jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, sx: stackStyles, mb: gutterBottom ? 0.7 : void 0, children: [
|
|
1035
|
-
startIcon
|
|
1035
|
+
startIcon,
|
|
1036
1036
|
/* @__PURE__ */ jsx(Typography, { ...props, sx: sxCopy, children }),
|
|
1037
|
-
endIcon
|
|
1037
|
+
endIcon
|
|
1038
1038
|
] });
|
|
1039
1039
|
};
|
|
1040
1040
|
|
|
@@ -1950,8 +1950,111 @@ import { csCZ, enUS } from "@mui/material/locale";
|
|
|
1950
1950
|
import { csCZ as dataGridCsCz, enUS as dataGridEnUs } from "@mui/x-data-grid-premium/locales";
|
|
1951
1951
|
import { csCZ as datePickersCsCz, enUS as datePickersEnUs } from "@mui/x-date-pickers-pro/locales";
|
|
1952
1952
|
import { useTranslation as useTranslation4 } from "react-i18next";
|
|
1953
|
-
|
|
1954
|
-
|
|
1953
|
+
|
|
1954
|
+
// src/utils/ClientUtils.ts
|
|
1955
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
1956
|
+
import axios from "axios";
|
|
1957
|
+
|
|
1958
|
+
// src/services/UserService.ts
|
|
1959
|
+
import Keycloak from "keycloak-js";
|
|
1960
|
+
var keycloakConfig = {
|
|
1961
|
+
url: import.meta.env.VITE_KEYCLOAK_URL,
|
|
1962
|
+
realm: import.meta.env.VITE_KEYCLOAK_REALM,
|
|
1963
|
+
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
|
|
1964
|
+
idpHint: import.meta.env.VITE_KEYCLOAK_IDP_HINT,
|
|
1965
|
+
confidentialClientId: import.meta.env.VITE_KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
|
|
1966
|
+
confidentialClientSecret: import.meta.env.VITE_KEYCLOAK_CONFIDENTIAL_CLIENT_SECRET
|
|
1967
|
+
};
|
|
1968
|
+
var _kc = new Keycloak({
|
|
1969
|
+
url: keycloakConfig.url,
|
|
1970
|
+
realm: keycloakConfig.realm,
|
|
1971
|
+
clientId: keycloakConfig.clientId
|
|
1972
|
+
});
|
|
1973
|
+
var initKeycloak = async () => {
|
|
1974
|
+
if (typeof window === "undefined") return;
|
|
1975
|
+
const authenticated = await _kc.init({ onLoad: "check-sso" });
|
|
1976
|
+
if (authenticated) {
|
|
1977
|
+
return await loadUserInfo();
|
|
1978
|
+
} else {
|
|
1979
|
+
await _kc.login({ idpHint: keycloakConfig.idpHint });
|
|
1980
|
+
return await loadUserInfo();
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
var login = _kc.login;
|
|
1984
|
+
var logout = _kc.logout;
|
|
1985
|
+
var getToken = () => {
|
|
1986
|
+
if (_kc.token) {
|
|
1987
|
+
return _kc.token;
|
|
1988
|
+
} else {
|
|
1989
|
+
return getConfidentialToken();
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
var getConfidentialToken = async () => {
|
|
1993
|
+
const { confidentialClientId, confidentialClientSecret, url, realm } = keycloakConfig;
|
|
1994
|
+
if (!confidentialClientId || !confidentialClientSecret)
|
|
1995
|
+
throw new Error("Confidential client id or secret is missing");
|
|
1996
|
+
const credentials = btoa(`${confidentialClientId}:${confidentialClientSecret}`);
|
|
1997
|
+
const urlencoded = new URLSearchParams({ grant_type: "client_credentials" });
|
|
1998
|
+
const response = await fetch(`${url}/realms/${realm}/protocol/openid-connect/token`, {
|
|
1999
|
+
method: "POST",
|
|
2000
|
+
headers: {
|
|
2001
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2002
|
+
"Authorization": `Basic ${credentials}`
|
|
2003
|
+
},
|
|
2004
|
+
body: urlencoded
|
|
2005
|
+
});
|
|
2006
|
+
if (!response.ok)
|
|
2007
|
+
throw new Error("Failed to obtain confidential token");
|
|
2008
|
+
const data = await response.json();
|
|
2009
|
+
return data.access_token;
|
|
2010
|
+
};
|
|
2011
|
+
var updateToken = () => _kc.updateToken(30).catch(login);
|
|
2012
|
+
var hasRole = (roles) => roles.some((role) => _kc.hasRealmRole(role) || _kc.hasResourceRole(role));
|
|
2013
|
+
var loadUserInfo = async () => {
|
|
2014
|
+
const user2 = await _kc.loadUserInfo();
|
|
2015
|
+
return {
|
|
2016
|
+
name: user2.name,
|
|
2017
|
+
employeeId: user2.employeeId?.toUpperCase() ?? "",
|
|
2018
|
+
department: user2.department ?? "",
|
|
2019
|
+
email: user2.email,
|
|
2020
|
+
company: user2.company,
|
|
2021
|
+
category: user2.category
|
|
2022
|
+
};
|
|
2023
|
+
};
|
|
2024
|
+
var UserService = {
|
|
2025
|
+
initKeycloak,
|
|
2026
|
+
login,
|
|
2027
|
+
logout,
|
|
2028
|
+
getToken,
|
|
2029
|
+
updateToken,
|
|
2030
|
+
hasRole
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
// src/utils/ClientUtils.ts
|
|
2034
|
+
var Platform = class {
|
|
2035
|
+
static isAndroid = /android/i.test(navigator.userAgent);
|
|
2036
|
+
static isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
2037
|
+
static isWindows = /windows/i.test(navigator.userAgent);
|
|
2038
|
+
static isMacOS = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent);
|
|
2039
|
+
};
|
|
2040
|
+
var getContrastTextColor = (color) => {
|
|
2041
|
+
const r = parseInt(color.substring(1, 3), 16);
|
|
2042
|
+
const g = parseInt(color.substring(3, 5), 16);
|
|
2043
|
+
const b = parseInt(color.substring(5, 7), 16);
|
|
2044
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1e3;
|
|
2045
|
+
return yiq >= 128 ? "black" : "white";
|
|
2046
|
+
};
|
|
2047
|
+
var wczApiClient = axios.create();
|
|
2048
|
+
wczApiClient.interceptors.request.use(async (config) => {
|
|
2049
|
+
await UserService.updateToken();
|
|
2050
|
+
const token = await UserService.getToken();
|
|
2051
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
2052
|
+
return config;
|
|
2053
|
+
});
|
|
2054
|
+
var WISTRON_PRIMARY_COLOR = "#00506E";
|
|
2055
|
+
var WISTRON_SECONDARY_COLOR = "#64DC00";
|
|
2056
|
+
|
|
2057
|
+
// src/hooks/ThemeHook.ts
|
|
1955
2058
|
var useGetTheme = (theme) => {
|
|
1956
2059
|
const { i18n: i18n2 } = useTranslation4();
|
|
1957
2060
|
return createTheme(
|
|
@@ -1962,14 +2065,14 @@ var useGetTheme = (theme) => {
|
|
|
1962
2065
|
colorSchemes: {
|
|
1963
2066
|
light: {
|
|
1964
2067
|
palette: {
|
|
1965
|
-
primary: { main:
|
|
1966
|
-
secondary: { main:
|
|
2068
|
+
primary: { main: WISTRON_PRIMARY_COLOR },
|
|
2069
|
+
secondary: { main: WISTRON_SECONDARY_COLOR }
|
|
1967
2070
|
}
|
|
1968
2071
|
},
|
|
1969
2072
|
dark: {
|
|
1970
2073
|
palette: {
|
|
1971
|
-
primary: { main: lighten(
|
|
1972
|
-
secondary: { main: darken(
|
|
2074
|
+
primary: { main: lighten(WISTRON_PRIMARY_COLOR, 0.1) },
|
|
2075
|
+
secondary: { main: darken(WISTRON_SECONDARY_COLOR, 0.1) }
|
|
1973
2076
|
}
|
|
1974
2077
|
},
|
|
1975
2078
|
...theme?.colorSchemes
|
|
@@ -2112,104 +2215,23 @@ var LayoutProvider = (props) => {
|
|
|
2112
2215
|
// src/index.ts
|
|
2113
2216
|
import { uuidv7 } from "uuidv7";
|
|
2114
2217
|
|
|
2115
|
-
// src/utils/
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
});
|
|
2133
|
-
var initKeycloak = async () => {
|
|
2134
|
-
if (typeof window === "undefined") return;
|
|
2135
|
-
const authenticated = await _kc.init({ onLoad: "check-sso" });
|
|
2136
|
-
if (authenticated) {
|
|
2137
|
-
return await loadUserInfo();
|
|
2138
|
-
} else {
|
|
2139
|
-
await _kc.login({ idpHint: keycloakConfig.idpHint });
|
|
2140
|
-
return await loadUserInfo();
|
|
2141
|
-
}
|
|
2142
|
-
};
|
|
2143
|
-
var login = _kc.login;
|
|
2144
|
-
var logout = _kc.logout;
|
|
2145
|
-
var getToken = () => {
|
|
2146
|
-
if (_kc.token) {
|
|
2147
|
-
return _kc.token;
|
|
2148
|
-
} else {
|
|
2149
|
-
return getConfidentialToken();
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
var getConfidentialToken = async () => {
|
|
2153
|
-
const { confidentialClientId, confidentialClientSecret, url, realm } = keycloakConfig;
|
|
2154
|
-
if (!confidentialClientId || !confidentialClientSecret)
|
|
2155
|
-
throw new Error("Confidential client id or secret is missing");
|
|
2156
|
-
const credentials = btoa(`${confidentialClientId}:${confidentialClientSecret}`);
|
|
2157
|
-
const urlencoded = new URLSearchParams({ grant_type: "client_credentials" });
|
|
2158
|
-
const response = await fetch(`${url}/realms/${realm}/protocol/openid-connect/token`, {
|
|
2159
|
-
method: "POST",
|
|
2160
|
-
headers: {
|
|
2161
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
2162
|
-
"Authorization": `Basic ${credentials}`
|
|
2163
|
-
},
|
|
2164
|
-
body: urlencoded
|
|
2165
|
-
});
|
|
2166
|
-
if (!response.ok)
|
|
2167
|
-
throw new Error("Failed to obtain confidential token");
|
|
2168
|
-
const data = await response.json();
|
|
2169
|
-
return data.access_token;
|
|
2170
|
-
};
|
|
2171
|
-
var updateToken = () => _kc.updateToken(30).catch(login);
|
|
2172
|
-
var hasRole = (roles) => roles.some((role) => _kc.hasRealmRole(role) || _kc.hasResourceRole(role));
|
|
2173
|
-
var loadUserInfo = async () => {
|
|
2174
|
-
const user2 = await _kc.loadUserInfo();
|
|
2175
|
-
return {
|
|
2176
|
-
name: user2.name,
|
|
2177
|
-
employeeId: user2.employeeId?.toUpperCase() ?? "",
|
|
2178
|
-
department: user2.department ?? "",
|
|
2179
|
-
email: user2.email,
|
|
2180
|
-
company: user2.company,
|
|
2181
|
-
category: user2.category
|
|
2182
|
-
};
|
|
2183
|
-
};
|
|
2184
|
-
var UserService = {
|
|
2185
|
-
initKeycloak,
|
|
2186
|
-
login,
|
|
2187
|
-
logout,
|
|
2188
|
-
getToken,
|
|
2189
|
-
updateToken,
|
|
2190
|
-
hasRole
|
|
2191
|
-
};
|
|
2192
|
-
|
|
2193
|
-
// src/utils/ClientUtils.ts
|
|
2194
|
-
var Platform = class {
|
|
2195
|
-
static isAndroid = /android/i.test(navigator.userAgent);
|
|
2196
|
-
static isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
2197
|
-
static isWindows = /windows/i.test(navigator.userAgent);
|
|
2198
|
-
static isMacOS = /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent);
|
|
2199
|
-
};
|
|
2200
|
-
var getContrastTextColor = (color) => {
|
|
2201
|
-
const r = parseInt(color.substring(1, 3), 16);
|
|
2202
|
-
const g = parseInt(color.substring(3, 5), 16);
|
|
2203
|
-
const b = parseInt(color.substring(5, 7), 16);
|
|
2204
|
-
const yiq = (r * 299 + g * 587 + b * 114) / 1e3;
|
|
2205
|
-
return yiq >= 128 ? "black" : "white";
|
|
2206
|
-
};
|
|
2207
|
-
var wczApiClient = axios.create();
|
|
2208
|
-
wczApiClient.interceptors.request.use(async (config) => {
|
|
2209
|
-
await UserService.updateToken();
|
|
2210
|
-
const token = await UserService.getToken();
|
|
2211
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
2212
|
-
return config;
|
|
2218
|
+
// src/utils/client/DefaultHead.ts
|
|
2219
|
+
var defaultHead = ({ name, themeColor }) => () => ({
|
|
2220
|
+
meta: [
|
|
2221
|
+
{ charSet: "utf-8" },
|
|
2222
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
2223
|
+
{ title: name },
|
|
2224
|
+
{ name: "og:type", content: "website" },
|
|
2225
|
+
{ name: "og:title", content: name },
|
|
2226
|
+
{ name: "og:image", content: "/favicon-32x32.png" }
|
|
2227
|
+
],
|
|
2228
|
+
links: [
|
|
2229
|
+
{ rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
|
|
2230
|
+
{ rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32.png" },
|
|
2231
|
+
{ rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16.png" },
|
|
2232
|
+
{ rel: "manifest", href: "/site.webmanifest", name, theme_color: themeColor },
|
|
2233
|
+
{ rel: "icon", href: "/favicon.ico" }
|
|
2234
|
+
]
|
|
2213
2235
|
});
|
|
2214
2236
|
|
|
2215
2237
|
// src/hooks/FormHooks.ts
|
|
@@ -2515,7 +2537,6 @@ var { useAppForm: useLayoutForm, withForm: withLayoutForm } = createFormHook({
|
|
|
2515
2537
|
import { useDialogs } from "@toolpad/core/useDialogs";
|
|
2516
2538
|
import { useNotifications } from "@toolpad/core/useNotifications";
|
|
2517
2539
|
import { useLocalStorageState } from "@toolpad/core/useLocalStorageState";
|
|
2518
|
-
import { useTranslation as useTranslation6 } from "react-i18next";
|
|
2519
2540
|
export {
|
|
2520
2541
|
ChipInputCell,
|
|
2521
2542
|
EditableColumnHeader,
|
|
@@ -2527,6 +2548,9 @@ export {
|
|
|
2527
2548
|
RouterNotFound,
|
|
2528
2549
|
RouterTab,
|
|
2529
2550
|
TypographyWithIcon,
|
|
2551
|
+
WISTRON_PRIMARY_COLOR,
|
|
2552
|
+
WISTRON_SECONDARY_COLOR,
|
|
2553
|
+
defaultHead,
|
|
2530
2554
|
getContrastTextColor,
|
|
2531
2555
|
useDialogs,
|
|
2532
2556
|
useFieldContext,
|
|
@@ -2534,7 +2558,6 @@ export {
|
|
|
2534
2558
|
useLayoutForm,
|
|
2535
2559
|
useLocalStorageState,
|
|
2536
2560
|
useNotifications,
|
|
2537
|
-
useTranslation6 as useTranslation,
|
|
2538
2561
|
uuidv7,
|
|
2539
2562
|
wczApiClient,
|
|
2540
2563
|
withLayoutForm
|