mirror of
https://github.com/devcode-it/openstamanager.git
synced 2025-06-05 22:09:38 +02:00
feat: ✨ 💥 Conversione a Typescript
- Rimossi Babel e Flow, sostituito con Typescript - Qualche piccolo fix qua e là - Aggiornate dipendenze
This commit is contained in:
1013
.editorconfig
1013
.editorconfig
File diff suppressed because it is too large
Load Diff
13
.flowconfig
13
.flowconfig
@ -1,13 +0,0 @@
|
|||||||
[ignore]
|
|
||||||
|
|
||||||
[include]
|
|
||||||
resources/js
|
|
||||||
resources/js/**
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
|
|
||||||
[options]
|
|
||||||
|
|
||||||
[strict]
|
|
26
.idea/codeStyles/Project.xml
generated
26
.idea/codeStyles/Project.xml
generated
@ -15,6 +15,16 @@
|
|||||||
<MarkdownNavigatorCodeStyleSettings>
|
<MarkdownNavigatorCodeStyleSettings>
|
||||||
<option name="WRAP_ON_TYPING" value="1" />
|
<option name="WRAP_ON_TYPING" value="1" />
|
||||||
</MarkdownNavigatorCodeStyleSettings>
|
</MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="PREFER_AS_TYPE_CAST" value="true" />
|
||||||
|
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="VAR_DECLARATION_WRAP" value="2" />
|
||||||
|
<option name="IMPORTS_WRAP" value="2" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_TYPE_BRACES" value="false" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
<codeStyleSettings language="JavaScript">
|
<codeStyleSettings language="JavaScript">
|
||||||
<option name="RIGHT_MARGIN" value="100" />
|
<option name="RIGHT_MARGIN" value="100" />
|
||||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
@ -36,5 +46,21 @@
|
|||||||
<option name="INDENT_SIZE" value="4" />
|
<option name="INDENT_SIZE" value="4" />
|
||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="RIGHT_MARGIN" value="100" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="2" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="1" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="1" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="1" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="1" />
|
||||||
|
<option name="WRAP_ON_TYPING" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
7
.idea/compiler.xml
generated
Normal file
7
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="TypeScriptCompiler">
|
||||||
|
<option name="typeScriptServiceDirectory" value="$PROJECT_DIR$/node_modules/typescript" />
|
||||||
|
<option name="versionType" value="SERVICE_DIRECTORY" />
|
||||||
|
</component>
|
||||||
|
</project>
|
2
.idea/jsLibraryMappings.xml
generated
2
.idea/jsLibraryMappings.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="JavaScriptLibraryMappings">
|
<component name="JavaScriptLibraryMappings">
|
||||||
<file url="PROJECT" libraries="{@types/mithril, @types/prop-types, es-module-shims}" />
|
<file url="PROJECT" libraries="{@types/prop-types}" />
|
||||||
<includedPredefinedLibrary name="Node.js Core" />
|
<includedPredefinedLibrary name="Node.js Core" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="JavaScriptSettings">
|
<component name="JavaScriptSettings">
|
||||||
<option name="languageLevel" value="FLOW" />
|
<option name="languageLevel" value="ES6" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectResources">
|
<component name="ProjectResources">
|
||||||
<resource url="https://cdn.jsdelivr.net/npm/@mdi/font@5.9.55/css/materialdesignicons.min.css" location="$PROJECT_DIR$" />
|
<resource url="https://cdn.jsdelivr.net/npm/@mdi/font@5.9.55/css/materialdesignicons.min.css" location="$PROJECT_DIR$" />
|
||||||
|
4
.idea/osm_rewrite.iml
generated
4
.idea/osm_rewrite.iml
generated
@ -92,10 +92,6 @@
|
|||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="@types/mithril" level="application" />
|
|
||||||
<orderEntry type="library" name="@types/mithril" level="application" />
|
|
||||||
<orderEntry type="library" name="es-module-shims" level="application" />
|
|
||||||
<orderEntry type="library" name="es-module-shims" level="application" />
|
|
||||||
<orderEntry type="library" name="@types/prop-types" level="application" />
|
<orderEntry type="library" name="@types/prop-types" level="application" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
@ -1,9 +0,0 @@
|
|||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
|
||||||
|
|
||||||
preferInteractive: true
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
|
@ -28,7 +28,7 @@ class DevServerFix extends Command
|
|||||||
{
|
{
|
||||||
foreach (glob(resource_path('static/vendor') . '/*/*/index.js', GLOB_NOSORT) as $file) {
|
foreach (glob(resource_path('static/vendor') . '/*/*/index.js', GLOB_NOSORT) as $file) {
|
||||||
$content = File::get($file);
|
$content = File::get($file);
|
||||||
File::put($file, str_replace('../../../index.js', '/resources/js/index.js', $content));
|
File::put($file, str_replace('../../../index.js', '/resources/js/index.ts', $content));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
|
16
babel.config.js
vendored
16
babel.config.js
vendored
@ -1,16 +0,0 @@
|
|||||||
// eslint-disable-next-line import/no-commonjs,unicorn/prefer-module
|
|
||||||
module.exports = function (api) {
|
|
||||||
api.assertVersion(7);
|
|
||||||
api.cache.forever();
|
|
||||||
|
|
||||||
return {
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@adeira/babel-preset-adeira',
|
|
||||||
{
|
|
||||||
target: 'js-esm'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
@ -117,7 +117,7 @@ return [
|
|||||||
| the development server starts.
|
| the development server starts.
|
||||||
*/
|
*/
|
||||||
'commands' => [
|
'commands' => [
|
||||||
'vite:aliases',
|
//'vite:aliases',
|
||||||
// 'typescript:generate'
|
// 'typescript:generate'
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
20
package.json
20
package.json
@ -15,7 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/inertia": "^0.10.1",
|
"@inertiajs/inertia": "^0.10.1",
|
||||||
"@inertiajs/progress": "^0.2.6",
|
"@inertiajs/progress": "^0.2.6",
|
||||||
"@maicol07/inertia-mithril": "^0.4.0",
|
"@maicol07/inertia-mithril": "^0.4.2",
|
||||||
"@maicol07/mwc-card": "^0.25.2-1",
|
"@maicol07/mwc-card": "^0.25.2-1",
|
||||||
"@maicol07/mwc-layout-grid": "^0.25.3-1",
|
"@maicol07/mwc-layout-grid": "^0.25.3-1",
|
||||||
"@material/data-table": "^13.0.0",
|
"@material/data-table": "^13.0.0",
|
||||||
@ -42,31 +42,31 @@
|
|||||||
"@material/theme": "^13.0.0",
|
"@material/theme": "^13.0.0",
|
||||||
"@material/typography": "^13.0.0",
|
"@material/typography": "^13.0.0",
|
||||||
"@mdi/font": "^6.5.95",
|
"@mdi/font": "^6.5.95",
|
||||||
"async-wait-until": "2.0.9",
|
"async-wait-until": "2.0.12",
|
||||||
"cash-dom": "^8.1.0",
|
"cash-dom": "^8.1.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"collect.js": "^4.29.3",
|
"collect.js": "^4.30.3",
|
||||||
"coloquent": "^2.4.0",
|
"coloquent": "^2.4.1",
|
||||||
"include-media": "^1.4.10",
|
"include-media": "^1.4.10",
|
||||||
"lit": "^2.0.2",
|
"lit": "^2.0.2",
|
||||||
"locale-code": "^2.0.2",
|
"locale-code": "^2.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash": "npm:lodash-es",
|
||||||
"lottie-web": "^5.8.1",
|
"lottie-web": "^5.8.1",
|
||||||
"mithril": "^2.0.4",
|
"mithril": "^2.0.4",
|
||||||
"mithril-node-render": "^3.0.2",
|
"mithril-node-render": "^3.0.2",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"prop-types": "^15.8.0",
|
|
||||||
"redaxios": "^0.4.1"
|
"redaxios": "^0.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adeira/babel-preset-adeira": "^4.0.0",
|
"@maicol07/eslint-config": "^2.1.2",
|
||||||
"@maicol07/eslint-config": "^1.1.3",
|
|
||||||
"@openstamanager/vite-config": "github:devcode-it/openstamanager-vite-config",
|
"@openstamanager/vite-config": "github:devcode-it/openstamanager-vite-config",
|
||||||
|
"@types/lodash": "^4.14.178",
|
||||||
|
"@types/mithril": "^2.0.8",
|
||||||
"@types/ziggy-js": "^1.3.0",
|
"@types/ziggy-js": "^1.3.0",
|
||||||
"concurrently": "^6.5.1",
|
"concurrently": "^7.0.0",
|
||||||
"csstype": "^3.0.10",
|
"csstype": "^3.0.10",
|
||||||
"laravel-vite": "^0.0.23",
|
"laravel-vite": "^0.0.23",
|
||||||
"sass": "^1.45.1",
|
"sass": "^1.45.2",
|
||||||
"stylelint": "^14.2.0",
|
"stylelint": "^14.2.0",
|
||||||
"stylelint-config-html": "^1.0.0",
|
"stylelint-config-html": "^1.0.0",
|
||||||
"stylelint-config-idiomatic-order": "^8.1.0",
|
"stylelint-config-idiomatic-order": "^8.1.0",
|
||||||
|
2798
pnpm-lock.yaml
generated
2798
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,80 +0,0 @@
|
|||||||
import '@material/mwc-dialog';
|
|
||||||
|
|
||||||
import {type Cash} from 'cash-dom/dist/cash';
|
|
||||||
import {uniqueId} from 'lodash-es';
|
|
||||||
import Lottie from 'lottie-web';
|
|
||||||
import {type ClassComponent} from 'mithril';
|
|
||||||
|
|
||||||
import Component from './Component.jsx';
|
|
||||||
|
|
||||||
export default class Alert extends Component implements ClassComponent<{
|
|
||||||
heading?: string,
|
|
||||||
icon?: string,
|
|
||||||
image?: string,
|
|
||||||
'image-width'?: string | number,
|
|
||||||
'image-height'?: string | number,
|
|
||||||
'image-alt'?: string,
|
|
||||||
trigger?: string,
|
|
||||||
open?: boolean
|
|
||||||
}> {
|
|
||||||
view(vnode) {
|
|
||||||
const image = {
|
|
||||||
src: this.attrs.pull('image'),
|
|
||||||
width: this.attrs.pull('image-width', '125px'),
|
|
||||||
height: this.attrs.pull('image-height', '125px'),
|
|
||||||
alt: this.attrs.pull('image-alt')
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = [];
|
|
||||||
for (const child of vnode.children) {
|
|
||||||
if (child.attrs && child.attrs.slot && ['primaryAction', 'secondaryAction'].includes(child.attrs.slot)) {
|
|
||||||
actions.push(child);
|
|
||||||
const index = vnode.children.indexOf(child);
|
|
||||||
vnode.children.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mwc-dialog {...this.attrs.all()}>
|
|
||||||
<div className="graphic" style={`width: ${image.width}; height: ${image.height}; margin: 0 auto;`}>
|
|
||||||
{image.src && <img src={image.src} alt={image.alt}/>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="content">
|
|
||||||
{vnode.children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{actions.length > 0 ? actions : <mwc-button label={__('OK')} slot="primaryAction" dialogAction="ok"/>}
|
|
||||||
</mwc-dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
if (this.attrs.get('id')) {
|
|
||||||
this.attrs.put('id', uniqueId('dialog_'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
const dialog: Cash = $(`#${this.attrs.get('id')}`);
|
|
||||||
|
|
||||||
if (this.attrs.has('icon')) {
|
|
||||||
const animation = Lottie.loadAnimation({
|
|
||||||
container: dialog.find('.graphic')[0],
|
|
||||||
renderer: 'svg',
|
|
||||||
loop: false,
|
|
||||||
autoplay: false,
|
|
||||||
path: new URL(`/animations/${this.attrs.pull('icon')}.json`, import.meta.url).href
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.on('opening', () => {
|
|
||||||
animation.goToAndStop(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.on('opened', () => {
|
|
||||||
animation.play();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
99
resources/js/Components/Alert.tsx
Normal file
99
resources/js/Components/Alert.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import '@material/mwc-dialog';
|
||||||
|
import 'cash-dom/dist/cash';
|
||||||
|
|
||||||
|
import type {Cash} from 'cash-dom/dist/cash';
|
||||||
|
import {uniqueId} from 'lodash';
|
||||||
|
import Lottie from 'lottie-web';
|
||||||
|
import type {
|
||||||
|
Children,
|
||||||
|
Vnode
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
|
import Component from './Component';
|
||||||
|
|
||||||
|
type Attributes = {
|
||||||
|
heading?: string
|
||||||
|
icon?: string
|
||||||
|
image?: string
|
||||||
|
'image-width'?: string | number
|
||||||
|
'image-height'?: string | number
|
||||||
|
'image-alt'?: string
|
||||||
|
trigger?: string
|
||||||
|
open?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Rimuovere per utilizzare solo gli snackbar?
|
||||||
|
|
||||||
|
export default class Alert extends Component<Attributes> {
|
||||||
|
view(vnode: Vnode<Attributes>) {
|
||||||
|
const image = {
|
||||||
|
src: this.attrs.pull('image'),
|
||||||
|
width: this.attrs.pull('image-width') ?? '125px',
|
||||||
|
height: this.attrs.pull('image-height') ?? '125px',
|
||||||
|
alt: this.attrs.pull('image-alt')
|
||||||
|
};
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
for (const child of vnode.children as Children[]) {
|
||||||
|
if (
|
||||||
|
child.attrs
|
||||||
|
&& child.attrs.slot
|
||||||
|
&& ['primaryAction', 'secondaryAction'].includes(child.attrs.slot)
|
||||||
|
) {
|
||||||
|
actions.push(child);
|
||||||
|
const index = vnode.children.indexOf(child);
|
||||||
|
vnode.children.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mwc-dialog {...this.attrs.all()}>
|
||||||
|
<div
|
||||||
|
className="graphic"
|
||||||
|
style={`width: ${image.width}; height: ${image.height}; margin: 0 auto;`}
|
||||||
|
>
|
||||||
|
{image.src && <img src={image.src} alt={image.alt} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">{vnode.children}</div>
|
||||||
|
|
||||||
|
{actions.length > 0 ? (
|
||||||
|
actions
|
||||||
|
) : (
|
||||||
|
<mwc-button label={__('OK')} slot="primaryAction" dialogAction="ok" />
|
||||||
|
)}
|
||||||
|
</mwc-dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
if (this.attrs.get('id')) {
|
||||||
|
this.attrs.put('id', uniqueId('dialog_'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
const dialog: Cash = $(`#${this.attrs.get('id')}`);
|
||||||
|
|
||||||
|
if (this.attrs.has('icon')) {
|
||||||
|
const animation = Lottie.loadAnimation({
|
||||||
|
container: dialog.find('.graphic')[0],
|
||||||
|
renderer: 'svg',
|
||||||
|
loop: false,
|
||||||
|
autoplay: false,
|
||||||
|
path: new URL(
|
||||||
|
`/animations/${this.attrs.pull('icon')}.json`,
|
||||||
|
import.meta.url
|
||||||
|
).href
|
||||||
|
});
|
||||||
|
dialog.on('opening', () => {
|
||||||
|
animation.goToAndStop(0);
|
||||||
|
});
|
||||||
|
dialog.on('opened', () => {
|
||||||
|
animation.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,185 +0,0 @@
|
|||||||
import type {Cash} from 'cash-dom/dist/cash';
|
|
||||||
import classnames, {Argument as ClassNames} from 'classnames';
|
|
||||||
import collect, {Collection} from 'collect.js';
|
|
||||||
import m, {
|
|
||||||
Children,
|
|
||||||
ClassComponent,
|
|
||||||
Vnode,
|
|
||||||
VnodeDOM
|
|
||||||
} from 'mithril';
|
|
||||||
|
|
||||||
interface Attributes extends Collection {
|
|
||||||
addClassNames(...classNames: ClassNames[]): void,
|
|
||||||
|
|
||||||
addStyles(...styles: string[]): void
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-secrets/no-secrets
|
|
||||||
// noinspection SpellCheckingInspection,JSUnusedGlobalSymbols
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
*
|
|
||||||
* The `Component` class defines a user interface 'building block'. A component
|
|
||||||
* generates a virtual DOM to be rendered on each redraw.
|
|
||||||
*
|
|
||||||
* Essentially, this is a wrapper for Mithril's components that adds several useful features:
|
|
||||||
*
|
|
||||||
* - In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs.
|
|
||||||
* This allows us to use attrs across components without having to pass the vnode to every single
|
|
||||||
* method.
|
|
||||||
* - The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise
|
|
||||||
* modify) the attrs that have been passed into a component.
|
|
||||||
* - When the component is created in the DOM, we store its DOM element under `this.element`;
|
|
||||||
* this lets us use Cash to modify child DOM state from internal methods via the `this.$()`
|
|
||||||
* method.
|
|
||||||
* - A convenience `component` method, which serves as an alternative to hyperscript and JSX.
|
|
||||||
*
|
|
||||||
* As with other Mithril components, components extending Component can be initialized
|
|
||||||
* and nested using JSX, hyperscript, or a combination of both. The `component` method can also
|
|
||||||
* be used.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* return m('div', <MyComponent foo="bar"><p>Hello World</p></MyComponent>);
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* return m('div', MyComponent.component({foo: 'bar'), m('p', 'Hello World!'));
|
|
||||||
*
|
|
||||||
* @see https://js.org/components.html
|
|
||||||
*/
|
|
||||||
export default class Component implements ClassComponent {
|
|
||||||
/**
|
|
||||||
* The root DOM element for the component.
|
|
||||||
*
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
element: Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The attributes passed into the component. They are transformed into a collection by initAttrs.
|
|
||||||
*
|
|
||||||
* @method <string> addClassNames()
|
|
||||||
*
|
|
||||||
* @see https://js.org/components.html#passing-data-to-components
|
|
||||||
* @see initAttrs
|
|
||||||
*
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
attrs: Attributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
view(vnode: Vnode): Children {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
oninit(vnode: Vnode) {
|
|
||||||
this.setAttrs(vnode.attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
oncreate(vnode: VnodeDOM) {
|
|
||||||
this.element = vnode.dom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
onbeforeupdate(vnode: VnodeDOM) {
|
|
||||||
this.setAttrs(vnode.attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
onupdate(vnode: VnodeDOM) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
onbeforeremove(vnode: VnodeDOM) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
onremove(vnode: VnodeDOM) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Cash object for this component's element. If you pass in a
|
|
||||||
* selector string, this method will return a Cash object, using the current
|
|
||||||
* element as its buffer.
|
|
||||||
*
|
|
||||||
* For example, calling `component.$('li')` will return a Cash object
|
|
||||||
* containing all of the `li` elements inside the DOM element of this
|
|
||||||
* component.
|
|
||||||
*
|
|
||||||
* @param [selector] a Cash-compatible selector string
|
|
||||||
* @returns the Cash object for the DOM node
|
|
||||||
* @final
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
$(selector?: string): Cash {
|
|
||||||
const $element: Cash<HTMLElement> = $(this.element);
|
|
||||||
return selector ? $element.find(element => selector(element)) : $element;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience method to attach a component without JSX.
|
|
||||||
* Has the same effect as calling `m(THIS_CLASS, attrs, children)`.
|
|
||||||
*
|
|
||||||
* @see https://js.org/hyperscript.html#mselector,-attributes,-children
|
|
||||||
*/
|
|
||||||
static component(attributes: {...} = {}, children): Vnode {
|
|
||||||
const componentAttributes: Record<string, any> = { ...attributes};
|
|
||||||
|
|
||||||
return m(this, componentAttributes, children);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a reference to the vnode attrs after running them through initAttrs,
|
|
||||||
* and checking for common issues.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
setAttrs(attributes: {...} = {}): void {
|
|
||||||
this.initAttrs(attributes);
|
|
||||||
if (attributes) {
|
|
||||||
if ('children' in attributes) {
|
|
||||||
// noinspection JSUnresolvedVariable
|
|
||||||
throw new Error(`[${this.constructor.name}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`);
|
|
||||||
}
|
|
||||||
if ('tag' in attributes) {
|
|
||||||
// noinspection JSUnresolvedVariable
|
|
||||||
throw new Error(`[${this.constructor.name}] You cannot use the "tag" attribute name with Mithril 2.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.attrs = collect(attributes);
|
|
||||||
this.attrs.macro('addClassNames', (...classNames: ClassNames[]) => {
|
|
||||||
this.attrs.put('className', classnames(this.attrs.get('className'), ...classNames));
|
|
||||||
});
|
|
||||||
this.attrs.macro('addStyles', (...styles: string[]) => {
|
|
||||||
let s: string = this.attrs.get('style', '');
|
|
||||||
|
|
||||||
if (!s.trimEnd().endsWith(';')) {
|
|
||||||
s += '; ';
|
|
||||||
}
|
|
||||||
|
|
||||||
s += styles.join('; ');
|
|
||||||
this.attrs.put('style', s);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the component's attrs.
|
|
||||||
*
|
|
||||||
* This can be used to assign default values for missing, optional attrs.
|
|
||||||
*
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
initAttrs(attributes: {...}): void {}
|
|
||||||
}
|
|
167
resources/js/Components/Component.ts
Normal file
167
resources/js/Components/Component.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import classnames, {Argument as ClassNames} from 'classnames';
|
||||||
|
import collect, {Collection} from 'collect.js';
|
||||||
|
import m, {
|
||||||
|
Children,
|
||||||
|
ClassComponent,
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
|
interface Attributes<T> extends Collection<T> {
|
||||||
|
addClassNames(...classNames: ClassNames[]): void
|
||||||
|
addStyles(...styles: string[]): void
|
||||||
|
}
|
||||||
|
// noinspection SpellCheckingInspection,JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*
|
||||||
|
* The `Component` class defines a user interface 'building block'. A component
|
||||||
|
* generates a virtual DOM to be rendered on each redraw.
|
||||||
|
*
|
||||||
|
* Essentially, this is a wrapper for Mithril's components that adds several useful features:
|
||||||
|
*
|
||||||
|
* — In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs.
|
||||||
|
* This allows us to use attrs across components without having to pass the vnode to every single
|
||||||
|
* method.
|
||||||
|
* — The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise
|
||||||
|
* modify) the attrs that have been passed into a component.
|
||||||
|
* — When the component is created in the DOM, we store its DOM element under `this.element`;
|
||||||
|
* this lets us use Cash to modify child DOM state from internal methods via the `this.$()`
|
||||||
|
* method.
|
||||||
|
* — A convenience `component` method, which serves as an alternative to hyperscript and JSX.
|
||||||
|
*
|
||||||
|
* As with other Mithril components, components extending Component can be initialized
|
||||||
|
* and nested using JSX. The `component` method can also
|
||||||
|
* be used.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* return m('div', <MyComponent foo="bar"><p>Hello World</p></MyComponent>);
|
||||||
|
*
|
||||||
|
* @see https://js.org/components.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Component<A> implements m.Component<A>, ClassComponent<A> {
|
||||||
|
/**
|
||||||
|
* The root DOM element for the component.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
element: Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes passed into the component. They are transformed into a collection by initAttrs.
|
||||||
|
*
|
||||||
|
* @method <string> addClassNames()
|
||||||
|
*
|
||||||
|
* @see https://js.org/components.html#passing-data-to-components
|
||||||
|
* @see initAttrs
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
attrs: Attributes<string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.element = undefined as unknown as Element;
|
||||||
|
this.attrs = undefined as unknown as Attributes<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
view(vnode: Vnode<A>): Children {
|
||||||
|
return m('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
oninit(vnode: Vnode<A>) {
|
||||||
|
this.setAttrs(vnode.attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
oncreate(vnode: VnodeDOM<A>) {
|
||||||
|
this.element = vnode.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
onbeforeupdate(vnode: VnodeDOM<A, this>) {
|
||||||
|
this.setAttrs(vnode.attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
onupdate(vnode: VnodeDOM<A>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
onbeforeremove(vnode: VnodeDOM<A>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
onremove(vnode: VnodeDOM<A>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a reference to the vnode attrs after running them through initAttrs,
|
||||||
|
* and checking for common issues.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
setAttrs(attributes: {} = {}): void {
|
||||||
|
this.initAttrs(attributes);
|
||||||
|
|
||||||
|
if (attributes) {
|
||||||
|
if ('children' in attributes) {
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
throw new Error(
|
||||||
|
`[${this.constructor.name}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('tag' in attributes) {
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
throw new Error(
|
||||||
|
`[${this.constructor.name}] You cannot use the "tag" attribute name with Mithril 2.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributesCollection: Collection<string> = collect(attributes);
|
||||||
|
attributesCollection.macro('addClassNames', (...classNames: ClassNames[]) => {
|
||||||
|
attributesCollection.put(
|
||||||
|
'className',
|
||||||
|
classnames(attributesCollection.get('className') as ClassNames, ...classNames)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
attributesCollection.macro('addStyles', (...styles: string[]) => {
|
||||||
|
let s: string = attributesCollection.get<string, string>('style', '' as unknown as () => string) as string; // Type conversions are required here because of the way the typescript compiler works.
|
||||||
|
|
||||||
|
if (!s.trimEnd().endsWith(';')) {
|
||||||
|
s += '; ';
|
||||||
|
}
|
||||||
|
|
||||||
|
s += styles.join('; ');
|
||||||
|
attributesCollection.put('style', s);
|
||||||
|
});
|
||||||
|
this.attrs = attributesCollection as Attributes<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedLocalSymbols
|
||||||
|
/**
|
||||||
|
* Initialize the component's attrs.
|
||||||
|
*
|
||||||
|
* This can be used to assign default values for missing, optional attrs.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
initAttrs(attributes: {}): void {}
|
||||||
|
}
|
@ -1,333 +0,0 @@
|
|||||||
import '@material/mwc-linear-progress';
|
|
||||||
import '@material/mwc-list/mwc-list-item';
|
|
||||||
import '../../WebComponents/Select';
|
|
||||||
|
|
||||||
import {type Cash} from 'cash-dom/dist/cash';
|
|
||||||
import {
|
|
||||||
type Children,
|
|
||||||
type Vnode
|
|
||||||
} from 'mithril';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Component from '../Component.jsx';
|
|
||||||
import Mdi from '../Mdi.jsx';
|
|
||||||
import TableColumn from './TableColumn.jsx';
|
|
||||||
import TableFooter from './TableFooter.jsx';
|
|
||||||
import TableRow from './TableRow.jsx';
|
|
||||||
|
|
||||||
export default class DataTable extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
'rows-per-page': PropTypes.number,
|
|
||||||
'default-rows-per-page': PropTypes.number,
|
|
||||||
'aria-label': PropTypes.string,
|
|
||||||
checkable: PropTypes.bool,
|
|
||||||
paginated: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
rows: Cash[] = [];
|
|
||||||
columns: Children[];
|
|
||||||
footer: Children[];
|
|
||||||
|
|
||||||
rowsPerPage = {
|
|
||||||
options: [10, 25, 50, 75, 100],
|
|
||||||
currentStart: 0,
|
|
||||||
value: 10,
|
|
||||||
currentEnd: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
onbeforeupdate(vnode) {
|
|
||||||
super.onbeforeupdate(vnode);
|
|
||||||
|
|
||||||
const children = (vnode.children: Children[]).flat();
|
|
||||||
|
|
||||||
this.rows = this.tableRows(children);
|
|
||||||
this.columns = this.filterElements(children, TableColumn);
|
|
||||||
this.footer = this.filterElements(children, TableFooter);
|
|
||||||
|
|
||||||
const rowsPerPage = this.attrs.get('rows-per-page');
|
|
||||||
if (rowsPerPage) {
|
|
||||||
this.rowsPerPage.options = rowsPerPage.split(',')
|
|
||||||
.map((value: string) => Number.parseInt(value, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
let defaultRowsPerPage = this.attrs.get('default-rows-per-page', 10);
|
|
||||||
if (typeof defaultRowsPerPage === 'string' && Number.isInteger(defaultRowsPerPage)) {
|
|
||||||
defaultRowsPerPage = Number.parseInt(defaultRowsPerPage, 10);
|
|
||||||
|
|
||||||
if (!this.rowsPerPage.options.includes(defaultRowsPerPage)) {
|
|
||||||
[defaultRowsPerPage] = this.rowsPerPage.options;
|
|
||||||
}
|
|
||||||
this.rowsPerPage.value = defaultRowsPerPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rowsPerPage.currentStart === 0) {
|
|
||||||
this.rowsPerPage.currentEnd = this.rowsPerPage.value >= this.rows.length ? this.rows.length
|
|
||||||
: defaultRowsPerPage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onupdate(vnode) {
|
|
||||||
super.onupdate(vnode);
|
|
||||||
|
|
||||||
const rows: Cash = $(this.element).find('tbody tr');
|
|
||||||
rows.hide();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
|
||||||
for (let index = this.rowsPerPage.currentStart; index < this.rowsPerPage.currentEnd; index++) {
|
|
||||||
rows.eq(index).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rowsPerPage.currentStart === 0) {
|
|
||||||
this.paginate('first');
|
|
||||||
}
|
|
||||||
|
|
||||||
$(this.element)
|
|
||||||
.find('thead th.mdc-data-table__header-cell--with-sort')
|
|
||||||
.on('click', this.onColumnClicked.bind(this));
|
|
||||||
|
|
||||||
$(this.element).find('.mdc-data-table__pagination-rows-per-page-select')
|
|
||||||
.on('selected', this.onPaginationSelected.bind(this));
|
|
||||||
|
|
||||||
$(this.element).find('.mdc-data-table__pagination-button')
|
|
||||||
.on('click', this.onPaginationButtonClicked.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
return <div className="mdc-data-table" {...this.attrs.all()}>
|
|
||||||
<div className="mdc-data-table__table-container">
|
|
||||||
<table className="mdc-data-table__table" aria-label={this.attrs.get('aria-label')}>
|
|
||||||
<thead>
|
|
||||||
<tr className="mdc-data-table__header-row">
|
|
||||||
{this.attrs.has('checkable') && <TableColumn type="checkbox"/>}
|
|
||||||
{this.columns}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody className="mdc-data-table__content">
|
|
||||||
{this.rows}
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
{this.footer}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{this.attrs.has('paginated') && <div className="mdc-data-table__pagination">
|
|
||||||
<div className="mdc-data-table__pagination-trailing">
|
|
||||||
<div className="mdc-data-table__pagination-rows-per-page">
|
|
||||||
<div className="mdc-data-table__pagination-rows-per-page-label">
|
|
||||||
{__('Righe per pagina')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<material-select
|
|
||||||
outlined
|
|
||||||
className="mdc-data-table__pagination-rows-per-page-select"
|
|
||||||
fixedMenuPosition
|
|
||||||
style="--mdc-select-width: 112px; --mdc-select-height: 36px; --mdc-menu-item-height: 36px;"
|
|
||||||
>
|
|
||||||
{this.rowsPerPage.options.map(
|
|
||||||
rowsPerPage => (
|
|
||||||
<mwc-list-item
|
|
||||||
key={rowsPerPage}
|
|
||||||
value={rowsPerPage}
|
|
||||||
selected={this.rowsPerPage.value === rowsPerPage}
|
|
||||||
>
|
|
||||||
{rowsPerPage}
|
|
||||||
</mwc-list-item>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</material-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mdc-data-table__pagination-navigation">
|
|
||||||
<div className="mdc-data-table__pagination-total">
|
|
||||||
{__(':start-:chunk di :total', {
|
|
||||||
start: this.rowsPerPage.currentStart + 1,
|
|
||||||
chunk: this.rowsPerPage.currentEnd,
|
|
||||||
total: this.rows.length
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<mwc-icon-button className="mdc-data-table__pagination-button" data-page="first"
|
|
||||||
disabled>
|
|
||||||
<Mdi icon="page-first"/>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<mwc-icon-button className="mdc-data-table__pagination-button" data-page="previous"
|
|
||||||
disabled>
|
|
||||||
<Mdi icon="chevron-left"/>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<mwc-icon-button className="mdc-data-table__pagination-button" data-page="next">
|
|
||||||
<Mdi icon="chevron-right"/>
|
|
||||||
</mwc-icon-button>
|
|
||||||
<mwc-icon-button className="mdc-data-table__pagination-button" data-page="last">
|
|
||||||
<Mdi icon="page-last"/>
|
|
||||||
</mwc-icon-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<div className="mdc-data-table__progress-indicator">
|
|
||||||
<div className="mdc-data-table__scrim"/>
|
|
||||||
<mwc-linear-progress className="mdc-data-table__linear-progress" indeterminate/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
tableRows(children: Children[]): Children[] {
|
|
||||||
let rows = this.filterElements(children, TableRow);
|
|
||||||
|
|
||||||
if (this.attrs.has('checkable')) {
|
|
||||||
rows = rows.map((row: Vnode) => (
|
|
||||||
<TableRow key={row.attrs.key} checkable {...row.attrs}>
|
|
||||||
{row.children}
|
|
||||||
</TableRow>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterElements(elements: Children[], tag: Component | string): Children[] {
|
|
||||||
const filtered = [];
|
|
||||||
|
|
||||||
for (const element: Vnode of elements) {
|
|
||||||
if (element.tag === tag) {
|
|
||||||
filtered.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress() {
|
|
||||||
$(this.element)
|
|
||||||
.addClass('mdc-data-table--in-progress')
|
|
||||||
.find('.mdc-data-table__progress-indicator mwc-linear-progress')
|
|
||||||
.get(0)
|
|
||||||
.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
hideProgress() {
|
|
||||||
$(this.element)
|
|
||||||
.removeClass('mdc-data-table--in-progress')
|
|
||||||
.find('.mdc-data-table__progress-indicator mwc-linear-progress')
|
|
||||||
.get(0)
|
|
||||||
.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
onColumnClicked(event: Event) {
|
|
||||||
this.showProgress();
|
|
||||||
|
|
||||||
const column: Cash = $(event.target)
|
|
||||||
.closest('th');
|
|
||||||
const ascendingClass = 'mdc-data-table__header-cell--sorted';
|
|
||||||
|
|
||||||
// Clean previously sorted info and arrows
|
|
||||||
const columns = $(this.element)
|
|
||||||
.find('thead th');
|
|
||||||
columns.removeClass(ascendingClass);
|
|
||||||
columns.off('click').on('click', this.onColumnClicked.bind(this));
|
|
||||||
|
|
||||||
// Add ony one header to sort
|
|
||||||
column.addClass(ascendingClass);
|
|
||||||
|
|
||||||
// Do sorting
|
|
||||||
this.sortTable(column.attr('id'), false);
|
|
||||||
|
|
||||||
// Set/remove callbacks
|
|
||||||
column.off('click');
|
|
||||||
column.find('mwc-icon-button-toggle').on('click', () => {
|
|
||||||
this.sortTable(column.attr('id'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sortTable(columnId: number, toggleClass = true) {
|
|
||||||
const column: Cash = $(`#${columnId}`);
|
|
||||||
const cells = $(this.element).find(`tr td:nth-child(${column.index() + 1})`).get();
|
|
||||||
|
|
||||||
// Handle button class
|
|
||||||
if (toggleClass) {
|
|
||||||
column.toggleClass('mdc-data-table__header-cell--sorted-descending');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNumeric = column.attr('type') === 'numeric';
|
|
||||||
const isDescending = column.hasClass('mdc-data-table__header-cell--sorted-descending');
|
|
||||||
|
|
||||||
cells.sort((a: HTMLElement, b: HTMLElement) => {
|
|
||||||
let aValue = a.textContent;
|
|
||||||
let bValue = b.textContent;
|
|
||||||
|
|
||||||
if (isNumeric) {
|
|
||||||
aValue = Number.parseFloat(aValue);
|
|
||||||
bValue = Number.parseFloat(bValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDescending) {
|
|
||||||
const temporary = aValue;
|
|
||||||
aValue = bValue;
|
|
||||||
bValue = temporary;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof aValue === 'string') {
|
|
||||||
return aValue.localeCompare(bValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return aValue < bValue ? -1 : (aValue > bValue ? 1 : 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const cell of cells) {
|
|
||||||
const row = $(cell)
|
|
||||||
.parent();
|
|
||||||
row.appendTo(row.parent());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hideProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaginationSelected(event: Event) {
|
|
||||||
this.rowsPerPage.value = $(event.target).find('mwc-list-item').eq(event.detail.index).val();
|
|
||||||
this.rowsPerPage.currentStart = 0;
|
|
||||||
this.rowsPerPage.currentEnd = this.rowsPerPage.value;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaginationButtonClicked(event: Event) {
|
|
||||||
const button: Cash = $(event.target);
|
|
||||||
this.paginate(button.data('page'));
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
paginate(action: 'first' | 'next' | 'previous' | 'last') {
|
|
||||||
const increments = {
|
|
||||||
first: -this.rowsPerPage.currentStart,
|
|
||||||
next: this.rowsPerPage.value,
|
|
||||||
previous: -this.rowsPerPage.value,
|
|
||||||
last: this.rows.length - this.rowsPerPage.currentStart
|
|
||||||
};
|
|
||||||
const increment = increments[action];
|
|
||||||
|
|
||||||
if (action !== 'first') {
|
|
||||||
this.rowsPerPage.currentStart += increment;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action !== 'last') {
|
|
||||||
this.rowsPerPage.currentEnd += increment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paginationButtons: Cash = $(this.element).find('.mdc-data-table__pagination-button');
|
|
||||||
const disabled = {
|
|
||||||
first: this.rowsPerPage.currentStart === 0,
|
|
||||||
previous: this.rowsPerPage.currentStart === 0,
|
|
||||||
next: this.rowsPerPage.currentEnd >= this.rows.length,
|
|
||||||
last: this.rowsPerPage.currentEnd >= this.rows.length
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const button of paginationButtons) {
|
|
||||||
const buttonElement = $(button);
|
|
||||||
const buttonAction = buttonElement.data('page');
|
|
||||||
buttonElement.prop('disabled', disabled[buttonAction]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
376
resources/js/Components/DataTable/DataTable.tsx
Normal file
376
resources/js/Components/DataTable/DataTable.tsx
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
import '@material/mwc-linear-progress';
|
||||||
|
import '@material/mwc-list/mwc-list-item';
|
||||||
|
import '../../WebComponents/Select';
|
||||||
|
|
||||||
|
import type {LinearProgress as MWCLinearProgress} from '@material/mwc-linear-progress';
|
||||||
|
import type {Cash} from 'cash-dom/dist/cash';
|
||||||
|
import type {
|
||||||
|
Children,
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
|
import Component from '../Component';
|
||||||
|
import Mdi from '../Mdi';
|
||||||
|
import TableColumn from './TableColumn';
|
||||||
|
import TableFooter from './TableFooter';
|
||||||
|
import TableRow, {type TableRowAttributes} from './TableRow';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
DataTable: DataTable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationAction = 'first' | 'next' | 'previous' | 'last';
|
||||||
|
|
||||||
|
export type Attributes = {
|
||||||
|
'rows-per-page'?: number,
|
||||||
|
'default-rows-per-page'?: number,
|
||||||
|
'aria-label'?: string,
|
||||||
|
checkable?: boolean,
|
||||||
|
paginated?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class DataTable extends Component<Attributes> {
|
||||||
|
rows: Children[] = [];
|
||||||
|
columns: Children[];
|
||||||
|
footer: Children[];
|
||||||
|
rowsPerPage = {
|
||||||
|
options: [10, 25, 50, 75, 100],
|
||||||
|
currentStart: 0,
|
||||||
|
value: 10,
|
||||||
|
currentEnd: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
onbeforeupdate(vnode: VnodeDOM<Attributes, this>) {
|
||||||
|
super.onbeforeupdate(vnode);
|
||||||
|
const children = (vnode.children as Children[]).flat();
|
||||||
|
this.rows = this.tableRows(children);
|
||||||
|
this.columns = this.filterElements(children, TableColumn);
|
||||||
|
this.footer = this.filterElements(children, TableFooter);
|
||||||
|
const rowsPerPage = this.attrs.get('rows-per-page');
|
||||||
|
|
||||||
|
if (rowsPerPage) {
|
||||||
|
this.rowsPerPage.options = rowsPerPage
|
||||||
|
.split(',')
|
||||||
|
.map((value: string) => Number.parseInt(value, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultRowsPerPage: number = Number.parseInt(this.attrs.get('default-rows-per-page', '10') as string, 10);
|
||||||
|
|
||||||
|
if (Number.isInteger(defaultRowsPerPage)) {
|
||||||
|
if (!this.rowsPerPage.options.includes(defaultRowsPerPage)) {
|
||||||
|
[defaultRowsPerPage] = this.rowsPerPage.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rowsPerPage.value = defaultRowsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rowsPerPage.currentStart === 0) {
|
||||||
|
this.rowsPerPage.currentEnd = this.rowsPerPage.value >= this.rows.length
|
||||||
|
? this.rows.length
|
||||||
|
: defaultRowsPerPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onupdate(vnode: VnodeDOM<Attributes, this>) {
|
||||||
|
super.onupdate(vnode);
|
||||||
|
const rows: Cash = $(this.element).find('tbody tr');
|
||||||
|
rows.hide();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (
|
||||||
|
let index = this.rowsPerPage.currentStart;
|
||||||
|
index < this.rowsPerPage.currentEnd;
|
||||||
|
index += 1
|
||||||
|
) {
|
||||||
|
rows.eq(index).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rowsPerPage.currentStart === 0) {
|
||||||
|
this.paginate('first');
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this.element)
|
||||||
|
.find('thead th.mdc-data-table__header-cell--with-sort')
|
||||||
|
.on('click', this.onColumnClicked.bind(this));
|
||||||
|
$(this.element)
|
||||||
|
.find('.mdc-data-table__pagination-rows-per-page-select')
|
||||||
|
.on('selected', this.onPaginationSelected.bind(this));
|
||||||
|
$(this.element)
|
||||||
|
.find('.mdc-data-table__pagination-button')
|
||||||
|
.on('click', this.onPaginationButtonClicked.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div className="mdc-data-table" {...this.attrs.all()}>
|
||||||
|
<div className="mdc-data-table__table-container">
|
||||||
|
<table
|
||||||
|
className="mdc-data-table__table"
|
||||||
|
aria-label={this.attrs.get('aria-label')}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr className="mdc-data-table__header-row">
|
||||||
|
{this.attrs.has('checkable') && <TableColumn type="checkbox" />}
|
||||||
|
{this.columns}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="mdc-data-table__content">{this.rows}</tbody>
|
||||||
|
|
||||||
|
{this.footer}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{this.attrs.has('paginated') && (
|
||||||
|
<div className="mdc-data-table__pagination">
|
||||||
|
<div className="mdc-data-table__pagination-trailing">
|
||||||
|
<div className="mdc-data-table__pagination-rows-per-page">
|
||||||
|
<div className="mdc-data-table__pagination-rows-per-page-label">
|
||||||
|
{__('Righe per pagina')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<material-select
|
||||||
|
outlined
|
||||||
|
className="mdc-data-table__pagination-rows-per-page-select"
|
||||||
|
fixedMenuPosition
|
||||||
|
// @ts-ignore
|
||||||
|
style="--mdc-select-width: 112px; --mdc-select-height: 36px; --mdc-menu-item-height: 36px;"
|
||||||
|
>
|
||||||
|
{this.rowsPerPage.options.map((rowsPerPage) => (
|
||||||
|
<mwc-list-item
|
||||||
|
key={rowsPerPage}
|
||||||
|
value={String(rowsPerPage)}
|
||||||
|
selected={this.rowsPerPage.value === rowsPerPage}
|
||||||
|
>
|
||||||
|
{rowsPerPage}
|
||||||
|
</mwc-list-item>
|
||||||
|
))}
|
||||||
|
</material-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mdc-data-table__pagination-navigation">
|
||||||
|
<div className="mdc-data-table__pagination-total">
|
||||||
|
{__(':start-:chunk di :total', {
|
||||||
|
start: this.rowsPerPage.currentStart + 1,
|
||||||
|
chunk: this.rowsPerPage.currentEnd,
|
||||||
|
total: this.rows.length
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<mwc-icon-button
|
||||||
|
className="mdc-data-table__pagination-button"
|
||||||
|
data-page="first"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Mdi icon="page-first" />
|
||||||
|
</mwc-icon-button>
|
||||||
|
<mwc-icon-button
|
||||||
|
className="mdc-data-table__pagination-button"
|
||||||
|
data-page="previous"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Mdi icon="chevron-left" />
|
||||||
|
</mwc-icon-button>
|
||||||
|
<mwc-icon-button
|
||||||
|
className="mdc-data-table__pagination-button"
|
||||||
|
data-page="next"
|
||||||
|
>
|
||||||
|
<Mdi icon="chevron-right" />
|
||||||
|
</mwc-icon-button>
|
||||||
|
<mwc-icon-button
|
||||||
|
className="mdc-data-table__pagination-button"
|
||||||
|
data-page="last"
|
||||||
|
>
|
||||||
|
<Mdi icon="page-last" />
|
||||||
|
</mwc-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mdc-data-table__progress-indicator">
|
||||||
|
<div className="mdc-data-table__scrim" />
|
||||||
|
<mwc-linear-progress
|
||||||
|
className="mdc-data-table__linear-progress"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRows(children: Children[]): Children[] {
|
||||||
|
let rows = this.filterElements(children, TableRow);
|
||||||
|
|
||||||
|
if (this.attrs.has('checkable')) {
|
||||||
|
rows = rows.map<Children>((row: Children) => {
|
||||||
|
if (!row) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const rowNode = row as Vnode<TableRowAttributes>;
|
||||||
|
return (
|
||||||
|
<TableRow key={rowNode.key} checkable {...rowNode.attrs}>
|
||||||
|
{rowNode.children}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterElements(
|
||||||
|
elements: Children[],
|
||||||
|
tag: typeof TableRow | typeof TableColumn | typeof TableFooter | string
|
||||||
|
): Children[] {
|
||||||
|
const filtered = [];
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
if ((element as Vnode).tag === tag) {
|
||||||
|
filtered.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProgress(): Element & Partial<MWCLinearProgress> | null {
|
||||||
|
return this.element.querySelector('.mdc-data-table__progress-indicator mwc-linear-progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgress() {
|
||||||
|
this.manageProgress(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideProgress() {
|
||||||
|
this.manageProgress(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onColumnClicked(event: Event) {
|
||||||
|
this.showProgress();
|
||||||
|
const column: Cash = $(event.target as Element).closest('th');
|
||||||
|
const ascendingClass = 'mdc-data-table__header-cell--sorted';
|
||||||
|
// Clean previously sorted info and arrows
|
||||||
|
const columns = $(this.element).find('thead th');
|
||||||
|
columns.removeClass(ascendingClass);
|
||||||
|
columns.off('click').on('click', this.onColumnClicked.bind(this));
|
||||||
|
// Add ony one header to sort
|
||||||
|
column.addClass(ascendingClass);
|
||||||
|
// Do sorting
|
||||||
|
this.sortTable(column, false);
|
||||||
|
// Set/remove callbacks
|
||||||
|
column.off('click');
|
||||||
|
column.find('mwc-icon-button-toggle').on('click', () => {
|
||||||
|
this.sortTable(column);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTable(column: Cash, toggleClass = true) {
|
||||||
|
const cells = $(this.element)
|
||||||
|
.find(`tr td:nth-child(${column.index() + 1})`)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Handle button class
|
||||||
|
if (toggleClass) {
|
||||||
|
column.toggleClass('mdc-data-table__header-cell--sorted-descending');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = column.attr('type') === 'numeric';
|
||||||
|
const isDescending = column.hasClass(
|
||||||
|
'mdc-data-table__header-cell--sorted-descending'
|
||||||
|
);
|
||||||
|
cells.sort((a: HTMLElement, b: HTMLElement) => {
|
||||||
|
let aValue: string | number = a.textContent as string;
|
||||||
|
let bValue: string | number = b.textContent as string;
|
||||||
|
|
||||||
|
if (isNumeric) {
|
||||||
|
aValue = Number.parseFloat(aValue);
|
||||||
|
bValue = Number.parseFloat(bValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDescending) {
|
||||||
|
const temporary = aValue;
|
||||||
|
aValue = bValue;
|
||||||
|
bValue = temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
return aValue.localeCompare(bValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aValue < bValue ? -1 : (aValue > bValue ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const cell of cells) {
|
||||||
|
const row = $(cell).parent();
|
||||||
|
row.appendTo(row.parent());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaginationSelected(event: Event & {detail: {index: number}}) {
|
||||||
|
this.rowsPerPage.value = Number.parseInt(
|
||||||
|
$(event.target as Element)
|
||||||
|
.find('mwc-list-item')
|
||||||
|
.eq(event.detail.index)
|
||||||
|
.val() as string,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
this.rowsPerPage.currentStart = 0;
|
||||||
|
this.rowsPerPage.currentEnd = this.rowsPerPage.value;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaginationButtonClicked(event: Event) {
|
||||||
|
const button: Cash = $(event.target as Element);
|
||||||
|
this.paginate(button.data('page') as PaginationAction);
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate(action: PaginationAction) {
|
||||||
|
const increments = {
|
||||||
|
first: -this.rowsPerPage.currentStart,
|
||||||
|
next: this.rowsPerPage.value,
|
||||||
|
previous: -this.rowsPerPage.value,
|
||||||
|
last: this.rows.length - this.rowsPerPage.currentStart
|
||||||
|
};
|
||||||
|
const increment = increments[action];
|
||||||
|
|
||||||
|
if (action !== 'first') {
|
||||||
|
this.rowsPerPage.currentStart += increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action !== 'last') {
|
||||||
|
this.rowsPerPage.currentEnd += increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginationButtons = this.element.querySelectorAll(
|
||||||
|
'.mdc-data-table__pagination-button'
|
||||||
|
);
|
||||||
|
const disabled = {
|
||||||
|
first: this.rowsPerPage.currentStart === 0,
|
||||||
|
previous: this.rowsPerPage.currentStart === 0,
|
||||||
|
next: this.rowsPerPage.currentEnd >= this.rows.length,
|
||||||
|
last: this.rowsPerPage.currentEnd >= this.rows.length
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const button of paginationButtons) {
|
||||||
|
const buttonElement = $(button);
|
||||||
|
const buttonAction = buttonElement.data('page') as PaginationAction;
|
||||||
|
buttonElement.prop('disabled', disabled[buttonAction]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private manageProgress(show: boolean) {
|
||||||
|
$(this.element).toggleClass('mdc-data-table--in-progress');
|
||||||
|
const progress = this.getProgress();
|
||||||
|
if (progress) {
|
||||||
|
(progress as MWCLinearProgress)[show ? 'open' : 'close']();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,41 @@
|
|||||||
import {type Cash} from 'cash-dom/dist/cash';
|
import type {Cash} from 'cash-dom/dist/cash';
|
||||||
import {inRange} from 'lodash-es';
|
import {inRange} from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import type {
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
import Component from '../Component.jsx';
|
import Component from '../Component';
|
||||||
|
|
||||||
export default class TableCell extends Component {
|
declare global {
|
||||||
static propTypes = {
|
namespace JSX {
|
||||||
type: PropTypes.string
|
interface IntrinsicElements {
|
||||||
};
|
TableCell: TableCell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view(vnode) {
|
type Attributes = {type?: string};
|
||||||
|
|
||||||
|
export default class TableCell extends Component<Attributes> {
|
||||||
|
view(vnode: Vnode) {
|
||||||
this.attrs.addClassNames('mdc-data-table__cell', {
|
this.attrs.addClassNames('mdc-data-table__cell', {
|
||||||
[`mdc-data-table__cell--${this.attrs.get('type')}`]: this.attrs.has('type')
|
[`mdc-data-table__cell--${this.attrs.get('type') as string}`]: this.attrs.has(
|
||||||
|
'type'
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((!vnode.children || vnode.children.length === 0) && this.attrs.get('type') === 'checkbox') {
|
if (
|
||||||
vnode.children = <mwc-checkbox className="mdc-data-table__row-checkbox"/>;
|
(!Array.isArray(vnode.children) || vnode.children.length === 0)
|
||||||
|
&& this.attrs.get('type') === 'checkbox'
|
||||||
|
) {
|
||||||
|
vnode.children = [<mwc-checkbox key="checkbox" className="mdc-data-table__row-checkbox" />];
|
||||||
}
|
}
|
||||||
|
|
||||||
return <td {...this.attrs.all()}>{vnode.children}</td>;
|
return <td {...this.attrs.all()}>{vnode.children}</td>;
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode: VnodeDOM<Attributes>) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
const checkboxes = (): Cash => $(this.element)
|
const checkboxes = (): Cash => $(this.element)
|
||||||
@ -32,8 +46,9 @@ export default class TableCell extends Component {
|
|||||||
cell.children('mwc-checkbox').on('change', () => {
|
cell.children('mwc-checkbox').on('change', () => {
|
||||||
const row = cell.parent();
|
const row = cell.parent();
|
||||||
row.toggleClass('mdc-data-table__row--selected');
|
row.toggleClass('mdc-data-table__row--selected');
|
||||||
|
const headerCheckbox = cell
|
||||||
const headerCheckbox = cell.closest('.mdc-data-table').find('thead th mwc-checkbox');
|
.closest('.mdc-data-table')
|
||||||
|
.find('thead th mwc-checkbox');
|
||||||
const checks = checkboxes();
|
const checks = checkboxes();
|
||||||
const checked = checks.filter('[checked]');
|
const checked = checks.filter('[checked]');
|
||||||
|
|
@ -1,125 +0,0 @@
|
|||||||
import '@material/mwc-icon-button-toggle';
|
|
||||||
|
|
||||||
import {type Cash} from 'cash-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Component from '../Component.jsx';
|
|
||||||
import Mdi from '../Mdi.jsx';
|
|
||||||
|
|
||||||
export default class TableColumn extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
type: PropTypes.oneOf(['numeric', 'checkbox']),
|
|
||||||
id: PropTypes.string,
|
|
||||||
sortable: PropTypes.bool,
|
|
||||||
filterable: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
this.attrs.addClassNames('mdc-data-table__header-cell', {
|
|
||||||
[`mdc-data-table__header-cell--${this.attrs.get('type')}`]: this.attrs.has('type')
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.attrs.has('sortable')) {
|
|
||||||
this.attrs.addClassNames('mdc-data-table__header-cell--with-sort');
|
|
||||||
this.attrs.put('aria-sort', 'none').put('data-column-id', this.attrs.get('id'));
|
|
||||||
|
|
||||||
vnode.children = (
|
|
||||||
<div className="mdc-data-table__header-cell-wrapper">
|
|
||||||
<mwc-icon-button-toggle style="--mdc-icon-button-size: 28px; display: none;">
|
|
||||||
<Mdi icon="arrow-down-thin" slot="onIcon"/>
|
|
||||||
<Mdi icon="arrow-up-thin" slot="offIcon" />
|
|
||||||
</mwc-icon-button-toggle>
|
|
||||||
|
|
||||||
<div className="mdc-data-table__header-cell-label">
|
|
||||||
{vnode.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!vnode.children || vnode.children.length === 0) && this.attrs.get('type') === 'checkbox') {
|
|
||||||
vnode.children = <mwc-checkbox className="mdc-data-table__header-row-checkbox" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attrs.get('type') !== 'checkbox' && this.attrs.has('filterable')) {
|
|
||||||
vnode.children = (
|
|
||||||
<>
|
|
||||||
{vnode.children}
|
|
||||||
<div style="margin-top: 8px;">
|
|
||||||
<text-field outlined className="mdc-data-table__filter-textfield" label={__('Filtro')} compact/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <th {...this.attrs.all()} role="columnheader" scope="col">{vnode.children}</th>;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
if (this.attrs.get('type') === 'checkbox') {
|
|
||||||
const checkbox = $(this.element)
|
|
||||||
.children('.mdc-data-table__header-row-checkbox');
|
|
||||||
|
|
||||||
checkbox.on('change', this.onCheckboxClicked.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle click on column (add arrows)
|
|
||||||
const observer = new MutationObserver(this.onClassChanged.bind(this));
|
|
||||||
observer.observe(this.element, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class']
|
|
||||||
});
|
|
||||||
|
|
||||||
$(this.element).find('.mdc-data-table__filter-textfield').on('input', this.onFilterInput.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onCheckboxClicked(event: Event) {
|
|
||||||
const row: Cash = $(this.element)
|
|
||||||
.closest('table')
|
|
||||||
.find('tbody tr[checkable]');
|
|
||||||
|
|
||||||
const selectedClass = 'mdc-data-table__row--selected';
|
|
||||||
if (event.target.checked) {
|
|
||||||
row.addClass(selectedClass);
|
|
||||||
} else {
|
|
||||||
row.removeClass(selectedClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
row.find('mwc-checkbox').prop('checked', event.target.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClassChanged(mutations: MutationRecord[]) {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
const {classList} = mutation.target;
|
|
||||||
const ascendingClass = 'mdc-data-table__header-cell--sorted';
|
|
||||||
const descendingClass = 'mdc-data-table__header-cell--sorted-descending';
|
|
||||||
|
|
||||||
const onValue = classList.contains(descendingClass);
|
|
||||||
|
|
||||||
const button: Cash = $(this.element).find('mwc-icon-button-toggle');
|
|
||||||
button.prop('on', onValue);
|
|
||||||
|
|
||||||
if (classList.contains(ascendingClass) || classList.contains(descendingClass)) {
|
|
||||||
$(this.element).css('cursor', 'auto');
|
|
||||||
button.show();
|
|
||||||
} else if (!classList.contains(ascendingClass) && !classList.contains(descendingClass)) {
|
|
||||||
$(this.element).css('cursor', 'pointer');
|
|
||||||
button.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterInput(event: InputEvent) {
|
|
||||||
const index = $(this.element).index();
|
|
||||||
const rows: Cash = $(this.element).closest('table').find('tbody tr');
|
|
||||||
rows.hide();
|
|
||||||
rows.filter((index_, element) => (
|
|
||||||
$(element)
|
|
||||||
.find(`td:nth-child(${index + 1})`)
|
|
||||||
.text()
|
|
||||||
.search(event.target.value) !== -1
|
|
||||||
)).show();
|
|
||||||
}
|
|
||||||
}
|
|
160
resources/js/Components/DataTable/TableColumn.tsx
Normal file
160
resources/js/Components/DataTable/TableColumn.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import '@material/mwc-icon-button-toggle';
|
||||||
|
|
||||||
|
import type {Cash} from 'cash-dom';
|
||||||
|
import type {
|
||||||
|
Children,
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
|
import Component from '../Component';
|
||||||
|
import Mdi from '../Mdi';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
TableColumn: TableColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attributes = {
|
||||||
|
type?: 'numeric' | 'checkbox',
|
||||||
|
id?: string,
|
||||||
|
sortable?: boolean,
|
||||||
|
filterable?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class TableColumn extends Component<Attributes> {
|
||||||
|
view(vnode: Vnode) {
|
||||||
|
this.attrs.addClassNames('mdc-data-table__header-cell', {
|
||||||
|
[`mdc-data-table__header-cell--${this.attrs.get(
|
||||||
|
'type'
|
||||||
|
) as string}`]: this.attrs.has('type')
|
||||||
|
});
|
||||||
|
|
||||||
|
let {children}: {children?: Children} = vnode;
|
||||||
|
|
||||||
|
if (this.attrs.has('sortable')) {
|
||||||
|
this.attrs.addClassNames('mdc-data-table__header-cell--with-sort');
|
||||||
|
this.attrs
|
||||||
|
.put('aria-sort', 'none')
|
||||||
|
.put('data-column-id', this.attrs.get('id'));
|
||||||
|
children = (
|
||||||
|
<div className="mdc-data-table__header-cell-wrapper">
|
||||||
|
<mwc-icon-button-toggle style="--mdc-icon-button-size: 28px; display: none;">
|
||||||
|
<Mdi icon="arrow-down-thin" slot="onIcon" />
|
||||||
|
<Mdi icon="arrow-up-thin" slot="offIcon" />
|
||||||
|
</mwc-icon-button-toggle>
|
||||||
|
|
||||||
|
<div className="mdc-data-table__header-cell-label">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((
|
||||||
|
(!children || (Array.isArray(children) && children.length === 0))
|
||||||
|
&& this.attrs.get('type') === 'checkbox'
|
||||||
|
)) {
|
||||||
|
children = <mwc-checkbox className="mdc-data-table__header-row-checkbox" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.attrs.get('type') !== 'checkbox' && this.attrs.has('filterable')) {
|
||||||
|
children = (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<text-field
|
||||||
|
outlined
|
||||||
|
className="mdc-data-table__filter-textfield"
|
||||||
|
label={__('Filtro') as string}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th {...this.attrs.all()} role="columnheader" scope="col">
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM<Attributes>) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
if (this.attrs.get('type') === 'checkbox') {
|
||||||
|
const checkbox = $(this.element).children(
|
||||||
|
'.mdc-data-table__header-row-checkbox'
|
||||||
|
);
|
||||||
|
checkbox.on('change', this.onCheckboxClicked.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle click on a column (add arrows)
|
||||||
|
const observer = new MutationObserver(this.onClassChanged.bind(this));
|
||||||
|
observer.observe(this.element, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
});
|
||||||
|
$(this.element)
|
||||||
|
.find('.mdc-data-table__filter-textfield')
|
||||||
|
.on('input', this.onFilterInput.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onCheckboxClicked(event: Event) {
|
||||||
|
const row: Cash = $(this.element)
|
||||||
|
.closest('table')
|
||||||
|
.find('tbody tr[checkable]');
|
||||||
|
const selectedClass = 'mdc-data-table__row--selected';
|
||||||
|
const checkbox = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
row.toggleClass(selectedClass, checkbox.checked);
|
||||||
|
|
||||||
|
row.find('mwc-checkbox').prop('checked', checkbox.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClassChanged(mutations: MutationRecord[]) {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
const {classList} = mutation.target as HTMLElement;
|
||||||
|
const ascendingClass = 'mdc-data-table__header-cell--sorted';
|
||||||
|
const descendingClass = 'mdc-data-table__header-cell--sorted-descending';
|
||||||
|
const onValue = classList.contains(descendingClass);
|
||||||
|
const button: Cash = $(this.element).find('mwc-icon-button-toggle');
|
||||||
|
button.prop('on', onValue);
|
||||||
|
|
||||||
|
if (
|
||||||
|
classList.contains(ascendingClass)
|
||||||
|
|| classList.contains(descendingClass)
|
||||||
|
) {
|
||||||
|
$(this.element).css('cursor', 'auto');
|
||||||
|
button.show();
|
||||||
|
} else if (
|
||||||
|
!classList.contains(ascendingClass)
|
||||||
|
&& !classList.contains(descendingClass)
|
||||||
|
) {
|
||||||
|
$(this.element).css('cursor', 'pointer');
|
||||||
|
button.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterInput(event: InputEvent) {
|
||||||
|
const index = $(this.element).index();
|
||||||
|
const rows: Cash = $(this.element).closest('table').find('tbody tr');
|
||||||
|
const {value} = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
rows.hide();
|
||||||
|
rows
|
||||||
|
.filter(
|
||||||
|
(index_, element) => $(element)
|
||||||
|
.find(`td:nth-child(${index + 1})`)
|
||||||
|
.text()
|
||||||
|
.search(value) !== -1
|
||||||
|
)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
import Component from '../Component.jsx';
|
|
||||||
|
|
||||||
export default class TableFooter extends Component {
|
|
||||||
view(vnode) {
|
|
||||||
return <tfoot {...this.attrs.all()}>{vnode.children}</tfoot>;
|
|
||||||
}
|
|
||||||
}
|
|
9
resources/js/Components/DataTable/TableFooter.tsx
Normal file
9
resources/js/Components/DataTable/TableFooter.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type {Vnode} from 'mithril';
|
||||||
|
|
||||||
|
import Component from '../Component';
|
||||||
|
|
||||||
|
export default class TableFooter extends Component<{}> {
|
||||||
|
view(vnode: Vnode) {
|
||||||
|
return <tfoot {...this.attrs.all()}>{vnode.children}</tfoot>;
|
||||||
|
}
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
import '@material/mwc-checkbox';
|
|
||||||
|
|
||||||
import {collect} from 'collect.js';
|
|
||||||
import {
|
|
||||||
type Children,
|
|
||||||
type Vnode
|
|
||||||
} from 'mithril';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Component from '../Component.jsx';
|
|
||||||
import TableCell from './TableCell.jsx';
|
|
||||||
|
|
||||||
export default class TableRow extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
checkable: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
this.attrs.addClassNames('mdc-data-table__row');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr {...this.attrs.all()}>
|
|
||||||
{this.checkbox(vnode.children)}
|
|
||||||
{vnode.children}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkbox(children: Children[]): Children {
|
|
||||||
if (!this.attrs.has('checkable')) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const child: Vnode of children) {
|
|
||||||
const attributes = collect(child.attrs)
|
|
||||||
if (attributes.get('type') === 'checkbox') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <TableCell type="checkbox"/>;
|
|
||||||
}
|
|
||||||
}
|
|
45
resources/js/Components/DataTable/TableRow.tsx
Normal file
45
resources/js/Components/DataTable/TableRow.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import '@material/mwc-checkbox';
|
||||||
|
|
||||||
|
import {collect} from 'collect.js';
|
||||||
|
import type {Children, Vnode} from 'mithril';
|
||||||
|
|
||||||
|
import Component from '../Component';
|
||||||
|
import TableCell from './TableCell';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'TableRow': TableRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableRowAttributes = {checkable?: boolean};
|
||||||
|
|
||||||
|
export default class TableRow extends Component<TableRowAttributes> {
|
||||||
|
view(vnode: Vnode<TableRowAttributes>) {
|
||||||
|
this.attrs.addClassNames('mdc-data-table__row');
|
||||||
|
return (
|
||||||
|
<tr {...this.attrs.all()}>
|
||||||
|
{this.checkbox(vnode.children as Children[])}
|
||||||
|
{vnode.children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox(children: Children[]): Children {
|
||||||
|
if (!this.attrs.has('checkable')) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
const attributes = collect((child as Vnode).attrs);
|
||||||
|
|
||||||
|
if (attributes.get('type') === 'checkbox') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TableCell type="checkbox" />;
|
||||||
|
}
|
||||||
|
}
|
6
resources/js/Components/DataTable/index.js
vendored
6
resources/js/Components/DataTable/index.js
vendored
@ -1,6 +0,0 @@
|
|||||||
export { default as DataTable } from './DataTable.jsx';
|
|
||||||
export { default as TableCell } from './TableCell.jsx';
|
|
||||||
export { default as TableColumn } from './TableColumn.jsx';
|
|
||||||
export { default as TableFooter } from './TableFooter.jsx';
|
|
||||||
export { default as TableRow } from './TableRow.jsx';
|
|
||||||
|
|
7
resources/js/Components/DataTable/index.ts
Normal file
7
resources/js/Components/DataTable/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
export {default as DataTable} from './DataTable';
|
||||||
|
export {default as TableCell} from './TableCell';
|
||||||
|
export {default as TableColumn} from './TableColumn';
|
||||||
|
export {default as TableFooter} from './TableFooter';
|
||||||
|
export * from './TableRow';
|
@ -1,58 +0,0 @@
|
|||||||
import '@material/mwc-circular-progress';
|
|
||||||
|
|
||||||
import {type Button} from '@material/mwc-button';
|
|
||||||
import type CSS from 'csstype';
|
|
||||||
import {type ClassComponent} from 'mithril';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Component from './Component.jsx';
|
|
||||||
import Mdi from './Mdi.jsx';
|
|
||||||
|
|
||||||
class LoadingButton extends Component implements ClassComponent<{ ...Button, icon?: string }> {
|
|
||||||
static propTypes = {
|
|
||||||
icon: PropTypes.string,
|
|
||||||
raised: PropTypes.bool,
|
|
||||||
outlined: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mwc-button {...this.attrs.all()}>
|
|
||||||
<span slot="icon" style="display: inline;">
|
|
||||||
<mwc-circular-progress
|
|
||||||
indeterminate
|
|
||||||
style={this.getCSSProperties()}/>
|
|
||||||
{this.attrs.has('icon') ? <Mdi icon={this.attrs.get('icon')}/> : ''}
|
|
||||||
</span>
|
|
||||||
</mwc-button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCSSProperties() {
|
|
||||||
const css: CSS.Properties<> = {
|
|
||||||
display: 'none',
|
|
||||||
verticalAlign: 'bottom'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.attrs.has('raised')) {
|
|
||||||
css['--mdc-theme-primary'] = '#ffffff';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attrs.has('icon')) {
|
|
||||||
css.marginRight = '8px';
|
|
||||||
}
|
|
||||||
|
|
||||||
return css;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
$(this.element)
|
|
||||||
.find('mwc-circular-progress')
|
|
||||||
.attr('density', -7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoadingButton;
|
|
66
resources/js/Components/LoadingButton.tsx
Normal file
66
resources/js/Components/LoadingButton.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import '@material/mwc-circular-progress';
|
||||||
|
import '@material/mwc-button';
|
||||||
|
|
||||||
|
import type {Button} from '@material/mwc-button';
|
||||||
|
import type CSS from 'csstype';
|
||||||
|
import type {VnodeDOM} from 'mithril';
|
||||||
|
|
||||||
|
import Component from './Component';
|
||||||
|
import Mdi from './Mdi';
|
||||||
|
|
||||||
|
type Attributes = Partial<Button> & {
|
||||||
|
icon?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
LoadingButton: LoadingButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingButton extends Component<Attributes> {
|
||||||
|
view() {
|
||||||
|
return ( // @ts-ignore
|
||||||
|
<mwc-button {...this.attrs.all()}>
|
||||||
|
<span slot="icon" style="display: inline;">
|
||||||
|
<mwc-circular-progress
|
||||||
|
indeterminate
|
||||||
|
// @ts-ignore
|
||||||
|
style={this.getCSSProperties()}
|
||||||
|
/>
|
||||||
|
{this.attrs.has('icon') ? (
|
||||||
|
<Mdi icon={this.attrs.get('icon')} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</mwc-button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCSSProperties() {
|
||||||
|
const css: CSS.Properties & Record<string, string> = {
|
||||||
|
display: 'none',
|
||||||
|
verticalAlign: 'bottom'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.attrs.has('raised')) {
|
||||||
|
css['--mdc-theme-primary'] = '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.attrs.has('icon')) {
|
||||||
|
css.marginRight = '8px';
|
||||||
|
}
|
||||||
|
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM<Attributes>) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
$(this.element).find('mwc-circular-progress').attr('density', String(-7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingButton;
|
@ -1,23 +1,27 @@
|
|||||||
import {type ClassComponent} from 'mithril';
|
import Component from './Component';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Component from './Component.jsx';
|
type Attributes = {
|
||||||
|
icon?: string
|
||||||
|
};
|
||||||
|
|
||||||
export default class Mdi extends Component implements ClassComponent<{icon?: string}> {
|
declare global {
|
||||||
static propTypes = {
|
namespace JSX {
|
||||||
icon: PropTypes.string
|
interface IntrinsicElements {
|
||||||
};
|
Mdi: Mdi
|
||||||
|
}
|
||||||
view(vnode) {
|
|
||||||
this.attrs.addClassNames('mdi', `mdi-${this.attrs.pull('icon')}`);
|
|
||||||
return <i {...this.attrs.all()} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default class Mdi extends Component<Attributes> {
|
||||||
|
view() {
|
||||||
|
this.attrs.addClassNames('mdi', `mdi-${this.attrs.pull('icon') as string}`);
|
||||||
|
return <i {...this.attrs.all()} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
Quando MWC supporterà pienamente le icone SVG si potrà fare così:
|
Quando MWC supporterà pienamente le icone SVG si potrà fare così:
|
||||||
import * as mdi from '@mdi/js';
|
import * as mdi from '@mdi/js';
|
||||||
import {camelCase} from 'lodash-es/string';
|
import {camelCase} from 'lodash/string';
|
||||||
|
|
||||||
return <svg class={`mdi ${vnode.attrs.class ?? ''}`}
|
return <svg class={`mdi ${vnode.attrs.class ?? ''}`}
|
||||||
{...vnode.attrs} viewBox={vnode.attrs.viewBox ?? '0 0 24 24'}>
|
{...vnode.attrs} viewBox={vnode.attrs.viewBox ?? '0 0 24 24'}>
|
@ -1,17 +0,0 @@
|
|||||||
import Component from './Component.jsx';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Page` component
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Page extends Component {
|
|
||||||
page: {
|
|
||||||
component: string,
|
|
||||||
locale: string,
|
|
||||||
props: {...},
|
|
||||||
url: string,
|
|
||||||
version: string,
|
|
||||||
...
|
|
||||||
} = JSON.parse($('#app').attr('data-page'));
|
|
||||||
}
|
|
19
resources/js/Components/Page.ts
Normal file
19
resources/js/Components/Page.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Component from './Component';
|
||||||
|
/**
|
||||||
|
* The `Page` component
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PageAttributes = {
|
||||||
|
component: string
|
||||||
|
locale: string
|
||||||
|
props: Record<string, any>
|
||||||
|
url: string
|
||||||
|
version: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Page extends Component<{}> {
|
||||||
|
page: PageAttributes = JSON.parse($('#app').attr('data-page') as string) as PageAttributes;
|
||||||
|
title?: string;
|
||||||
|
}
|
@ -1,376 +0,0 @@
|
|||||||
import '@maicol07/mwc-layout-grid';
|
|
||||||
import '@material/mwc-dialog';
|
|
||||||
import '@material/mwc-fab';
|
|
||||||
import '@material/mwc-snackbar';
|
|
||||||
|
|
||||||
import type {Cash} from 'cash-dom/dist/cash';
|
|
||||||
import collect, {Collection} from 'collect.js';
|
|
||||||
import {Children} from 'mithril';
|
|
||||||
|
|
||||||
import {Model} from '../../Models';
|
|
||||||
import type {
|
|
||||||
SelectT,
|
|
||||||
TextAreaT,
|
|
||||||
TextFieldT
|
|
||||||
} from '../../types';
|
|
||||||
import {
|
|
||||||
getFormData,
|
|
||||||
isFormValid,
|
|
||||||
showSnackbar
|
|
||||||
} from '../../utils';
|
|
||||||
import type {
|
|
||||||
TextArea,
|
|
||||||
TextField
|
|
||||||
} from '../../WebComponents';
|
|
||||||
import DataTable from '../DataTable/DataTable.jsx';
|
|
||||||
import TableCell from '../DataTable/TableCell.jsx';
|
|
||||||
import TableColumn from '../DataTable/TableColumn.jsx';
|
|
||||||
import TableRow from '../DataTable/TableRow.jsx';
|
|
||||||
import LoadingButton from '../LoadingButton.jsx';
|
|
||||||
import Mdi from '../Mdi.jsx';
|
|
||||||
import Page from '../Page.jsx';
|
|
||||||
|
|
||||||
|
|
||||||
export type ColumnT = {
|
|
||||||
id?: string,
|
|
||||||
title: string,
|
|
||||||
type?: 'checkbox' | 'numeric',
|
|
||||||
valueModifier?: (instance: Model, prop: string) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SectionT = {
|
|
||||||
id?: string,
|
|
||||||
heading?: string,
|
|
||||||
columns?: number,
|
|
||||||
fields: TextFieldT[] | TextAreaT | SelectT[] | { [string]: TextFieldT | TextAreaT | SelectT }
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ColumnsT = { [string]: [string] | ColumnT };
|
|
||||||
export type RowsT = Collection<Model>;
|
|
||||||
export type SectionsT = { [string]: SectionT } | SectionT[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export class RecordsPage extends Page {
|
|
||||||
columns: ColumnsT;
|
|
||||||
rows: RowsT = collect({});
|
|
||||||
|
|
||||||
sections: SectionsT;
|
|
||||||
|
|
||||||
dialogs: Children[];
|
|
||||||
|
|
||||||
recordDialogMaxWidth: string | number = 'auto';
|
|
||||||
|
|
||||||
model: typeof Model;
|
|
||||||
|
|
||||||
customSetter: (model: Model, fields: Cash) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What fields should take precedence when saving the record
|
|
||||||
*/
|
|
||||||
fieldsPrecedence: string[] = [];
|
|
||||||
|
|
||||||
async oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
const response = await this.model.all();
|
|
||||||
const data: Model[] = response.getData();
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
for (const record of data) {
|
|
||||||
this.rows.put(record.id, record);
|
|
||||||
}
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onupdate(vnode) {
|
|
||||||
const rows: Cash = $('.mdc-data-table__row[data-model-id]');
|
|
||||||
if (rows.length > 0) {
|
|
||||||
rows.on(
|
|
||||||
'click',
|
|
||||||
async (event: PointerEvent) => {
|
|
||||||
if (event.target.tagName === 'MWC-CHECKBOX') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.updateRecord($(event.target)
|
|
||||||
.parent('tr')
|
|
||||||
.data('model-id'));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tableColumns(): Children {
|
|
||||||
return collect(this.columns)
|
|
||||||
.map(
|
|
||||||
(column: ColumnT | string, id: string) => (
|
|
||||||
<TableColumn id={id} key={id} {...((typeof column === 'object') ? column : {})} sortable
|
|
||||||
filterable>
|
|
||||||
{typeof column === 'string' ? column : column.title}
|
|
||||||
</TableColumn>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
tableRows(): Children {
|
|
||||||
if (this.rows.isEmpty()) {
|
|
||||||
return (
|
|
||||||
<TableRow key="no-data">
|
|
||||||
<TableCell colspan={collect(this.columns)
|
|
||||||
.count()} style="text-align: center;">
|
|
||||||
{__('Non sono presenti dati')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.rows.map((instance: Model, index) => (
|
|
||||||
<TableRow key={index} data-model-id={instance.id} style="cursor: pointer">
|
|
||||||
{collect(this.columns).map((column, index_) => (
|
|
||||||
<TableCell key={index_}>
|
|
||||||
{this.getModelValue(instance, column.id ?? index_)}
|
|
||||||
</TableCell>
|
|
||||||
)).toArray()}
|
|
||||||
</TableRow>
|
|
||||||
)).toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateRecord(id: number) {
|
|
||||||
const response = await this.model.find(id);
|
|
||||||
const instance = response.getData();
|
|
||||||
const dialog = $('mwc-dialog#add-record-dialog');
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
dialog.find('text-field, text-area, material-select')
|
|
||||||
.each(
|
|
||||||
(index, field: TextFieldT | TextAreaT | SelectT) => this.getModelValue(instance, field.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
dialog.find('mwc-button#delete-button')
|
|
||||||
.show()
|
|
||||||
.on('click', () => {
|
|
||||||
const confirmDialog = $('mwc-dialog#confirm-delete-record-dialog');
|
|
||||||
const confirmButton = confirmDialog.find('mwc-button#confirm-button');
|
|
||||||
const loading: Cash = confirmButton.find('mwc-circular-progress');
|
|
||||||
|
|
||||||
confirmButton.on('click', async () => {
|
|
||||||
loading.show();
|
|
||||||
|
|
||||||
await instance.delete();
|
|
||||||
|
|
||||||
// noinspection JSUnresolvedVariable
|
|
||||||
this.rows.forget(instance.id);
|
|
||||||
m.redraw();
|
|
||||||
await showSnackbar(__('Record eliminato!'), 4000);
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.hide();
|
|
||||||
|
|
||||||
confirmDialog.get(0)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.get(0)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
recordDialog() {
|
|
||||||
return (
|
|
||||||
<mwc-dialog id="add-record-dialog" class="record-dialog"
|
|
||||||
heading={__('Aggiungi nuovo record')}
|
|
||||||
style={`--mdc-dialog-max-width: ${this.recordDialogMaxWidth}`}>
|
|
||||||
<form>
|
|
||||||
<text-field id="id" name="id" style="display: none;" data-default-value=""/>
|
|
||||||
{(() => {
|
|
||||||
const sections = collect(this.sections);
|
|
||||||
|
|
||||||
return sections.map((section, index) => (
|
|
||||||
<>
|
|
||||||
<div id={section.id ?? index}>
|
|
||||||
<h4 class="mdc-typography--overline">{section.heading}</h4>
|
|
||||||
<mwc-layout-grid>
|
|
||||||
{(() => {
|
|
||||||
const fields = collect(section.fields);
|
|
||||||
|
|
||||||
return fields.map((field, fieldIndex) => (
|
|
||||||
<mwc-layout-grid-cell key={fieldIndex}
|
|
||||||
span={12 / (section.columns ?? 3)}>
|
|
||||||
{m(field.elementType ?? this.getElementFromType(field.type), {
|
|
||||||
...field,
|
|
||||||
id: field.id ?? fieldIndex,
|
|
||||||
name: field.name ?? field.id ?? fieldIndex,
|
|
||||||
'data-default-value': field.value ?? (field.selected ?? '')
|
|
||||||
}, this.getFieldBody(field))}
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
))
|
|
||||||
.toArray();
|
|
||||||
})()}
|
|
||||||
</mwc-layout-grid>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
.toArray();
|
|
||||||
})()}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<LoadingButton type="submit" slot="primaryAction" label={__('Conferma')}/>
|
|
||||||
<mwc-button slot="secondaryAction" dialogAction="cancel">
|
|
||||||
{__('Annulla')}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button id="delete-button" slot="secondaryAction" label={__('Elimina')}
|
|
||||||
style="--mdc-theme-primary: var(--mdc-theme-error, red); float: left; display: none;">
|
|
||||||
<Mdi icon="delete-outline" slot="icon"/>
|
|
||||||
</mwc-button>
|
|
||||||
</mwc-dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRecordDialog(): Children {
|
|
||||||
return (
|
|
||||||
<mwc-dialog id="confirm-delete-record-dialog">
|
|
||||||
<p>{__('Sei sicuro di voler eliminare questo record?')}</p>
|
|
||||||
<LoadingButton id="confirm-button" slot="primaryAction" label={__('Sì')}/>
|
|
||||||
<mwc-button slot="secondaryAction" dialogAction="discard" label={__('No')}/>
|
|
||||||
</mwc-dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>{this.title}</h2>
|
|
||||||
<DataTable checkable paginated>
|
|
||||||
{this.tableColumns()}
|
|
||||||
{this.tableRows()}
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<mwc-fab id="add-record" label={__('Aggiungi')} class="sticky">
|
|
||||||
<Mdi icon="plus" slot="icon"/>
|
|
||||||
</mwc-fab>
|
|
||||||
{this.recordDialog()}
|
|
||||||
{this.deleteRecordDialog()}
|
|
||||||
{this.dialogs}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
const fab: Cash = $('mwc-fab#add-record');
|
|
||||||
const dialog: Cash = fab.next('mwc-dialog#add-record-dialog');
|
|
||||||
const form: Cash = dialog.find('form');
|
|
||||||
|
|
||||||
fab.on('click', () => {
|
|
||||||
form.find('text-field, text-area, material-select')
|
|
||||||
.each((index, field) => {
|
|
||||||
field.value = $(field)
|
|
||||||
.data('default-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.find('mwc-button[type="submit"] mwc-circular-progress')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
dialog.find('mwc-button#delete-button')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
dialog.get(0)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
const button = dialog.find('mwc-button[type="submit"]');
|
|
||||||
button.on('click', () => {
|
|
||||||
form.trigger('submit');
|
|
||||||
});
|
|
||||||
const loading: Cash = button.find('mwc-circular-progress');
|
|
||||||
|
|
||||||
form.on('submit', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
loading.show();
|
|
||||||
|
|
||||||
if (isFormValid(form)) {
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const instance: Model = new this.model();
|
|
||||||
|
|
||||||
if (this.customSetter) {
|
|
||||||
this.customSetter(instance, collect(getFormData(form)));
|
|
||||||
} else {
|
|
||||||
const fields = form.find('text-field, text-area, material-select');
|
|
||||||
fields.filter(this.fieldsPrecedence.map(value => `#${value}`).join(', '))
|
|
||||||
.each((index, field) => {
|
|
||||||
instance[field.id] = field.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
fields.each((index, field: TextField | TextArea) => {
|
|
||||||
instance[field.id] = field.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await instance.save();
|
|
||||||
if (response.getModelId()) {
|
|
||||||
dialog.get(0)
|
|
||||||
.close();
|
|
||||||
|
|
||||||
const model = response.getModel();
|
|
||||||
this.rows.put(model.id, model);
|
|
||||||
|
|
||||||
m.redraw();
|
|
||||||
await showSnackbar(__('Record salvato'), 4000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loading.hide();
|
|
||||||
await showSnackbar(__('Campi non validi. Controlla i dati inseriti'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getModelValue(model: Model, field: string): any {
|
|
||||||
const column = this.columns[field]
|
|
||||||
let value = model[field];
|
|
||||||
|
|
||||||
if (typeof column === 'object' && column.valueModifier) {
|
|
||||||
value = column.valueModifier(model, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getElementFromType(type: string) {
|
|
||||||
switch (type) {
|
|
||||||
case 'text':
|
|
||||||
return 'text-field';
|
|
||||||
case 'textarea':
|
|
||||||
return 'text-area';
|
|
||||||
case 'select':
|
|
||||||
return 'material-select';
|
|
||||||
/* Case 'checkbox':
|
|
||||||
case 'radio':
|
|
||||||
return Radio; */
|
|
||||||
default:
|
|
||||||
return 'text-field';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldBody(field: TextFieldT | TextAreaT | SelectT) {
|
|
||||||
const list = [];
|
|
||||||
switch (field.type) {
|
|
||||||
case 'select':
|
|
||||||
for (const option: { value: string, label: string } of field.options) {
|
|
||||||
list.push(<mwc-list-item key={option} value={option.value}>{option.label}</mwc-list-item>)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'checkbox':
|
|
||||||
return '';
|
|
||||||
case 'radio':
|
|
||||||
return '';
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.icon) {
|
|
||||||
list.push(<Mdi icon={field.icon} slot="icon"/>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
}
|
|
430
resources/js/Components/Pages/RecordsPage.tsx
Normal file
430
resources/js/Components/Pages/RecordsPage.tsx
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
import '@maicol07/mwc-layout-grid';
|
||||||
|
import '@material/mwc-dialog';
|
||||||
|
import '@material/mwc-fab';
|
||||||
|
import '@material/mwc-snackbar';
|
||||||
|
|
||||||
|
import type {Dialog as MWCDialog} from '@material/mwc-dialog';
|
||||||
|
import type {Cash} from 'cash-dom';
|
||||||
|
import collect, {type Collection} from 'collect.js';
|
||||||
|
import type {
|
||||||
|
Children,
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
|
||||||
|
import {Model} from '../../Models';
|
||||||
|
import type {
|
||||||
|
FieldT,
|
||||||
|
SelectT,
|
||||||
|
TextAreaT,
|
||||||
|
TextFieldT
|
||||||
|
} from '../../types';
|
||||||
|
import {getFormData, isFormValid, showSnackbar} from '../../utils';
|
||||||
|
import DataTable from '../DataTable/DataTable';
|
||||||
|
import TableCell from '../DataTable/TableCell';
|
||||||
|
import TableColumn from '../DataTable/TableColumn';
|
||||||
|
import TableRow from '../DataTable/TableRow';
|
||||||
|
import LoadingButton from '../LoadingButton';
|
||||||
|
import Mdi from '../Mdi';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
export type ColumnT = {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
type?: 'checkbox' | 'numeric'
|
||||||
|
valueModifier?: (instance: Model, property: string) => any
|
||||||
|
};
|
||||||
|
export type SectionT = {
|
||||||
|
id?: string
|
||||||
|
heading?: string
|
||||||
|
columns?: number
|
||||||
|
fields:
|
||||||
|
| TextFieldT[]
|
||||||
|
| TextAreaT
|
||||||
|
| SelectT[]
|
||||||
|
| Record<string, TextFieldT | TextAreaT | SelectT>
|
||||||
|
};
|
||||||
|
export type ColumnsT = Record<string, string | ColumnT>;
|
||||||
|
export type RowsT = Collection<Model>;
|
||||||
|
export type SectionsT = Record<string, SectionT> | SectionT[];
|
||||||
|
|
||||||
|
type IndexedModel = Model & {[prop: string]: any};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export class RecordsPage extends Page {
|
||||||
|
columns: ColumnsT;
|
||||||
|
rows: RowsT = collect({});
|
||||||
|
sections: SectionsT;
|
||||||
|
dialogs: Children[];
|
||||||
|
recordDialogMaxWidth: string | number = 'auto';
|
||||||
|
model: typeof Model;
|
||||||
|
customSetter: (model: Model, fields: Collection<File | string>) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What fields should take precedence when saving the record
|
||||||
|
*/
|
||||||
|
fieldsPrecedence: string[] = [];
|
||||||
|
|
||||||
|
async oninit(vnode: Vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
const response = await this.model.all();
|
||||||
|
const data = response.getData() as Model[];
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
for (const record of data) {
|
||||||
|
this.rows.put(record.getId(), record);
|
||||||
|
}
|
||||||
|
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onupdate(vnode: VnodeDOM) {
|
||||||
|
const rows: Cash = $('.mdc-data-table__row[data-model-id]');
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
rows.on('click', async (event: PointerEvent) => {
|
||||||
|
const cell = event.target as HTMLElement;
|
||||||
|
if (cell.tagName === 'MWC-CHECKBOX') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateRecord($(cell).parent('tr').data('model-id') as number);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableColumns(): JSX.Element[] {
|
||||||
|
return collect(this.columns)
|
||||||
|
// @ts-ignore
|
||||||
|
.map((column: ColumnT | string, id: string) => (
|
||||||
|
<TableColumn
|
||||||
|
id={id}
|
||||||
|
key={id}
|
||||||
|
{...(typeof column === 'object' ? column : {})}
|
||||||
|
sortable
|
||||||
|
filterable
|
||||||
|
>
|
||||||
|
{typeof column === 'string' ? column : column.title}
|
||||||
|
</TableColumn>
|
||||||
|
))
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRows(): Children {
|
||||||
|
if (this.rows.isEmpty()) {
|
||||||
|
return (
|
||||||
|
<TableRow key="no-data">
|
||||||
|
<TableCell
|
||||||
|
colspan={collect(this.columns).count()}
|
||||||
|
style="text-align: center;"
|
||||||
|
>
|
||||||
|
{__('Non sono presenti dati')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rows
|
||||||
|
.map((instance: IndexedModel, index: string) => (
|
||||||
|
<TableRow
|
||||||
|
key={index}
|
||||||
|
data-model-id={instance.getId()}
|
||||||
|
style="cursor: pointer"
|
||||||
|
>
|
||||||
|
{collect(this.columns)
|
||||||
|
.map((column, index_: string) => (
|
||||||
|
<TableCell key={index_}>
|
||||||
|
{this.getModelValue(instance, (typeof column === 'object' ? column.id : index_) as string)}
|
||||||
|
</TableCell>
|
||||||
|
))
|
||||||
|
.toArray()}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRecord(id: number) {
|
||||||
|
// @ts-ignore
|
||||||
|
const response = await this.model.find(id);
|
||||||
|
const instance = response.getData() as IndexedModel;
|
||||||
|
const dialog = $('mwc-dialog#add-record-dialog');
|
||||||
|
|
||||||
|
dialog
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
.find('text-field, text-area, material-select')
|
||||||
|
.each((index, field) => {
|
||||||
|
(field as HTMLInputElement).value = this.getModelValue(instance, field.id) as string;
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.find('mwc-button#delete-button')
|
||||||
|
.show()
|
||||||
|
.on('click', () => {
|
||||||
|
const confirmDialog = $('mwc-dialog#confirm-delete-record-dialog');
|
||||||
|
const confirmButton = confirmDialog.find('mwc-button#confirm-button');
|
||||||
|
const loading: Cash = confirmButton.find('mwc-circular-progress');
|
||||||
|
confirmButton.on('click', async () => {
|
||||||
|
loading.show();
|
||||||
|
await instance.delete();
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
this.rows.forget(instance.getId());
|
||||||
|
m.redraw();
|
||||||
|
await showSnackbar(__('Record eliminato!'), 4000);
|
||||||
|
});
|
||||||
|
loading.hide();
|
||||||
|
(confirmDialog.get(0) as MWCDialog).show();
|
||||||
|
});
|
||||||
|
(dialog.get(0) as MWCDialog).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDialog() {
|
||||||
|
return (
|
||||||
|
<mwc-dialog
|
||||||
|
id="add-record-dialog"
|
||||||
|
className="record-dialog"
|
||||||
|
heading={__('Aggiungi nuovo record')}
|
||||||
|
// @ts-ignore
|
||||||
|
style={`--mdc-dialog-max-width: ${this.recordDialogMaxWidth}`}
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<text-field
|
||||||
|
id="id"
|
||||||
|
name="id"
|
||||||
|
// @ts-ignore
|
||||||
|
style="display: none;"
|
||||||
|
data-default-value=""
|
||||||
|
/>
|
||||||
|
{(() => {
|
||||||
|
const sections = collect(this.sections);
|
||||||
|
return sections
|
||||||
|
.map((section, index: string | number) => (
|
||||||
|
<>
|
||||||
|
<div id={section.id ?? index}>
|
||||||
|
<h4 class="mdc-typography--overline">{section.heading}</h4>
|
||||||
|
<mwc-layout-grid>
|
||||||
|
{(() => {
|
||||||
|
const fields = collect(section.fields);
|
||||||
|
return fields
|
||||||
|
.map((field: TextFieldT | TextAreaT | SelectT, fieldIndex: string) => (
|
||||||
|
<mwc-layout-grid-cell
|
||||||
|
key={fieldIndex}
|
||||||
|
span={12 / (section.columns ?? 3)}
|
||||||
|
>
|
||||||
|
{m(
|
||||||
|
(field.elementType)
|
||||||
|
?? this.getElementFromType(field.type as string),
|
||||||
|
{
|
||||||
|
...field,
|
||||||
|
id: field.id ?? fieldIndex,
|
||||||
|
name: field.name ?? field.id ?? fieldIndex,
|
||||||
|
'data-default-value':
|
||||||
|
field.value ?? (field as SelectT).selected ?? ''
|
||||||
|
},
|
||||||
|
this.getFieldBody(field)
|
||||||
|
)}
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
))
|
||||||
|
.toArray();
|
||||||
|
})()}
|
||||||
|
</mwc-layout-grid>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.toArray();
|
||||||
|
})()}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
slot="primaryAction"
|
||||||
|
label={__('Conferma')}
|
||||||
|
/>
|
||||||
|
<mwc-button slot="secondaryAction" dialogAction="cancel">
|
||||||
|
{__('Annulla')}
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
id="delete-button"
|
||||||
|
slot="secondaryAction"
|
||||||
|
label={__('Elimina')}
|
||||||
|
style="--mdc-theme-primary: var(--mdc-theme-error, red); float: left; display: none;"
|
||||||
|
>
|
||||||
|
<Mdi icon="delete-outline" slot="icon" />
|
||||||
|
</mwc-button>
|
||||||
|
</mwc-dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRecordDialog(): Children {
|
||||||
|
return (
|
||||||
|
<mwc-dialog id="confirm-delete-record-dialog">
|
||||||
|
<p>{__('Sei sicuro di voler eliminare questo record?')}</p>
|
||||||
|
<LoadingButton
|
||||||
|
id="confirm-button"
|
||||||
|
slot="primaryAction"
|
||||||
|
label={__('Sì')}
|
||||||
|
/>
|
||||||
|
<mwc-button
|
||||||
|
slot="secondaryAction"
|
||||||
|
dialogAction="discard"
|
||||||
|
label={__('No')}
|
||||||
|
/>
|
||||||
|
</mwc-dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view(vnode: Vnode) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>{this.title}</h2>
|
||||||
|
<DataTable checkable paginated>
|
||||||
|
{this.tableColumns()}
|
||||||
|
{this.tableRows()}
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<mwc-fab id="add-record" label={__('Aggiungi')} className="sticky">
|
||||||
|
<Mdi icon="plus" slot="icon" />
|
||||||
|
</mwc-fab>
|
||||||
|
{this.recordDialog()}
|
||||||
|
{this.deleteRecordDialog()}
|
||||||
|
{this.dialogs}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
const fab: Cash = $('mwc-fab#add-record');
|
||||||
|
const dialog: Cash = fab.next('mwc-dialog#add-record-dialog');
|
||||||
|
const form: Cash = dialog.find('form');
|
||||||
|
|
||||||
|
// Open "New record" dialog
|
||||||
|
fab.on('click', () => {
|
||||||
|
form
|
||||||
|
.find('text-field, text-area, material-select')
|
||||||
|
.each((index, field) => {
|
||||||
|
(field as HTMLInputElement).value = $(field).data('default-value') as string;
|
||||||
|
});
|
||||||
|
dialog.find('mwc-button[type="submit"] mwc-circular-progress').hide();
|
||||||
|
dialog.find('mwc-button#delete-button').hide();
|
||||||
|
const dialogElement: HTMLElement & Partial<MWCDialog> | undefined = dialog.get(0);
|
||||||
|
if (dialogElement) {
|
||||||
|
(dialogElement as MWCDialog).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = dialog.find('mwc-button[type="submit"]');
|
||||||
|
button.on('click', () => {
|
||||||
|
form.trigger('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading: Cash = button.find('mwc-circular-progress');
|
||||||
|
form.on('submit', async (event: SubmitEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
if (isFormValid(form)) {
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const instance = new this.model() as IndexedModel;
|
||||||
|
|
||||||
|
if (this.customSetter) {
|
||||||
|
this.customSetter(instance, collect(getFormData(form)));
|
||||||
|
} else {
|
||||||
|
const fields = form.find('text-field, text-area, material-select');
|
||||||
|
fields
|
||||||
|
.filter(this.fieldsPrecedence.map((value) => `#${value}`).join(', '))
|
||||||
|
.each((index, field) => {
|
||||||
|
instance[field.id] = (field as HTMLInputElement).value;
|
||||||
|
});
|
||||||
|
fields.each((index, field) => {
|
||||||
|
instance[field.id] = (field as HTMLInputElement).value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await instance.save();
|
||||||
|
|
||||||
|
const model = response.getModel();
|
||||||
|
if (model) {
|
||||||
|
const dialogElement = dialog.get(0);
|
||||||
|
if (dialogElement) {
|
||||||
|
(dialogElement as MWCDialog).close();
|
||||||
|
}
|
||||||
|
this.rows.put((model as IndexedModel).getId(), model);
|
||||||
|
m.redraw();
|
||||||
|
await showSnackbar(__('Record salvato'), 4000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loading.hide();
|
||||||
|
await showSnackbar(__('Campi non validi. Controlla i dati inseriti'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getModelValue(model: IndexedModel, field: string): any {
|
||||||
|
const column = this.columns[field];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
let value: any = model[field];
|
||||||
|
|
||||||
|
if (typeof column === 'object' && column.valueModifier) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
value = column.valueModifier(model, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementFromType(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'text':
|
||||||
|
return 'text-field';
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
return 'text-area';
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
return 'material-select';
|
||||||
|
|
||||||
|
/* Case 'checkbox':
|
||||||
|
case 'radio':
|
||||||
|
return Radio; */
|
||||||
|
default:
|
||||||
|
return 'text-field';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldBody(field: FieldT | TextFieldT | TextAreaT | SelectT) {
|
||||||
|
const list = [];
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case 'select':
|
||||||
|
// @ts-ignore
|
||||||
|
for (const option of (field as SelectT).options) {
|
||||||
|
list.push(
|
||||||
|
<mwc-list-item key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</mwc-list-item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
return '';
|
||||||
|
|
||||||
|
case 'radio':
|
||||||
|
return '';
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.icon) {
|
||||||
|
list.push(<Mdi icon={field.icon} slot="icon" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
1
resources/js/Components/Pages/index.js
vendored
1
resources/js/Components/Pages/index.js
vendored
@ -1 +0,0 @@
|
|||||||
export * from './RecordsPage.jsx';
|
|
1
resources/js/Components/Pages/index.ts
Normal file
1
resources/js/Components/Pages/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './RecordsPage';
|
@ -2,15 +2,29 @@
|
|||||||
* @source https://github.com/flarum/core/blob/master/js/src/common/extend.js
|
* @source https://github.com/flarum/core/blob/master/js/src/common/extend.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that returns an array of all keys of a provided object that are of the provided type,
|
||||||
|
* or a subtype of the type.
|
||||||
|
*/
|
||||||
|
declare type KeysOfType<Type extends object, Match> = {
|
||||||
|
[Key in keyof Type]-?: Type[Key] extends Match ? Key : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that matches one of the keys of an object that is of the provided
|
||||||
|
* type, or a subtype of it.
|
||||||
|
*/
|
||||||
|
declare type KeyOfType<Type extends object, Match> = KeysOfType<Type, Match>[keyof Type];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend an object's method by running its output through a mutating callback
|
* Extend an object's method by running its output through a mutating callback
|
||||||
* every time it is called.
|
* every time it is called.
|
||||||
*
|
*
|
||||||
* The callback accepts the method's return value and should perform any
|
* The callback accepts the method's return value and should perform any
|
||||||
* mutations directly on this value. For this reason, this function will not be
|
* mutations directly on this value. For this reason, this function will not be
|
||||||
* effective on methods which return scalar values (numbers, strings, booleans).
|
* effective on methods, which return scalar values (numbers, strings, booleans).
|
||||||
*
|
*
|
||||||
* Care should be taken to extend the correct object – in most cases, a class'
|
* Care should be taken to extend the correct object — usually, a class'
|
||||||
* prototype will be the desired target of extension, not the class itself.
|
* prototype will be the desired target of extension, not the class itself.
|
||||||
*
|
*
|
||||||
* @example <caption>Example usage of extending one method.</caption>
|
* @example <caption>Example usage of extending one method.</caption>
|
||||||
@ -23,25 +37,30 @@
|
|||||||
* // something that needs to be run on creation and update
|
* // something that needs to be run on creation and update
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* @param {object} proto The prototype of the object/class that owns the method
|
* @param object The object that owns the method
|
||||||
* @param {string|string[]} methods The name or names of the method(s) to extend
|
* @param methods The name or names of the method(s) to extend
|
||||||
* @param {function} callback A callback which mutates the method's output
|
* @param callback A callback which mutates the method's output
|
||||||
*/
|
*/
|
||||||
export function extend(proto, methods, callback) {
|
export function extend<T extends Record<string, any>, K extends KeyOfType<T, Function>>(
|
||||||
|
object: T,
|
||||||
|
methods: K | K[],
|
||||||
|
callback: (this: T, value: ReturnType<T[K]>, ...arguments_: Parameters<T[K]>) => void
|
||||||
|
) {
|
||||||
const allMethods = Array.isArray(methods) ? methods : [methods];
|
const allMethods = Array.isArray(methods) ? methods : [methods];
|
||||||
|
|
||||||
for (const method of allMethods) {
|
for (const method of allMethods) {
|
||||||
const original = proto[method];
|
const original: Function | undefined = object[method];
|
||||||
|
|
||||||
proto[method] = function (...arguments_) {
|
object[method] = function (this: T, ...arguments_: Parameters<T[K]>): any {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const value = original ? original.apply(this, arguments_) : undefined;
|
const value = original ? original.apply(this, arguments_) : undefined;
|
||||||
|
|
||||||
Reflect.apply(callback, this, [value, ...arguments_]);
|
Reflect.apply(callback, this, [value, ...arguments_]);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
};
|
} as T[K];
|
||||||
|
|
||||||
Object.assign(proto[method], original);
|
Object.assign(object[method], original);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,19 +87,23 @@ export function extend(proto, methods, callback) {
|
|||||||
* // something that needs to be run on creation and update
|
* // something that needs to be run on creation and update
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* @param {object} object The object that owns the method
|
* @param object The object that owns the method
|
||||||
* @param {string|string[]} methods The name or names of the method(s) to override
|
* @param methods The name or names of the method(s) to override
|
||||||
* @param {function} newMethod The method to replace it with
|
* @param newMethod The method to replace it with
|
||||||
*/
|
*/
|
||||||
export function override(object, methods, newMethod) {
|
export function override<T extends Record<any, any>, K extends KeyOfType<T, Function>>(
|
||||||
|
object: T,
|
||||||
|
methods: K | K[],
|
||||||
|
newMethod: (this: T, orig: T[K], ...arguments_: Parameters<T[K]>) => void
|
||||||
|
) {
|
||||||
const allMethods = Array.isArray(methods) ? methods : [methods];
|
const allMethods = Array.isArray(methods) ? methods : [methods];
|
||||||
|
|
||||||
for (const method of allMethods) {
|
for (const method of allMethods) {
|
||||||
const original = object[method];
|
const original: Function = object[method];
|
||||||
|
|
||||||
object[method] = function (...arguments_) {
|
object[method] = function (this: T, ...arguments_: Parameters<T[K]>): any {
|
||||||
return Reflect.apply(newMethod, this, [original.bind(this), ...arguments_]);
|
return Reflect.apply(newMethod, this, [original.bind(this), ...arguments_]);
|
||||||
};
|
} as T[K];
|
||||||
|
|
||||||
Object.assign(object[method], original);
|
Object.assign(object[method], original);
|
||||||
}
|
}
|
10
resources/js/Components/index.js
vendored
10
resources/js/Components/index.js
vendored
@ -1,10 +0,0 @@
|
|||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
|
|
||||||
export {default as Alert} from './Alert.jsx';
|
|
||||||
export { default as Component } from './Component.jsx';
|
|
||||||
export * from './DataTable';
|
|
||||||
export { extend, override } from './extend';
|
|
||||||
export {default as LoadingButton} from './LoadingButton.jsx';
|
|
||||||
export {default as Mdi} from './Mdi.jsx';
|
|
||||||
export { default as Page } from './Page.jsx';
|
|
||||||
export * from './Pages';
|
|
11
resources/js/Components/index.ts
Normal file
11
resources/js/Components/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable import/export */
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
export {default as Alert} from './Alert';
|
||||||
|
export {default as Component} from './Component';
|
||||||
|
export * from './DataTable';
|
||||||
|
export {extend, override} from './extend';
|
||||||
|
export {default as LoadingButton} from './LoadingButton';
|
||||||
|
export {default as Mdi} from './Mdi';
|
||||||
|
export {default as Page} from './Page';
|
||||||
|
export * from './Pages';
|
@ -2,25 +2,21 @@ import {
|
|||||||
type PluralResponse,
|
type PluralResponse,
|
||||||
Model as BaseModel
|
Model as BaseModel
|
||||||
} from 'coloquent';
|
} from 'coloquent';
|
||||||
import {snakeCase} from 'lodash-es';
|
import {snakeCase} from 'lodash';
|
||||||
|
|
||||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
|
||||||
/**
|
/**
|
||||||
* The base model for all models.
|
* The base model for all models.
|
||||||
*
|
|
||||||
* @property {number} id
|
|
||||||
* @abstract
|
|
||||||
*/
|
*/
|
||||||
export default class Model extends BaseModel {
|
export default abstract class Model extends BaseModel {
|
||||||
jsonApiType: string;
|
jsonApiType: string = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// Return a proxy of this object to allow dynamic attributes getters and setters
|
// Return a proxy of this object to allow dynamic attributes getters and setters
|
||||||
// eslint-disable-next-line no-constructor-return
|
// eslint-disable-next-line no-constructor-return, @typescript-eslint/no-unsafe-return
|
||||||
return new Proxy(this, {
|
return new Proxy(this, {
|
||||||
get(target: this, property, receiver) {
|
get(target, property: string, receiver): any {
|
||||||
const snakeCasedProperty = snakeCase(property);
|
const snakeCasedProperty = snakeCase(property);
|
||||||
if (snakeCasedProperty in target.getAttributes()) {
|
if (snakeCasedProperty in target.getAttributes()) {
|
||||||
return target.getAttribute(snakeCasedProperty);
|
return target.getAttribute(snakeCasedProperty);
|
||||||
@ -28,7 +24,7 @@ export default class Model extends BaseModel {
|
|||||||
|
|
||||||
return Reflect.get(target, property, receiver);
|
return Reflect.get(target, property, receiver);
|
||||||
},
|
},
|
||||||
set(target: this, property, value, receiver) {
|
set(target, property: string, value) {
|
||||||
target.setAttribute(snakeCase(property), value);
|
target.setAttribute(snakeCase(property), value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -39,14 +35,15 @@ export default class Model extends BaseModel {
|
|||||||
* Just an alias to the get() method.
|
* Just an alias to the get() method.
|
||||||
*
|
*
|
||||||
* Returns all the instances of the model.
|
* Returns all the instances of the model.
|
||||||
*/
|
*/ // @ts-ignore
|
||||||
static all(): Promise<PluralResponse<InstanceType<Model>>> {
|
static all(): Promise<PluralResponse<InstanceType<Model>>> {
|
||||||
|
// @ts-ignore
|
||||||
return this.get();
|
return this.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
setAttributes(attributes: { [string]: any }): void {
|
setAttributes(attributes: Record<string, any>): void {
|
||||||
for (const [attribute, value] of Object.entries(attributes)) {
|
for (const [attribute, value] of Object.entries(attributes)) {
|
||||||
this[attribute] = value;
|
this.setAttribute(attribute, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +55,7 @@ export default class Model extends BaseModel {
|
|||||||
super.setAttribute(attributeName, value);
|
super.setAttribute(attributeName, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributes(): { [p: string]: any } {
|
getAttributes(): {[p: string]: any} {
|
||||||
return super.getAttributes();
|
return super.getAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,4 +66,8 @@ export default class Model extends BaseModel {
|
|||||||
getJsonApiType(): string {
|
getJsonApiType(): string {
|
||||||
return (super.getJsonApiType() ?? snakeCase(this.constructor.name));
|
return (super.getJsonApiType() ?? snakeCase(this.constructor.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.getApiId();
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,91 +0,0 @@
|
|||||||
import '@maicol07/mwc-card';
|
|
||||||
import '../WebComponents/TextField';
|
|
||||||
|
|
||||||
import {Inertia} from '@inertiajs/inertia';
|
|
||||||
import type {TextField} from '@material/mwc-textfield';
|
|
||||||
import type {Cash} from 'cash-dom';
|
|
||||||
import redaxios from 'redaxios';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-absolute-path
|
|
||||||
import logoUrl from '/images/logo_completo.png';
|
|
||||||
|
|
||||||
import LoadingButton from '../Components/LoadingButton.jsx';
|
|
||||||
import Mdi from '../Components/Mdi.jsx';
|
|
||||||
import Page from '../Components/Page.jsx';
|
|
||||||
import {
|
|
||||||
getFormData,
|
|
||||||
isFormValid,
|
|
||||||
showSnackbar
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
export default class AdminSetupPage extends Page {
|
|
||||||
loading: Cash;
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
return (
|
|
||||||
<mwc-card outlined className="center ext-container ext-container-small">
|
|
||||||
<img src={logoUrl} className="center stretch" alt={__('OpenSTAManager')}/>
|
|
||||||
<form id="new-admin" style="padding: 16px; text-align: center;">
|
|
||||||
<h3 style="margin-top: 0;">{__('Creazione account amministratore')}</h3>
|
|
||||||
<p>{__('Inserisci le informazioni richieste per creare un nuovo account amministratore.')}</p>
|
|
||||||
<text-field label={__('Nome utente')} id="username" name="username" required style="margin-bottom: 16px;">
|
|
||||||
<Mdi icon="account-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<text-field label={__('Email')} id="email" name="email" type="email" required style="margin-bottom: 16px;">
|
|
||||||
<Mdi icon="email-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<text-field label={__('Password')} id="password" name="password" type="password" required style="margin-bottom: 16px;">
|
|
||||||
<Mdi icon="lock-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<text-field label={__('Conferma password')} id="password_confirm" name="password_confirm" type="password" required style="margin-bottom: 16px;">
|
|
||||||
<Mdi icon="repeat-variant" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<LoadingButton raised id="create-account-button" label={__('Crea account')} icon="account-plus-outline" type="submit" />
|
|
||||||
</form>
|
|
||||||
</mwc-card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.loading = $(this.element).find('#login-button mwc-circular-progress');
|
|
||||||
|
|
||||||
$(this.element)
|
|
||||||
.find('#create-account-button')
|
|
||||||
.on('click', this.onCreateAccountButtonClicked.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
async onCreateAccountButtonClicked(event: PointerEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.loading.show();
|
|
||||||
|
|
||||||
const form = $(this.element).find('form#new-admin');
|
|
||||||
const password: TextField = form.find('#password').get(0);
|
|
||||||
const passwordConfirm: TextField = form.find('#password_confirm').get(0);
|
|
||||||
|
|
||||||
passwordConfirm.setCustomValidity(
|
|
||||||
password.value !== passwordConfirm.value ? __('Le password non corrispondono') : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isFormValid(form)) {
|
|
||||||
this.loading.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = getFormData(form);
|
|
||||||
|
|
||||||
formData._token = $('meta[name="csrf-token"]').attr('content');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redaxios.put(window.route('setup.admin.save'), formData);
|
|
||||||
} catch (error) {
|
|
||||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
|
||||||
this.loading.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Inertia.visit('/');
|
|
||||||
showSnackbar(__('Account creato con successo. Puoi ora accedere.'));
|
|
||||||
}
|
|
||||||
}
|
|
132
resources/js/Views/AdminSetupPage.tsx
Normal file
132
resources/js/Views/AdminSetupPage.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import '@maicol07/mwc-card';
|
||||||
|
import '../WebComponents/TextField';
|
||||||
|
|
||||||
|
import {Inertia} from '@inertiajs/inertia';
|
||||||
|
import type {Cash} from 'cash-dom';
|
||||||
|
import type {VnodeDOM} from 'mithril';
|
||||||
|
import redaxios from 'redaxios';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-absolute-path
|
||||||
|
import logoUrl from '/images/logo_completo.png';
|
||||||
|
|
||||||
|
import LoadingButton from '../Components/LoadingButton';
|
||||||
|
import Mdi from '../Components/Mdi';
|
||||||
|
import Page from '../Components/Page';
|
||||||
|
import type {ErrorResponse} from '../types';
|
||||||
|
import {
|
||||||
|
getFormData,
|
||||||
|
isFormValid,
|
||||||
|
showSnackbar,
|
||||||
|
validatePassword
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
|
export default class AdminSetupPage extends Page {
|
||||||
|
loading: Cash;
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<mwc-card outlined className="center ext-container ext-container-small">
|
||||||
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
className="center stretch"
|
||||||
|
alt={__('OpenSTAManager')}
|
||||||
|
/>
|
||||||
|
<form id="new-admin" style="padding: 16px; text-align: center;">
|
||||||
|
<h3 style="margin-top: 0;">
|
||||||
|
{__('Creazione account amministratore')}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
{__(
|
||||||
|
'Inserisci le informazioni richieste per creare un nuovo account amministratore.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<text-field
|
||||||
|
label={__('Nome utente')}
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="account-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<text-field
|
||||||
|
label={__('Email')}
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="email-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<text-field
|
||||||
|
label={__('Password')}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="lock-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<text-field
|
||||||
|
label={__('Conferma password')}
|
||||||
|
id="password_confirm"
|
||||||
|
name="password_confirm"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="repeat-variant" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<LoadingButton
|
||||||
|
raised
|
||||||
|
id="create-account-button"
|
||||||
|
label={__('Crea account')}
|
||||||
|
icon="account-plus-outline"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</mwc-card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
this.loading = $(this.element).find('#login-button mwc-circular-progress');
|
||||||
|
$(this.element)
|
||||||
|
.find('#create-account-button')
|
||||||
|
.on('click', this.onCreateAccountButtonClicked.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onCreateAccountButtonClicked(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loading.show();
|
||||||
|
const form = $(this.element).find('form#new-admin');
|
||||||
|
|
||||||
|
// noinspection DuplicatedCode
|
||||||
|
const password: HTMLElement | undefined = form.find('#password').get(0);
|
||||||
|
const passwordConfirm: HTMLElement | undefined = form.find('#password_confirm').get(0);
|
||||||
|
|
||||||
|
validatePassword(password as HTMLInputElement, passwordConfirm as HTMLInputElement);
|
||||||
|
|
||||||
|
if (!isFormValid(form)) {
|
||||||
|
this.loading.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = getFormData(form);
|
||||||
|
formData._token = $('meta[name="csrf-token"]').attr('content') as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redaxios.put(route('setup.admin.save'), formData);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.loading.hide();
|
||||||
|
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inertia.visit('/');
|
||||||
|
await showSnackbar(__('Account creato con successo. Puoi ora accedere.'));
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
import Page from '../Components/Page.jsx';
|
|
||||||
|
|
||||||
export default class Dashboard extends Page {
|
|
||||||
view(vnode) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>{__('Dashboard')}</h2>
|
|
||||||
<p>
|
|
||||||
{__('Seleziona una voce dal menu a sinistra')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
12
resources/js/Views/Dashboard.tsx
Normal file
12
resources/js/Views/Dashboard.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Page from '../Components/Page';
|
||||||
|
|
||||||
|
export default class Dashboard extends Page {
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{__('Dashboard')}</h2>
|
||||||
|
<p>{__('Seleziona una voce dal menu a sinistra')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,38 +5,55 @@ import '@material/mwc-formfield';
|
|||||||
import '../WebComponents/TextField';
|
import '../WebComponents/TextField';
|
||||||
|
|
||||||
import type {Cash} from 'cash-dom';
|
import type {Cash} from 'cash-dom';
|
||||||
|
import type {
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
import redaxios from 'redaxios';
|
import redaxios from 'redaxios';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-absolute-path
|
// eslint-disable-next-line import/no-absolute-path
|
||||||
import logoUrl from '/images/logo_completo.png';
|
import logoUrl from '/images/logo_completo.png';
|
||||||
|
|
||||||
import LoadingButton from '../Components/LoadingButton.jsx';
|
import LoadingButton from '../Components/LoadingButton';
|
||||||
import Mdi from '../Components/Mdi.jsx';
|
import Mdi from '../Components/Mdi';
|
||||||
import Page from '../Components/Page.jsx';
|
import Page from '../Components/Page';
|
||||||
import {
|
import {ErrorResponse} from '../types';
|
||||||
getFormData,
|
import {getFormData, isFormValid, showSnackbar} from '../utils';
|
||||||
isFormValid,
|
|
||||||
showSnackbar
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
export default class LoginPage extends Page {
|
export default class LoginPage extends Page {
|
||||||
loading: Cash;
|
loading: Cash;
|
||||||
forgotPasswordLoading: Cash;
|
forgotPasswordLoading: Cash;
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode: Vnode) {
|
||||||
return (
|
return (
|
||||||
<mwc-card outlined className="center ext-container ext-container-small">
|
<mwc-card outlined className="center ext-container ext-container-small">
|
||||||
<img src={logoUrl} className="center stretch" alt={__('OpenSTAManager')}/>
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
className="center stretch"
|
||||||
|
alt={__('OpenSTAManager')}
|
||||||
|
/>
|
||||||
<form id="login" style="padding: 16px; text-align: center;">
|
<form id="login" style="padding: 16px; text-align: center;">
|
||||||
<h3 style="margin-top: 0;">{__('Accedi')}</h3>
|
<h3 style="margin-top: 0;">{__('Accedi')}</h3>
|
||||||
<text-field label={__('Nome utente/email')} id="username" name="username" required style="margin-bottom: 16px;">
|
<text-field
|
||||||
<Mdi icon="account-outline" slot="icon"/>
|
label={__('Nome utente/email')}
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="account-outline" slot="icon" />
|
||||||
</text-field>
|
</text-field>
|
||||||
<text-field label={__('Password')} id="password" name="password" required type="password">
|
<text-field
|
||||||
<Mdi icon="lock-outline" slot="icon"/>
|
label={__('Password')}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<Mdi icon="lock-outline" slot="icon" />
|
||||||
</text-field>
|
</text-field>
|
||||||
<mwc-formfield label={__('Ricordami')} style="display: block;">
|
<mwc-formfield label={__('Ricordami')} style="display: block;">
|
||||||
<mwc-checkbox id="remember" name="remember"/>
|
<mwc-checkbox id="remember" name="remember" />
|
||||||
</mwc-formfield>
|
</mwc-formfield>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -60,17 +77,17 @@ export default class LoginPage extends Page {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode: VnodeDOM) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
this.loading = $(this.element).find('#login-button mwc-circular-progress');
|
this.loading = $(this.element).find('#login-button mwc-circular-progress');
|
||||||
this.forgotPasswordLoading = $(this.element).find('#forgot-password-button mwc-circular-progress');
|
this.forgotPasswordLoading = $(this.element).find(
|
||||||
|
'#forgot-password-button mwc-circular-progress'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onLoginButtonClicked(event: PointerEvent) {
|
async onLoginButtonClicked(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.loading.show();
|
this.loading.show();
|
||||||
|
|
||||||
const form = $(this.element).find('#login');
|
const form = $(this.element).find('#login');
|
||||||
|
|
||||||
if (!isFormValid(form)) {
|
if (!isFormValid(form)) {
|
||||||
@ -79,48 +96,49 @@ export default class LoginPage extends Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = getFormData(form);
|
const formData = getFormData(form);
|
||||||
|
formData._token = $('meta[name="csrf-token"]').attr('content') as string;
|
||||||
formData._token = $('meta[name="csrf-token"]').attr('content');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await redaxios(window.route('auth.authenticate'), {
|
await redaxios(route('auth.authenticate'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData
|
data: formData
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// noinspection ES6MissingAwait
|
|
||||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
|
||||||
this.loading.hide();
|
this.loading.hide();
|
||||||
|
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = window.route('dashboard');
|
window.location.href = route('dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
async onForgotPasswordButtonClicked() {
|
async onForgotPasswordButtonClicked() {
|
||||||
this.forgotPasswordLoading.show();
|
this.forgotPasswordLoading.show();
|
||||||
const field: HTMLFormElement = document.querySelector('#username');
|
const field: HTMLInputElement | null = this.element.querySelector('#username');
|
||||||
field.type = 'email';
|
if (field) {
|
||||||
if (!field.reportValidity()) {
|
field.type = 'email';
|
||||||
|
|
||||||
|
if (!field.reportValidity()) {
|
||||||
|
field.type = 'text';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
field.type = 'text';
|
field.type = 'text';
|
||||||
return;
|
|
||||||
}
|
|
||||||
field.type = 'text';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await redaxios.post(window.route('password.forgot'), {
|
await redaxios.post(route('password.forgot'), {
|
||||||
email: field.value,
|
email: field.value,
|
||||||
_token: $('meta[name="csrf-token"]').attr('content')
|
_token: $('meta[name="csrf-token"]')
|
||||||
});
|
.attr('content')
|
||||||
} catch (error) {
|
});
|
||||||
// noinspection ES6MissingAwait
|
} catch (error: any) {
|
||||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
this.loading.hide();
|
||||||
|
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading.hide();
|
this.loading.hide();
|
||||||
return;
|
await showSnackbar(__('La password è stata inviata alla tua email'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection ES6MissingAwait
|
|
||||||
showSnackbar(__('La password è stata inviata alla tua email'));
|
|
||||||
this.loading.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,103 +0,0 @@
|
|||||||
// noinspection DuplicatedCode
|
|
||||||
|
|
||||||
import '@maicol07/mwc-card';
|
|
||||||
import '@maicol07/mwc-layout-grid';
|
|
||||||
import '../WebComponents/TextField';
|
|
||||||
|
|
||||||
import {Inertia} from '@inertiajs/inertia';
|
|
||||||
import type {TextField} from '@material/mwc-textfield';
|
|
||||||
import type {Cash} from 'cash-dom';
|
|
||||||
import redaxios from 'redaxios';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-absolute-path
|
|
||||||
import logoUrl from '/images/logo_completo.png';
|
|
||||||
|
|
||||||
import LoadingButton from '../Components/LoadingButton.jsx';
|
|
||||||
import Mdi from '../Components/Mdi.jsx';
|
|
||||||
import Page from '../Components/Page.jsx';
|
|
||||||
import {
|
|
||||||
getFormData,
|
|
||||||
isFormValid,
|
|
||||||
showSnackbar
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
export default class ResetPasswordPage extends Page {
|
|
||||||
loading: Cash;
|
|
||||||
parameters: URLSearchParams;
|
|
||||||
|
|
||||||
oninit(vnode) {
|
|
||||||
super.oninit(vnode);
|
|
||||||
|
|
||||||
this.parameters = new URLSearchParams(window.location.search);
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
return (
|
|
||||||
<mwc-card outlined className="center ext-container ext-container-small">
|
|
||||||
<img src={logoUrl} className="center stretch" alt={__('OpenSTAManager')}/>
|
|
||||||
<form id="reset-password" style="padding: 16px; text-align: center;">
|
|
||||||
<h3 style="margin-top: 0;">{__('Reimposta password')}</h3>
|
|
||||||
<input hidden id="email" name="email" value={this.parameters.get('email')}/>
|
|
||||||
<input hidden id="token" name="token" value={this.parameters.get('token')}/>
|
|
||||||
<text-field label={__('Password')} id="password" name="password" required type="password">
|
|
||||||
<Mdi icon="lock-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<text-field label={__('Conferma password')} id="password_confirm" name="password_confirm" type="password" required style="margin-top: 16px;">
|
|
||||||
<Mdi icon="repeat-variant" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
<LoadingButton
|
|
||||||
type="submit"
|
|
||||||
raised
|
|
||||||
id="reset-password-button"
|
|
||||||
label={__('Resetta password')}
|
|
||||||
icon="lock-reset"
|
|
||||||
style="float: right;"
|
|
||||||
onclick={this.onResetPasswordButtonClicked.bind(this)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</mwc-card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.loading = $(this.element).find('#reset-password mwc-circular-progress');
|
|
||||||
}
|
|
||||||
|
|
||||||
async onResetPasswordButtonClicked(event: PointerEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.loading.show();
|
|
||||||
|
|
||||||
const form = $(this.element).find('#reset-password');
|
|
||||||
const password: TextField = form.find('#password').get(0);
|
|
||||||
const passwordConfirm: TextField = form.find('#password_confirm').get(0);
|
|
||||||
|
|
||||||
passwordConfirm.setCustomValidity(
|
|
||||||
password.value !== passwordConfirm.value ? __('Le password non corrispondono') : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isFormValid(form)) {
|
|
||||||
this.loading.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = getFormData(form);
|
|
||||||
|
|
||||||
formData._token = $('meta[name="csrf-token"]').attr('content');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redaxios.put(window.route('password.resetPassword'), formData);
|
|
||||||
} catch (error) {
|
|
||||||
// noinspection ES6MissingAwait
|
|
||||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
|
||||||
this.loading.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Inertia.visit('/');
|
|
||||||
|
|
||||||
// noinspection ES6MissingAwait
|
|
||||||
showSnackbar(__('Reset della password effettuato con successo. Puoi ora accedere.'));
|
|
||||||
}
|
|
||||||
}
|
|
129
resources/js/Views/ResetPasswordPage.tsx
Normal file
129
resources/js/Views/ResetPasswordPage.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// noinspection DuplicatedCode
|
||||||
|
import '@maicol07/mwc-card';
|
||||||
|
import '@maicol07/mwc-layout-grid';
|
||||||
|
import '../WebComponents/TextField';
|
||||||
|
|
||||||
|
import {Inertia} from '@inertiajs/inertia';
|
||||||
|
import type {Cash} from 'cash-dom';
|
||||||
|
import type {
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM
|
||||||
|
} from 'mithril';
|
||||||
|
import redaxios from 'redaxios';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-absolute-path
|
||||||
|
import logoUrl from '/images/logo_completo.png';
|
||||||
|
|
||||||
|
import LoadingButton from '../Components/LoadingButton';
|
||||||
|
import Mdi from '../Components/Mdi';
|
||||||
|
import Page from '../Components/Page';
|
||||||
|
import {ErrorResponse} from '../types';
|
||||||
|
import {
|
||||||
|
getFormData,
|
||||||
|
isFormValid,
|
||||||
|
showSnackbar,
|
||||||
|
validatePassword
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
|
export default class ResetPasswordPage extends Page {
|
||||||
|
loading: Cash;
|
||||||
|
parameters: URLSearchParams;
|
||||||
|
|
||||||
|
oninit(vnode: Vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
this.parameters = new URLSearchParams(window.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<mwc-card outlined className="center ext-container ext-container-small">
|
||||||
|
<img
|
||||||
|
src={logoUrl}
|
||||||
|
className="center stretch"
|
||||||
|
alt={__('OpenSTAManager')}
|
||||||
|
/>
|
||||||
|
<form id="reset-password" style="padding: 16px; text-align: center;">
|
||||||
|
<h3 style="margin-top: 0;">{__('Reimposta password')}</h3>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={this.parameters.get('email')}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
id="token"
|
||||||
|
name="token"
|
||||||
|
value={this.parameters.get('token')}
|
||||||
|
/>
|
||||||
|
<text-field
|
||||||
|
label={__('Password')}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<Mdi icon="lock-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<text-field
|
||||||
|
label={__('Conferma password')}
|
||||||
|
id="password_confirm"
|
||||||
|
name="password_confirm"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
style="margin-top: 16px;"
|
||||||
|
>
|
||||||
|
<Mdi icon="repeat-variant" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
raised
|
||||||
|
id="reset-password-button"
|
||||||
|
label={__('Resetta password')}
|
||||||
|
icon="lock-reset"
|
||||||
|
style="float: right;"
|
||||||
|
onclick={this.onResetPasswordButtonClicked.bind(this)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</mwc-card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
this.loading = $(this.element).find('#reset-password mwc-circular-progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onResetPasswordButtonClicked(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loading.show();
|
||||||
|
const form = $(this.element).find('#reset-password');
|
||||||
|
|
||||||
|
// noinspection DuplicatedCode
|
||||||
|
const password: HTMLElement | undefined = form.find('#password').get(0);
|
||||||
|
const passwordConfirm: HTMLElement | undefined = form.find('#password_confirm').get(0);
|
||||||
|
|
||||||
|
validatePassword(password as HTMLInputElement, passwordConfirm as HTMLInputElement);
|
||||||
|
|
||||||
|
if (!isFormValid(form)) {
|
||||||
|
this.loading.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = getFormData(form);
|
||||||
|
formData._token = $('meta[name="csrf-token"]').attr('content') as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redaxios.put(route('password.resetPassword'), formData);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.loading.hide();
|
||||||
|
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inertia.visit('/');
|
||||||
|
await showSnackbar(
|
||||||
|
__('Reset della password effettuato con successo. Puoi ora accedere.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,268 +0,0 @@
|
|||||||
import '@maicol07/mwc-card';
|
|
||||||
import '@maicol07/mwc-layout-grid';
|
|
||||||
import '@material/mwc-button';
|
|
||||||
import '@material/mwc-checkbox';
|
|
||||||
import '@material/mwc-fab';
|
|
||||||
import '@material/mwc-formfield';
|
|
||||||
import '@material/mwc-list/mwc-list-item';
|
|
||||||
import '@material/mwc-select';
|
|
||||||
import '@material/mwc-textarea';
|
|
||||||
import '../WebComponents/TextField';
|
|
||||||
import '../WebComponents/Select';
|
|
||||||
|
|
||||||
import collect from 'collect.js';
|
|
||||||
import LocaleCode from 'locale-code';
|
|
||||||
import Mithril from 'mithril';
|
|
||||||
import redaxios from 'redaxios';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-absolute-path
|
|
||||||
import logoUrl from '/images/logo_completo.png';
|
|
||||||
|
|
||||||
import {Alert} from '../Components';
|
|
||||||
import Mdi from '../Components/Mdi.jsx';
|
|
||||||
import Page from '../Components/Page.jsx';
|
|
||||||
import {
|
|
||||||
getFormData,
|
|
||||||
showSnackbar
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
function getFlag(language: string, slot: string = 'graphic', styles: {...} = {}) {
|
|
||||||
if (!styles.display) {
|
|
||||||
styles.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div slot={slot} style={styles}>
|
|
||||||
<img style="border-radius: 4px;"
|
|
||||||
src={`https://flagicons.lipis.dev/flags/4x3/${LocaleCode.getCountryCode(language)
|
|
||||||
.toLowerCase()}.svg`}
|
|
||||||
alt={LocaleCode.getLanguageNativeName(language)}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SetupPage extends Page {
|
|
||||||
languages() {
|
|
||||||
const listItems: Mithril.Vnode[] = [];
|
|
||||||
|
|
||||||
for (const lang of this.page.props.languages) {
|
|
||||||
const attributes = {
|
|
||||||
selected: this.page.props.locale === lang
|
|
||||||
};
|
|
||||||
const langCode = lang.replace('_', '-');
|
|
||||||
listItems.push(
|
|
||||||
<mwc-list-item graphic="icon" value={lang} {...attributes}>
|
|
||||||
{getFlag(langCode)}
|
|
||||||
<span>{LocaleCode.getLanguageNativeName(langCode)}</span>
|
|
||||||
</mwc-list-item>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (attributes.selected) {
|
|
||||||
listItems.push(getFlag(langCode, 'icon', {display: 'block', width: '24px', lineHeight: '22px'}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return listItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
view(vnode) {
|
|
||||||
const examplesTexts = collect();
|
|
||||||
for (const example of ['localhost', 'root', 'mysql', 'openstamanager']) {
|
|
||||||
examplesTexts.put(example, __('Esempio: :example', {example}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<mwc-card outlined className="center ext-container">
|
|
||||||
<form id="setup">
|
|
||||||
<img src={logoUrl} className="center" alt={__('OpenSTAManager')} />
|
|
||||||
<mwc-layout-grid>
|
|
||||||
<mwc-layout-grid-cell span-desktop="8">
|
|
||||||
<h2>{__('Benvenuto in :name!', {name: <strong>{__('OpenSTAManager')}</strong>})}</h2>
|
|
||||||
<p>{__('Puoi procedere alla configurazione tecnica del software attraverso i '
|
|
||||||
+ 'parametri seguenti, che potranno essere corretti secondo necessità tramite il file .env.')}<br/>
|
|
||||||
{__("Se necessiti supporto puoi contattarci tramite l':contactLink o tramite il nostro :forumLink.", {
|
|
||||||
// eslint-disable-next-line no-secrets/no-secrets
|
|
||||||
contactLink: <a href="https://www.openstamanager.com/contattaci/?subject=Assistenza%20installazione%20OSM">{__('assistenza ufficiale')}</a>,
|
|
||||||
forumLink: <a href="https://forum.openstamanager.com">{__('forum')}</a>
|
|
||||||
})}</p>
|
|
||||||
<h4>{__('Formato date')}</h4>
|
|
||||||
<p className="mdc-typography--subtitle2" style="font-size: small;">
|
|
||||||
{__('I formati sono impostabili attraverso lo standard previsto da :link.',
|
|
||||||
{link: <a href="https://www.php.net/manual/en/function.date.php#refsect1-function.date-parameters">PHP</a>})
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<mwc-layout-grid inner>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<text-field name="timestamp_format" label={__('Formato data lunga')}
|
|
||||||
required value="d/m/Y H:i">
|
|
||||||
<Mdi icon="calendar-clock" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<text-field name="date_format" label={__('Formato data corta')}
|
|
||||||
required value="d/m/Y">
|
|
||||||
<Mdi icon="calendar-month-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<text-field name="time_format" label={__('Formato orario')} required
|
|
||||||
value="H:i">
|
|
||||||
<Mdi icon="clock-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
</mwc-layout-grid>
|
|
||||||
<hr/>
|
|
||||||
<h4>{__('Database')}</h4>
|
|
||||||
<mwc-layout-grid inner>
|
|
||||||
<mwc-layout-grid-cell span="4">
|
|
||||||
<text-field name="host" label={__('Host')} required
|
|
||||||
helper={examplesTexts.get('localhost')}>
|
|
||||||
<Mdi icon="server-network" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell span="4">
|
|
||||||
<text-field name="username" label={__('Nome utente')} required
|
|
||||||
helper={examplesTexts.get('root')}>
|
|
||||||
<Mdi icon="account-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell span="4">
|
|
||||||
<text-field name="password" label={__('Password')}
|
|
||||||
helper={examplesTexts.get('mysql')}>
|
|
||||||
<Mdi icon="lock-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell span="4">
|
|
||||||
<text-field name="database_name" label={__('Nome database')} required
|
|
||||||
helper={examplesTexts.get('openstamanager')}>
|
|
||||||
<Mdi icon="database-outline" slot="icon"/>
|
|
||||||
</text-field>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
</mwc-layout-grid>
|
|
||||||
<hr/>
|
|
||||||
<mwc-layout-grid inner>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<small>{__('* Campi obbligatori')}</small>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<mwc-button id="save-install" raised label={__('Salva e installa')} onclick={this.onSaveButtonClicked.bind(this)}>
|
|
||||||
<Mdi icon="check" slot="icon"/>
|
|
||||||
</mwc-button>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<mwc-button id="test-db" outlined label={__('Testa il database')} onclick={this.onTestButtonClicked.bind(this)}>
|
|
||||||
<Mdi icon="test-tube" slot="icon"/>
|
|
||||||
</mwc-button>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
</mwc-layout-grid>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<h4>{__('Lingua')}</h4>
|
|
||||||
<material-select id="language-select" name="locale">
|
|
||||||
{this.languages()}
|
|
||||||
</material-select>
|
|
||||||
<hr />
|
|
||||||
<h4>{__('Licenza')}</h4>
|
|
||||||
<p>{__('OpenSTAManager è tutelato dalla licenza GPL 3.0, da accettare obbligatoriamente per poter utilizzare il gestionale.')}</p>
|
|
||||||
<mwc-textarea value={this.page.props.license} rows="15" cols="40" disabled style="margin-bottom: 8px;"/>
|
|
||||||
<mwc-layout-grid inner>
|
|
||||||
<mwc-layout-grid-cell span-desktop="8" span-tablet="8">
|
|
||||||
<mwc-formfield label={__('Ho visionato e accetto la licenza')}>
|
|
||||||
<mwc-checkbox name="license_agreement"/>
|
|
||||||
</mwc-formfield>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
<mwc-layout-grid-cell>
|
|
||||||
<a href="https://www.gnu.org/licenses/translations.en.html#GPL" target="_blank">
|
|
||||||
<mwc-button label={__('Versioni tradotte')}>
|
|
||||||
<Mdi icon="license" slot="icon"/>
|
|
||||||
</mwc-button>
|
|
||||||
</a>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
</mwc-layout-grid>
|
|
||||||
</mwc-layout-grid-cell>
|
|
||||||
</mwc-layout-grid>
|
|
||||||
</form>
|
|
||||||
</mwc-card>
|
|
||||||
<mwc-fab id="contrast-switcher" className="sticky contrast-light"
|
|
||||||
label={__('Attiva/disattiva contrasto elevato')}>
|
|
||||||
<Mdi icon="contrast-circle" slot="icon" className="light-bg"/>
|
|
||||||
</mwc-fab>
|
|
||||||
<Alert id="test-connection-alert-error" icon="error"/>
|
|
||||||
<Alert id="test-connection-alert-success" icon="success">
|
|
||||||
<p>{__('Connessione al database riuscita')}</p>
|
|
||||||
</Alert>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode: Mithril.VnodeDOM) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
$('mwc-fab#contrast-switcher')
|
|
||||||
.on('click', function () {
|
|
||||||
$(this)
|
|
||||||
.toggleClass('contrast-light')
|
|
||||||
.toggleClass('contrast-dark');
|
|
||||||
$('body')
|
|
||||||
.toggleClass('mdc-high-contrast');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#language-select').on('action', this.onLanguageSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTestButtonClicked(event: Event) {
|
|
||||||
this.testDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSaveButtonClicked(event: Event) {
|
|
||||||
const form = $(event.target).closest('form');
|
|
||||||
this.save(getFormData(form));
|
|
||||||
}
|
|
||||||
|
|
||||||
onLanguageSelected(event: Event) {
|
|
||||||
const {detail, target: select} = event;
|
|
||||||
const selected = $(select).find(`mwc-list-item:nth-child(${detail.index + 1}) [slot="graphic"] img`);
|
|
||||||
$(select).find('[slot="icon"] img').attr('src', selected.attr('src'));
|
|
||||||
window.location.href = window.route('app.language', {language: select.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
async testDatabase(silentSuccess = false, silentError = false): boolean {
|
|
||||||
const form = $('form');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redaxios.options(window.route('setup.test'), {data: getFormData(form)});
|
|
||||||
} catch (error) {
|
|
||||||
if (!silentError) {
|
|
||||||
const alert = $('#test-connection-alert-error');
|
|
||||||
alert.find('.content').text(__('Si è verificato un errore durante la connessione al'
|
|
||||||
+ ' database: :error', {error: error.data.error}));
|
|
||||||
alert.get(0).show();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!silentSuccess) {
|
|
||||||
document.querySelector('#test-connection-alert-success')
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(data: {...}) {
|
|
||||||
const test = this.testDatabase(true);
|
|
||||||
if (!test) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redaxios.put(window.route('setup.save'), data);
|
|
||||||
} catch (error) {
|
|
||||||
await showSnackbar(error.data.error_description);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await showSnackbar(__('Impostazioni salvate correttamente'));
|
|
||||||
window.location.href = window.route('auth.login');
|
|
||||||
}
|
|
||||||
}
|
|
387
resources/js/Views/SetupPage.tsx
Normal file
387
resources/js/Views/SetupPage.tsx
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
import '@maicol07/mwc-card';
|
||||||
|
import '@maicol07/mwc-layout-grid';
|
||||||
|
import '@material/mwc-button';
|
||||||
|
import '@material/mwc-checkbox';
|
||||||
|
import '@material/mwc-fab';
|
||||||
|
import '@material/mwc-formfield';
|
||||||
|
import '@material/mwc-list/mwc-list-item';
|
||||||
|
import '@material/mwc-select';
|
||||||
|
import '@material/mwc-textarea';
|
||||||
|
import '../WebComponents/TextField';
|
||||||
|
import '../WebComponents/Select';
|
||||||
|
|
||||||
|
import type {Dialog as MWCDialog} from '@material/mwc-dialog';
|
||||||
|
import type CSS from 'csstype';
|
||||||
|
import LocaleCode from 'locale-code';
|
||||||
|
import type {Vnode, VnodeDOM} from 'mithril';
|
||||||
|
import redaxios, {Response} from 'redaxios';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-absolute-path
|
||||||
|
import logoUrl from '/images/logo_completo.png';
|
||||||
|
|
||||||
|
import {Alert} from '../Components';
|
||||||
|
import Mdi from '../Components/Mdi';
|
||||||
|
import Page from '../Components/Page';
|
||||||
|
import {getFormData, showSnackbar} from '../utils';
|
||||||
|
import {TextArea} from '../WebComponents';
|
||||||
|
|
||||||
|
function getFlag(language: string, slot: string = 'graphic', styles: CSS.Properties = {}) {
|
||||||
|
if (!styles.display) {
|
||||||
|
styles.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div slot={slot} style={styles}>
|
||||||
|
<img
|
||||||
|
style="border-radius: 4px;"
|
||||||
|
src={`https://flagicons.lipis.dev/flags/4x3/${LocaleCode.getCountryCode(
|
||||||
|
language
|
||||||
|
).toLowerCase()}.svg`}
|
||||||
|
alt={LocaleCode.getLanguageNativeName(language)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SetupPage extends Page {
|
||||||
|
languages() {
|
||||||
|
const listItems: Vnode[] = [];
|
||||||
|
|
||||||
|
for (const lang of this.page.props.languages) {
|
||||||
|
const language = lang as string;
|
||||||
|
const attributes = {
|
||||||
|
selected: this.page.props.locale === lang
|
||||||
|
};
|
||||||
|
const langCode = language.replace('_', '-');
|
||||||
|
listItems.push(
|
||||||
|
<mwc-list-item graphic="icon" value={language} {...attributes}>
|
||||||
|
{getFlag(langCode)}
|
||||||
|
<span>{LocaleCode.getLanguageNativeName(langCode)}</span>
|
||||||
|
</mwc-list-item>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attributes.selected) {
|
||||||
|
listItems.push(
|
||||||
|
getFlag(langCode, 'icon', {
|
||||||
|
display: 'block',
|
||||||
|
width: '24px',
|
||||||
|
lineHeight: '22px'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const examplesTexts: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const example of ['localhost', 'root', 'mysql', 'openstamanager']) {
|
||||||
|
examplesTexts[example] = __('Esempio: :example', {
|
||||||
|
example
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mwc-card outlined className="center ext-container">
|
||||||
|
<form id="setup">
|
||||||
|
<img src={logoUrl} className="center" alt={__('OpenSTAManager')} />
|
||||||
|
<mwc-layout-grid>
|
||||||
|
<mwc-layout-grid-cell span-desktop={8}>
|
||||||
|
<h2>
|
||||||
|
{__('Benvenuto in :name!', {
|
||||||
|
name: <strong>{__('OpenSTAManager')}</strong>
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{__(
|
||||||
|
'Puoi procedere alla configurazione tecnica del software attraverso i '
|
||||||
|
+ 'parametri seguenti, che potranno essere corretti secondo necessità tramite il file .env.'
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
{__(
|
||||||
|
"Se necessiti supporto puoi contattarci tramite l':contactLink o tramite il nostro :forumLink.",
|
||||||
|
{
|
||||||
|
contactLink: (
|
||||||
|
<a href="https://www.openstamanager.com/contattaci/?subject=Assistenza%20installazione%20OSM">
|
||||||
|
{__('assistenza ufficiale')}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
forumLink: (
|
||||||
|
<a href="https://forum.openstamanager.com">
|
||||||
|
{__('forum')}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<h4>{__('Formato date')}</h4>
|
||||||
|
<p
|
||||||
|
className="mdc-typography--subtitle2"
|
||||||
|
style="font-size: small;"
|
||||||
|
>
|
||||||
|
{__(
|
||||||
|
'I formati sono impostabili attraverso lo standard previsto da :link.',
|
||||||
|
{
|
||||||
|
link: (
|
||||||
|
<a href="https://www.php.net/manual/en/function.date.php#refsect1-function.date-parameters">
|
||||||
|
PHP
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<mwc-layout-grid inner>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<text-field
|
||||||
|
name="timestamp_format"
|
||||||
|
label={__('Formato data lunga')}
|
||||||
|
required
|
||||||
|
value="d/m/Y H:i"
|
||||||
|
>
|
||||||
|
<Mdi icon="calendar-clock" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<text-field
|
||||||
|
name="date_format"
|
||||||
|
label={__('Formato data corta')}
|
||||||
|
required
|
||||||
|
value="d/m/Y"
|
||||||
|
>
|
||||||
|
<Mdi icon="calendar-month-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<text-field
|
||||||
|
name="time_format"
|
||||||
|
label={__('Formato orario')}
|
||||||
|
required
|
||||||
|
value="H:i"
|
||||||
|
>
|
||||||
|
<Mdi icon="clock-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
</mwc-layout-grid>
|
||||||
|
<hr />
|
||||||
|
<h4>{__('Database')}</h4>
|
||||||
|
<mwc-layout-grid inner>
|
||||||
|
<mwc-layout-grid-cell span="4">
|
||||||
|
<text-field
|
||||||
|
name="host"
|
||||||
|
label={__('Host')}
|
||||||
|
required
|
||||||
|
helper={examplesTexts.localhost}
|
||||||
|
>
|
||||||
|
<Mdi icon="server-network" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell span="4">
|
||||||
|
<text-field
|
||||||
|
name="username"
|
||||||
|
label={__('Nome utente')}
|
||||||
|
required
|
||||||
|
helper={examplesTexts.root}
|
||||||
|
>
|
||||||
|
<Mdi icon="account-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell span="4">
|
||||||
|
<text-field
|
||||||
|
name="password"
|
||||||
|
label={__('Password')}
|
||||||
|
helper={examplesTexts.mysql}
|
||||||
|
>
|
||||||
|
<Mdi icon="lock-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell span="4">
|
||||||
|
<text-field
|
||||||
|
name="database_name"
|
||||||
|
label={__('Nome database')}
|
||||||
|
required
|
||||||
|
helper={examplesTexts.openstamanager}
|
||||||
|
>
|
||||||
|
<Mdi icon="database-outline" slot="icon" />
|
||||||
|
</text-field>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
</mwc-layout-grid>
|
||||||
|
<hr />
|
||||||
|
<mwc-layout-grid inner>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<small>{__('* Campi obbligatori')}</small>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<mwc-button
|
||||||
|
id="save-install"
|
||||||
|
raised
|
||||||
|
label={__('Salva e installa')}
|
||||||
|
onclick={this.onSaveButtonClicked.bind(this)}
|
||||||
|
>
|
||||||
|
<Mdi icon="check" slot="icon" />
|
||||||
|
</mwc-button>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<mwc-button
|
||||||
|
id="test-db"
|
||||||
|
outlined
|
||||||
|
label={__('Testa il database')}
|
||||||
|
onclick={this.onTestButtonClicked.bind(this)}
|
||||||
|
>
|
||||||
|
<Mdi icon="test-tube" slot="icon" />
|
||||||
|
</mwc-button>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
</mwc-layout-grid>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<h4>{__('Lingua')}</h4>
|
||||||
|
<material-select id="language-select" name="locale">
|
||||||
|
{this.languages()}
|
||||||
|
</material-select>
|
||||||
|
<hr />
|
||||||
|
<h4>{__('Licenza')}</h4>
|
||||||
|
<p>
|
||||||
|
{__(
|
||||||
|
'OpenSTAManager è tutelato dalla licenza GPL 3.0, da accettare obbligatoriamente per poter utilizzare il gestionale.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<TextArea
|
||||||
|
value={this.page.props.license as string}
|
||||||
|
rows="15"
|
||||||
|
cols="40"
|
||||||
|
disabled
|
||||||
|
style="margin-bottom: 8px;"
|
||||||
|
/>
|
||||||
|
<mwc-layout-grid inner>
|
||||||
|
<mwc-layout-grid-cell span-desktop={8} span-tablet={8}>
|
||||||
|
<mwc-formfield
|
||||||
|
label={__('Ho visionato e accetto la licenza')}
|
||||||
|
>
|
||||||
|
<mwc-checkbox name="license_agreement" />
|
||||||
|
</mwc-formfield>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
<mwc-layout-grid-cell>
|
||||||
|
<a
|
||||||
|
href="https://www.gnu.org/licenses/translations.en.html#GPL"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<mwc-button label={__('Versioni tradotte')}>
|
||||||
|
<Mdi icon="license" slot="icon" />
|
||||||
|
</mwc-button>
|
||||||
|
</a>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
</mwc-layout-grid>
|
||||||
|
</mwc-layout-grid-cell>
|
||||||
|
</mwc-layout-grid>
|
||||||
|
</form>
|
||||||
|
</mwc-card>
|
||||||
|
<mwc-fab
|
||||||
|
id="contrast-switcher"
|
||||||
|
className="sticky contrast-light"
|
||||||
|
label={__('Attiva/disattiva contrasto elevato')}
|
||||||
|
>
|
||||||
|
<Mdi icon="contrast-circle" slot="icon" className="light-bg" />
|
||||||
|
</mwc-fab>
|
||||||
|
<Alert id="test-connection-alert-error" icon="error" />
|
||||||
|
<Alert id="test-connection-alert-success" icon="success">
|
||||||
|
<p>{__('Connessione al database riuscita')}</p>
|
||||||
|
</Alert>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
$('mwc-fab#contrast-switcher').on('click', function (this: HTMLElement) {
|
||||||
|
$(this).toggleClass('contrast-light').toggleClass('contrast-dark');
|
||||||
|
$('body').toggleClass('mdc-high-contrast');
|
||||||
|
});
|
||||||
|
$('#language-select').on('action', (event: Event) => this.onLanguageSelected(event as Event & {detail: {index: number}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTestButtonClicked() {
|
||||||
|
await this.testDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSaveButtonClicked(event: Event) {
|
||||||
|
const form = $(event.target as HTMLElement)
|
||||||
|
.closest('form');
|
||||||
|
await this.save(getFormData(form));
|
||||||
|
}
|
||||||
|
|
||||||
|
onLanguageSelected(event: Event & {detail: {index: number}}) {
|
||||||
|
const {detail, target: select} = event;
|
||||||
|
const selected: HTMLImageElement | null = (select as HTMLElement).querySelector(
|
||||||
|
`mwc-list-item:nth-child(${detail.index + 1}) [slot="graphic"] img`
|
||||||
|
);
|
||||||
|
const selectIcon: HTMLImageElement | null = (select as HTMLElement).querySelector('[slot="icon"] img');
|
||||||
|
|
||||||
|
if (selected && selectIcon) {
|
||||||
|
selectIcon.src = selected.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = route('app.language', {
|
||||||
|
language: (select as HTMLInputElement).value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async testDatabase(silentSuccess = false, silentError = false): Promise<boolean> {
|
||||||
|
const form = $('form');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redaxios.options(route('setup.test'), {
|
||||||
|
data: getFormData(form)
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (!silentError) {
|
||||||
|
const alert = this.element.querySelector('#test-connection-alert-error');
|
||||||
|
if (alert) {
|
||||||
|
const content = alert.querySelector('.content');
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
content.textContent = __(
|
||||||
|
'Si è verificato un errore durante la connessione al'
|
||||||
|
+ ' database: :error',
|
||||||
|
{
|
||||||
|
error: (error as Response<{error: string}>).data.error
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(alert as MWCDialog).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silentSuccess) {
|
||||||
|
const alert = document.querySelector('#test-connection-alert-success');
|
||||||
|
if (alert) {
|
||||||
|
(alert as MWCDialog).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(data: {}) {
|
||||||
|
const test = await this.testDatabase(true);
|
||||||
|
|
||||||
|
if (!test) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redaxios.put(route('setup.save'), data);
|
||||||
|
} catch (error: any) {
|
||||||
|
await showSnackbar((error as Response<{error_description: string}>).data.error_description);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showSnackbar(__('Impostazioni salvate correttamente'));
|
||||||
|
window.location.href = route('auth.login');
|
||||||
|
}
|
||||||
|
}
|
6
resources/js/Views/index.js
vendored
6
resources/js/Views/index.js
vendored
@ -1,6 +0,0 @@
|
|||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
|
|
||||||
export { default as AdminSetupPage } from './AdminSetupPage.jsx';
|
|
||||||
export { default as Dashboard } from './Dashboard.jsx';
|
|
||||||
export { default as LoginPage } from './LoginPage.jsx';
|
|
||||||
export { default as SetupPage } from './SetupPage.jsx';
|
|
7
resources/js/Views/index.ts
Normal file
7
resources/js/Views/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
export {default as AdminSetupPage} from './AdminSetupPage';
|
||||||
|
export {default as Dashboard} from './Dashboard';
|
||||||
|
export {default as LoginPage} from './LoginPage';
|
||||||
|
export {default as ResetPasswordPage} from './ResetPasswordPage';
|
||||||
|
export {default as SetupPage} from './SetupPage';
|
@ -1,8 +1,23 @@
|
|||||||
import {Drawer as MWCDrawer} from '@material/mwc-drawer';
|
import {Drawer as MWCDrawer} from '@material/mwc-drawer';
|
||||||
import {css} from 'lit';
|
import {css} from 'lit';
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import {customElement} from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import type {JSXElement} from '../types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'material-drawer': JSXElement<MaterialDrawer>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('material-drawer')
|
||||||
export default class MaterialDrawer extends MWCDrawer {
|
export default class MaterialDrawer extends MWCDrawer {
|
||||||
static styles = [MWCDrawer.styles, css`
|
static styles = [
|
||||||
|
...MWCDrawer.styles,
|
||||||
|
css`
|
||||||
:first-child {
|
:first-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
@ -17,5 +32,3 @@ export default class MaterialDrawer extends MWCDrawer {
|
|||||||
}
|
}
|
||||||
`];
|
`];
|
||||||
}
|
}
|
||||||
|
|
||||||
window.customElements.define('material-drawer', MaterialDrawer);
|
|
@ -1,19 +1,33 @@
|
|||||||
import {Select as MWCSelect} from '@material/mwc-select';
|
import {Select as MWCSelect} from '@material/mwc-select';
|
||||||
import {waitUntil} from 'async-wait-until';
|
import {waitUntil} from 'async-wait-until';
|
||||||
import {
|
import type {TemplateResult} from 'lit';
|
||||||
css,
|
import {css, html} from 'lit';
|
||||||
html,
|
// eslint-disable-next-line import/extensions
|
||||||
type TemplateResult
|
import {customElement} from 'lit/decorators.js';
|
||||||
} from 'lit';
|
|
||||||
|
|
||||||
// noinspection JSCheckFunctionSignatures
|
import type {JSXElement} from '../types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'material-select': JSXElement<Select>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('material-select')
|
||||||
export default class Select extends MWCSelect {
|
export default class Select extends MWCSelect {
|
||||||
static styles = [MWCSelect.styles, css`
|
static styles = [
|
||||||
|
...MWCSelect.styles,
|
||||||
|
css`
|
||||||
.mdc-select__anchor {
|
.mdc-select__anchor {
|
||||||
width: var(--mdc-select-width, 200px) !important;
|
width: var(--mdc-select-width, 200px) !important;
|
||||||
height: var(--mdc-select-height, 56px) !important;
|
height: var(--mdc-select-height, 56px) !important;
|
||||||
}
|
}
|
||||||
`];
|
`
|
||||||
|
];
|
||||||
|
|
||||||
|
private _initialValidationMessage: string | undefined;
|
||||||
|
|
||||||
get nativeValidationMessage() {
|
get nativeValidationMessage() {
|
||||||
return this.formElement.validationMessage;
|
return this.formElement.validationMessage;
|
||||||
@ -21,36 +35,45 @@ export default class Select extends MWCSelect {
|
|||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
// Wait until slots are added to DOM
|
// Wait until slots are added to DOM
|
||||||
await waitUntil(() => this.shadowRoot.querySelectorAll('slot[name=icon]').length > 0);
|
await waitUntil(
|
||||||
|
() => this.shadowRoot && this.shadowRoot.querySelectorAll('slot[name=icon]').length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.shadowRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const slot = this.shadowRoot.querySelector('slot[name=icon]');
|
const slot = this.shadowRoot.querySelector('slot[name=icon]');
|
||||||
|
if (!slot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const slotClass = 'mdc-select__icon';
|
const slotClass = 'mdc-select__icon';
|
||||||
const rootClass = 'mdc-select--with-leading-icon';
|
const rootClass = 'mdc-select--with-leading-icon';
|
||||||
// noinspection DuplicatedCode
|
|
||||||
const slotParent = slot.parentElement;
|
const slotParent = slot.parentElement;
|
||||||
const rootElement = this.shadowRoot.firstElementChild;
|
const rootElement = this.shadowRoot.firstElementChild;
|
||||||
|
|
||||||
// Check if slot has content
|
// Check if slot has content
|
||||||
if (slot.assignedNodes().length > 0) {
|
if ((slot as HTMLSlotElement).assignedNodes().length > 0) {
|
||||||
slotParent.classList.add(slotClass);
|
slotParent?.classList.add(slotClass);
|
||||||
rootElement.classList.add(rootClass);
|
rootElement?.classList.add(rootClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes in slot (added/removed)
|
// Listen for changes in slot (added/removed)
|
||||||
slot.addEventListener('slotchange', () => {
|
slot.addEventListener('slotchange', () => {
|
||||||
if (slot.assignedNodes().length > 0) {
|
if ((slot as HTMLSlotElement).assignedNodes().length > 0) {
|
||||||
slotParent.classList.add(slotClass);
|
slotParent?.classList.add(slotClass);
|
||||||
rootElement.classList.add(rootClass);
|
rootElement?.classList.add(rootClass);
|
||||||
} else {
|
} else {
|
||||||
slotParent.classList.remove(slotClass);
|
slotParent?.classList.remove(slotClass);
|
||||||
rootElement.classList.remove(rootClass);
|
rootElement?.classList.remove(rootClass);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLeadingIcon(): TemplateResult {
|
renderLeadingIcon(): TemplateResult<1> {
|
||||||
return html`
|
return html`
|
||||||
<span>
|
<span>
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
@ -66,21 +89,22 @@ export default class Select extends MWCSelect {
|
|||||||
* https://github.com/material-components/material-components-web-components/issues/971
|
* https://github.com/material-components/material-components-web-components/issues/971
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
firstUpdated() {
|
async firstUpdated() {
|
||||||
if (this.validationMessage) {
|
if (this.validationMessage) {
|
||||||
this._initialValidationMessage = this.validationMessage;
|
this._initialValidationMessage = this.validationMessage;
|
||||||
}
|
}
|
||||||
super.firstUpdated();
|
|
||||||
|
await super.firstUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
reportValidity() {
|
reportValidity() {
|
||||||
const isValid = super.reportValidity();
|
const isValid = super.reportValidity();
|
||||||
|
|
||||||
// Note(cg): override validationMessage only if no initial message set.
|
// Note(cg): override validationMessage only if no initial message set.
|
||||||
if (!this._initialValidationMessage && !isValid) {
|
if (!this._initialValidationMessage && !isValid) {
|
||||||
this.validationMessage = this.nativeValidationMessage;
|
this.validationMessage = this.nativeValidationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.customElements.define('material-select', Select);
|
|
@ -1,6 +1,21 @@
|
|||||||
import {TextArea as MWCTextArea} from '@material/mwc-textarea';
|
import {TextArea as MWCTextArea} from '@material/mwc-textarea';
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import {customElement} from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import {type JSXElement} from '../types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'text-area': JSXElement<TextArea>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('text-area')
|
||||||
export default class TextArea extends MWCTextArea {
|
export default class TextArea extends MWCTextArea {
|
||||||
|
private _initialValidationMessage: string | undefined;
|
||||||
|
|
||||||
get nativeValidationMessage() {
|
get nativeValidationMessage() {
|
||||||
return this.formElement.validationMessage;
|
return this.formElement.validationMessage;
|
||||||
}
|
}
|
||||||
@ -9,17 +24,18 @@ export default class TextArea extends MWCTextArea {
|
|||||||
if (this.validationMessage) {
|
if (this.validationMessage) {
|
||||||
this._initialValidationMessage = this.validationMessage;
|
this._initialValidationMessage = this.validationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.firstUpdated();
|
super.firstUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
reportValidity() {
|
reportValidity() {
|
||||||
const isValid = super.reportValidity();
|
const isValid = super.reportValidity();
|
||||||
|
|
||||||
// Note(cg): override validationMessage only if no initial message set.
|
// Note(cg): override validationMessage only if no initial message set.
|
||||||
if (!this._initialValidationMessage && !isValid) {
|
if (!this._initialValidationMessage && !isValid) {
|
||||||
this.validationMessage = this.nativeValidationMessage;
|
this.validationMessage = this.nativeValidationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.customElements.define('text-area', TextArea);
|
|
@ -1,59 +1,69 @@
|
|||||||
import {TextField as MWCTextField} from '@material/mwc-textfield';
|
import {TextField as MWCTextField} from '@material/mwc-textfield';
|
||||||
import {waitUntil} from 'async-wait-until';
|
import {waitUntil} from 'async-wait-until';
|
||||||
|
import classnames from 'classnames';
|
||||||
import {
|
import {
|
||||||
css,
|
type TemplateResult, css, html, unsafeCSS
|
||||||
html,
|
|
||||||
type TemplateResult,
|
|
||||||
unsafeCSS
|
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import {customElement, property} from 'lit/decorators.js';
|
||||||
|
|
||||||
import styles from '../../scss/material/text-field.scss';
|
import styles from '../../scss/material/text-field.scss';
|
||||||
import classnames from 'classnames';
|
import type {JSXElement} from '../types';
|
||||||
|
|
||||||
// noinspection JSCheckFunctionSignatures
|
declare global {
|
||||||
export default class TextField extends MWCTextField {
|
namespace JSX {
|
||||||
static styles = [MWCTextField.styles, css`${unsafeCSS(styles)}`];
|
interface IntrinsicElements {
|
||||||
|
'text-field': JSXElement<TextField>;
|
||||||
static properties = {
|
|
||||||
...MWCTextField.properties,
|
|
||||||
comfortable: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
dense: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
compact: {
|
|
||||||
type: Boolean
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('text-field')
|
||||||
|
export default class TextField extends MWCTextField {
|
||||||
|
static styles = [...MWCTextField.styles, css`${unsafeCSS(styles)}`];
|
||||||
|
|
||||||
|
@property({type: Boolean}) declare comfortable: boolean;
|
||||||
|
@property({type: Boolean}) declare dense: boolean;
|
||||||
|
@property({type: Boolean}) declare compact: boolean;
|
||||||
|
private _initialValidationMessage: string | undefined;
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
// Wait until slots are added to DOM
|
// Wait until slots are added to DOM
|
||||||
await waitUntil(() => this.shadowRoot.querySelectorAll('slot[name^=icon]').length > 0);
|
await waitUntil(
|
||||||
|
() => this.shadowRoot && this.shadowRoot.querySelectorAll('slot[name^=icon]').length > 0
|
||||||
|
);
|
||||||
|
|
||||||
const slots = this.shadowRoot.querySelectorAll('slot[name^=icon]');
|
if (!this.shadowRoot) {
|
||||||
for (const slot: HTMLSlotElement of slots) {
|
return;
|
||||||
const slotClass = `mdc-text-field__icon--${slot.name === 'icon' ? 'leading' : 'trailing'}`;
|
}
|
||||||
const rootClass = `mdc-text-field--with-${slot.name === 'icon' ? 'leading' : 'trailing'}-icon`;
|
|
||||||
|
const slots: NodeListOf<HTMLSlotElement> = this.shadowRoot.querySelectorAll('slot[name^=icon]');
|
||||||
|
|
||||||
|
for (const slot of slots) {
|
||||||
|
const slotClass = `mdc-text-field__icon--${
|
||||||
|
slot.name === 'icon' ? 'leading' : 'trailing'
|
||||||
|
}`;
|
||||||
|
const rootClass = `mdc-text-field--with-${
|
||||||
|
slot.name === 'icon' ? 'leading' : 'trailing'
|
||||||
|
}-icon`;
|
||||||
const slotParent = slot.parentElement;
|
const slotParent = slot.parentElement;
|
||||||
const rootElement = this.shadowRoot.firstElementChild;
|
const rootElement = this.shadowRoot.firstElementChild;
|
||||||
|
|
||||||
// Check if slot has content
|
// Check if slot has content
|
||||||
if (slot.assignedNodes().length > 0) {
|
if (slot.assignedNodes().length > 0) {
|
||||||
slotParent.classList.add(slotClass);
|
slotParent?.classList.add(slotClass);
|
||||||
rootElement.classList.add(rootClass);
|
rootElement?.classList.add(rootClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for changes in slot (added/removed)
|
// Listen for changes in slot (added/removed)
|
||||||
slot.addEventListener('slotchange', () => {
|
slot.addEventListener('slotchange', () => {
|
||||||
if (slot.assignedNodes().length > 0) {
|
if (slot.assignedNodes().length > 0) {
|
||||||
slotParent.classList.add(slotClass);
|
slotParent?.classList.add(slotClass);
|
||||||
rootElement.classList.add(rootClass);
|
rootElement?.classList.add(rootClass);
|
||||||
} else {
|
} else {
|
||||||
slotParent.classList.remove(slotClass);
|
slotParent?.classList.remove(slotClass);
|
||||||
rootElement.classList.remove(rootClass);
|
rootElement?.classList.remove(rootClass);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -61,7 +71,9 @@ export default class TextField extends MWCTextField {
|
|||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1;
|
const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1;
|
||||||
const shouldRenderHelperText = !!this.helper || !!this.validationMessage || shouldRenderCharCounter;
|
const shouldRenderHelperText = !!this.helper
|
||||||
|
|| !!this.validationMessage
|
||||||
|
|| shouldRenderCharCounter;
|
||||||
|
|
||||||
/** @classMap */
|
/** @classMap */
|
||||||
const classes = {
|
const classes = {
|
||||||
@ -76,7 +88,6 @@ export default class TextField extends MWCTextField {
|
|||||||
'mdc-text-field--dense': this.dense,
|
'mdc-text-field--dense': this.dense,
|
||||||
'mdc-text-field--compact': this.compact
|
'mdc-text-field--compact': this.compact
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<label class="mdc-text-field ${classnames(classes)}">
|
<label class="mdc-text-field ${classnames(classes)}">
|
||||||
${this.renderRipple()}
|
${this.renderRipple()}
|
||||||
@ -93,14 +104,14 @@ export default class TextField extends MWCTextField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLeadingIcon() {
|
renderLeadingIcon() {
|
||||||
return this.renderIcon();
|
return this.renderIcon('');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTrailingIcon() {
|
renderTrailingIcon() {
|
||||||
return this.renderIcon(true);
|
return this.renderIcon('', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIcon(isTrailingIcon: boolean = false): TemplateResult {
|
renderIcon(icon: string, isTrailingIcon: boolean = false): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<span class="mdc-text-field__icon">
|
<span class="mdc-text-field__icon">
|
||||||
<slot name="icon${isTrailingIcon ? 'Trailing' : ''}"></slot>
|
<slot name="icon${isTrailingIcon ? 'Trailing' : ''}"></slot>
|
||||||
@ -124,17 +135,18 @@ export default class TextField extends MWCTextField {
|
|||||||
if (this.validationMessage) {
|
if (this.validationMessage) {
|
||||||
this._initialValidationMessage = this.validationMessage;
|
this._initialValidationMessage = this.validationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.firstUpdated();
|
super.firstUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
reportValidity() {
|
reportValidity() {
|
||||||
const isValid = super.reportValidity();
|
const isValid = super.reportValidity();
|
||||||
|
|
||||||
// Note(cg): override validationMessage only if no initial message set.
|
// Note(cg): override validationMessage only if no initial message set.
|
||||||
if (!this._initialValidationMessage && !isValid) {
|
if (!this._initialValidationMessage && !isValid) {
|
||||||
this.validationMessage = this.nativeValidationMessage;
|
this.validationMessage = this.nativeValidationMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.customElements.define('text-field', TextField);
|
|
12
resources/js/WebComponents/TopAppBar.js
vendored
12
resources/js/WebComponents/TopAppBar.js
vendored
@ -1,12 +0,0 @@
|
|||||||
import {TopAppBar as MWCTopAppBar} from '@material/mwc-top-app-bar';
|
|
||||||
import {css} from 'lit';
|
|
||||||
|
|
||||||
export default class TopAppBar extends MWCTopAppBar {
|
|
||||||
static styles = [MWCTopAppBar.styles, css`
|
|
||||||
header.mdc-top-app-bar {
|
|
||||||
border-bottom: 1px solid var(--mdc-theme-outline-color, #e0e0e0);
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
`];
|
|
||||||
}
|
|
||||||
window.customElements.define('top-app-bar', TopAppBar);
|
|
22
resources/js/WebComponents/TopAppBar.ts
Normal file
22
resources/js/WebComponents/TopAppBar.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {TopAppBar as MWCTopAppBar} from '@material/mwc-top-app-bar';
|
||||||
|
import {css} from 'lit';
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import {customElement} from 'lit/decorators.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'top-app-bar': Partial<TopAppBar>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('top-app-bar')
|
||||||
|
export default class TopAppBar extends MWCTopAppBar {
|
||||||
|
static styles = [...MWCTopAppBar.styles, css`
|
||||||
|
header.mdc-top-app-bar {
|
||||||
|
border-bottom: 1px solid var(--mdc-theme-outline-color, #e0e0e0);
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
`];
|
||||||
|
}
|
68
resources/js/_material.js
vendored
68
resources/js/_material.js
vendored
@ -1,68 +0,0 @@
|
|||||||
import '@material/mwc-button';
|
|
||||||
import '@material/mwc-drawer';
|
|
||||||
import '@material/mwc-icon-button';
|
|
||||||
import '@material/mwc-list';
|
|
||||||
import '@material/mwc-menu';
|
|
||||||
import './WebComponents/TopAppBar';
|
|
||||||
import './WebComponents/MaterialDrawer';
|
|
||||||
|
|
||||||
import {Inertia} from '@inertiajs/inertia';
|
|
||||||
import type {Dialog as MWCDialog} from '@material/mwc-dialog';
|
|
||||||
import {ListItem as MWCListItem} from '@material/mwc-list/mwc-list-item';
|
|
||||||
import type {Menu as MWCMenu} from '@material/mwc-menu';
|
|
||||||
import $ from 'cash-dom';
|
|
||||||
|
|
||||||
// Remove the ugly underline under mwc button text when inside <a> tags
|
|
||||||
$('a').has('mwc-button').css('text-decoration', 'none');
|
|
||||||
|
|
||||||
// Submit forms with enter key
|
|
||||||
$('mwc-button[type="submit"], mwc-icon-button[type="submit"]')
|
|
||||||
.closest('form')
|
|
||||||
.find('text-field, select, text-area')
|
|
||||||
.on('keydown', function (event: KeyboardEvent) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
$(this).closest('form')
|
|
||||||
.find('mwc-button[type=submit], mwc-icon-button[type="submit"]')
|
|
||||||
.trigger('click');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const drawer = document.querySelector('material-drawer');
|
|
||||||
if (drawer) {
|
|
||||||
drawer.parentElement.addEventListener('MDCTopAppBar:nav', () => {
|
|
||||||
drawer.open = !drawer.open;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drawer items click
|
|
||||||
$(drawer).find('a.drawer-item').on('click', function (event: PointerEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
Inertia.visit(this.href);
|
|
||||||
const drawerItem: MWCListItem = this.firstElementChild;
|
|
||||||
drawerItem.activated = true;
|
|
||||||
$(this).siblings('.drawer-item')
|
|
||||||
.filter((index, item) => $(item).has('mwc-list-item[activated]'))
|
|
||||||
.find('mwc-list-item')
|
|
||||||
.prop('activated', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('mwc-menu').each((index, menu: MWCMenu) => {
|
|
||||||
const trigger: Attr = menu.getAttribute('trigger');
|
|
||||||
const button = trigger ? $(`#${trigger}`) : $(menu).prev();
|
|
||||||
button.on('click', () => {
|
|
||||||
menu.open = !menu.open;
|
|
||||||
});
|
|
||||||
menu.anchor = button.get(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('mwc-dialog').each((index, dialog: MWCDialog) => {
|
|
||||||
const trigger = dialog.getAttribute('trigger');
|
|
||||||
const button = trigger ? $(`#${trigger}`) : $(dialog).prev('mwc-dialog-button');
|
|
||||||
if (button) {
|
|
||||||
button.on('click', () => {
|
|
||||||
dialog.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
112
resources/js/_material.ts
Normal file
112
resources/js/_material.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import '@material/mwc-button';
|
||||||
|
import '@material/mwc-icon-button';
|
||||||
|
import '@material/mwc-list';
|
||||||
|
import '@material/mwc-menu';
|
||||||
|
import './WebComponents/TopAppBar';
|
||||||
|
import './WebComponents/MaterialDrawer';
|
||||||
|
|
||||||
|
import {Inertia} from '@inertiajs/inertia';
|
||||||
|
import type {MWCCard} from '@maicol07/mwc-card';
|
||||||
|
import type {LayoutGrid as MWCLayoutGrid, LayoutGridCell as MWCLayoutGridCell} from '@maicol07/mwc-layout-grid';
|
||||||
|
import type {Button as MWCButton} from '@material/mwc-button';
|
||||||
|
import type {Checkbox as MWCCheckbox} from '@material/mwc-checkbox';
|
||||||
|
import type {CircularProgress as MWCCircularProgress} from '@material/mwc-circular-progress';
|
||||||
|
import type {Dialog as MWCDialog} from '@material/mwc-dialog';
|
||||||
|
import type {Drawer as MWCDrawer} from '@material/mwc-drawer';
|
||||||
|
import type {Fab as MWCFab} from '@material/mwc-fab';
|
||||||
|
import type {Formfield as MWCFormfield} from '@material/mwc-formfield';
|
||||||
|
import type {IconButton as MWCIconButton} from '@material/mwc-icon-button';
|
||||||
|
import type {IconButtonToggle as MWCIconButtonToggle} from '@material/mwc-icon-button-toggle';
|
||||||
|
import type {LinearProgress as MWCLinearProgress} from '@material/mwc-linear-progress';
|
||||||
|
import type {List as MWCList} from '@material/mwc-list';
|
||||||
|
import type {ListItem as MWCListItem} from '@material/mwc-list/mwc-list-item';
|
||||||
|
import type {Menu as MWCMenu} from '@material/mwc-menu';
|
||||||
|
import $, {
|
||||||
|
type Cash,
|
||||||
|
type Element
|
||||||
|
} from 'cash-dom';
|
||||||
|
|
||||||
|
import type {JSXElement} from './types';
|
||||||
|
|
||||||
|
// Declare Material JSX components
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'mwc-button': JSXElement<MWCButton & {dialogAction?: string | 'ok' | 'discard' | 'close' | 'cancel' | 'accept' | 'decline'}>;
|
||||||
|
'mwc-checkbox': JSXElement<MWCCheckbox>;
|
||||||
|
'mwc-card': JSXElement<MWCCard>
|
||||||
|
'mwc-circular-progress': JSXElement<MWCCircularProgress>;
|
||||||
|
'mwc-dialog': JSXElement<MWCDialog>;
|
||||||
|
'mwc-fab': JSXElement<MWCFab>;
|
||||||
|
'mwc-formfield': JSXElement<MWCFormfield>;
|
||||||
|
'mwc-icon-button': JSXElement<MWCIconButton>;
|
||||||
|
'mwc-icon-button-toggle': JSXElement<MWCIconButtonToggle>;
|
||||||
|
'mwc-layout-grid': JSXElement<MWCLayoutGrid>;
|
||||||
|
'mwc-layout-grid-cell': JSXElement<MWCLayoutGridCell> & {'span-desktop'?: number, 'span-tablet'?: number, 'span-phone'?: number};
|
||||||
|
'mwc-linear-progress': JSXElement<MWCLinearProgress>;
|
||||||
|
'mwc-list': JSXElement<MWCList>;
|
||||||
|
'mwc-list-item': JSXElement<MWCListItem>;
|
||||||
|
'mwc-menu': JSXElement<MWCMenu>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the ugly underline under mwc button text when inside <a> tags.
|
||||||
|
$('a').has('mwc-button').css('text-decoration', 'none');
|
||||||
|
|
||||||
|
// Submit forms with enter key
|
||||||
|
$('mwc-button[type="submit"], mwc-icon-button[type="submit"]')
|
||||||
|
.closest('form')
|
||||||
|
.find('text-field, select, text-area')
|
||||||
|
.on('keydown', function (this: Cash, event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
$(this).closest('form')
|
||||||
|
.find('mwc-button[type=submit], mwc-icon-button[type="submit"]')
|
||||||
|
.trigger('click');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawer: MWCDrawer | null = document.querySelector('material-drawer');
|
||||||
|
if (drawer && drawer.parentElement) {
|
||||||
|
drawer.parentElement.addEventListener('MDCTopAppBar:nav', () => {
|
||||||
|
drawer.open = !drawer.open;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drawer items click
|
||||||
|
$(drawer).find('a.drawer-item').on('click', function (this: HTMLAnchorElement, event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
Inertia.visit(this.href);
|
||||||
|
|
||||||
|
const drawerItem: Element & Partial<MWCListItem> | null = this.firstElementChild;
|
||||||
|
if (drawerItem) {
|
||||||
|
drawerItem.activated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).siblings('.drawer-item')
|
||||||
|
.filter((index, item) => $(item).has('mwc-list-item[activated]').length > 0)
|
||||||
|
.find('mwc-list-item')
|
||||||
|
.prop('activated', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('mwc-menu').each((index, menu: HTMLElement & Partial<MWCMenu>) => {
|
||||||
|
const trigger: string | null = menu.getAttribute('trigger');
|
||||||
|
const button = trigger ? $(`#${trigger}`) : $(menu).prev();
|
||||||
|
button.on('click', () => {
|
||||||
|
menu.open = !menu.open;
|
||||||
|
});
|
||||||
|
menu.anchor = button.get(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('mwc-dialog').each((index, dialog: HTMLElement & Partial<MWCDialog>) => {
|
||||||
|
const trigger = dialog.getAttribute('trigger');
|
||||||
|
const button = trigger ? $(`#${trigger}`) : $(dialog).prev('mwc-dialog-button');
|
||||||
|
if (button) {
|
||||||
|
button.on('click', () => {
|
||||||
|
(dialog as MWCDialog).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
55
resources/js/app.js
vendored
55
resources/js/app.js
vendored
@ -1,55 +0,0 @@
|
|||||||
import '../scss/app.scss';
|
|
||||||
import '@mdi/font/scss/materialdesignicons.scss';
|
|
||||||
|
|
||||||
import {InertiaProgress} from '@inertiajs/progress';
|
|
||||||
import {createInertiaApp} from '@maicol07/inertia-mithril';
|
|
||||||
import $ from 'cash-dom';
|
|
||||||
import m from 'mithril';
|
|
||||||
// noinspection SpellCheckingInspection
|
|
||||||
import redaxios from 'redaxios';
|
|
||||||
|
|
||||||
import {__} from './utils';
|
|
||||||
|
|
||||||
// Variabili globali
|
|
||||||
window.$ = $;
|
|
||||||
window.m = m;
|
|
||||||
window.__ = __;
|
|
||||||
|
|
||||||
InertiaProgress.init();
|
|
||||||
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
|
||||||
createInertiaApp({
|
|
||||||
title: title => `${title} - OpenSTAManager`,
|
|
||||||
resolve: async (name) => {
|
|
||||||
const split = name.split('::');
|
|
||||||
|
|
||||||
if (split.length === 1) {
|
|
||||||
// Load bundled page
|
|
||||||
const {default: page} = await import(`./Views/${name}.jsx`);
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load page from module
|
|
||||||
const [modulePath, page] = split;
|
|
||||||
|
|
||||||
// noinspection JSUnresolvedVariable
|
|
||||||
const osmModule = await import(
|
|
||||||
/* @vite-ignore */
|
|
||||||
`${window.import_path}/vendor/${modulePath}/index.js`
|
|
||||||
);
|
|
||||||
|
|
||||||
return osmModule[page];
|
|
||||||
},
|
|
||||||
setup({
|
|
||||||
el,
|
|
||||||
app
|
|
||||||
}) {
|
|
||||||
m.mount(el, app);
|
|
||||||
import('./_material');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#logout-button').on('click', async () => {
|
|
||||||
await redaxios.post(window.route('auth.logout'));
|
|
||||||
window.location.href = window.route('auth.login');
|
|
||||||
});
|
|
68
resources/js/app.ts
Normal file
68
resources/js/app.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable no-var,vars-on-top */
|
||||||
|
// noinspection ES6ConvertVarToLetConst
|
||||||
|
|
||||||
|
import '../scss/app.scss';
|
||||||
|
import '@mdi/font/scss/materialdesignicons.scss';
|
||||||
|
|
||||||
|
import {InertiaProgress} from '@inertiajs/progress';
|
||||||
|
import {createInertiaApp} from '@maicol07/inertia-mithril';
|
||||||
|
import cash from 'cash-dom';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
// noinspection SpellCheckingInspection
|
||||||
|
import redaxios from 'redaxios';
|
||||||
|
import type router from 'ziggy-js';
|
||||||
|
|
||||||
|
import {type Page} from './Components';
|
||||||
|
import {__ as translator} from './utils';
|
||||||
|
|
||||||
|
// Variabili globali
|
||||||
|
declare global {
|
||||||
|
const importPath: string;
|
||||||
|
const translations: {[key: string]: string};
|
||||||
|
const route: typeof router;
|
||||||
|
|
||||||
|
var $: typeof cash;
|
||||||
|
var m: typeof Mithril;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
var __: typeof translator;
|
||||||
|
}
|
||||||
|
globalThis.$ = cash;
|
||||||
|
globalThis.m = Mithril;
|
||||||
|
globalThis.__ = translator;
|
||||||
|
|
||||||
|
InertiaProgress.init();
|
||||||
|
|
||||||
|
await createInertiaApp({
|
||||||
|
title: ((title) => `${title} - OpenSTAManager`),
|
||||||
|
resolve: async (name: string) => {
|
||||||
|
const split = name.split('::');
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
// Load bundled page
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const {default: page}: {default: Page} = await import(`./Views/${name}.tsx`);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load page from module
|
||||||
|
const [modulePath, page] = split;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const osmModule: {default: unknown, [key: string]: unknown} = await import(
|
||||||
|
/* @vite-ignore */
|
||||||
|
`${importPath}/vendor/${modulePath}/index.js`
|
||||||
|
);
|
||||||
|
|
||||||
|
return osmModule[page];
|
||||||
|
},
|
||||||
|
setup({el, app}: {el: Element, app: Mithril.ComponentTypes}) {
|
||||||
|
m.mount(el, app);
|
||||||
|
import('./_material');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#logout-button')
|
||||||
|
.on('click', async () => {
|
||||||
|
await redaxios.post(route('auth.logout'));
|
||||||
|
window.location.href = route('auth.login');
|
||||||
|
});
|
10
resources/js/lib/typings.d.ts
vendored
Normal file
10
resources/js/lib/typings.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
declare module 'mithril-node-render' {
|
||||||
|
import m from 'mithril';
|
||||||
|
|
||||||
|
export const escapeText: string;
|
||||||
|
export const escapeAttribute: string;
|
||||||
|
export default function render(vnode: m.Vnode): string;
|
||||||
|
export function sync(vnode: m.Vnode): string;
|
||||||
|
}
|
@ -4,75 +4,96 @@ import type {
|
|||||||
TextFieldInputMode,
|
TextFieldInputMode,
|
||||||
TextFieldType
|
TextFieldType
|
||||||
} from '@material/mwc-textfield';
|
} from '@material/mwc-textfield';
|
||||||
|
import type {
|
||||||
|
Attributes,
|
||||||
|
CommonAttributes
|
||||||
|
} from 'mithril';
|
||||||
|
import type {Response} from 'redaxios';
|
||||||
|
|
||||||
|
export type ErrorResponse = Response<{errors: Record<string, string>}>;
|
||||||
|
|
||||||
|
export type JSXElement<T> = Omit<Partial<T>, 'children' | 'style'>
|
||||||
|
& CommonAttributes<any, any>
|
||||||
|
& {
|
||||||
|
children?: JSX.Element | JSX.Element[] | Attributes | Attributes[],
|
||||||
|
style?: string | CSSStyleDeclaration
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldT = {
|
export type FieldT = {
|
||||||
id?: string,
|
id?: string
|
||||||
name?: string,
|
name?: string
|
||||||
value?: string,
|
value?: string
|
||||||
label?: string,
|
label?: string
|
||||||
outlined?: boolean,
|
outlined?: boolean
|
||||||
helper?: string,
|
helper?: string
|
||||||
icon?: string | MaterialIcons,
|
icon?: string | MaterialIcons
|
||||||
placeholder?: string,
|
placeholder?: string
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
required?: boolean,
|
required?: boolean
|
||||||
validity?: ValidityState,
|
validity?: ValidityState
|
||||||
validityTransform?: (value: string, nativeValidity: ValidityState) => Partial<ValidityState> |
|
validityTransform?: (
|
||||||
null,
|
value: string,
|
||||||
validateOnInitialRender?: boolean,
|
nativeValidity: ValidityState,
|
||||||
validationMessage?: string,
|
) => Partial<ValidityState> | null
|
||||||
|
validateOnInitialRender?: boolean
|
||||||
|
validationMessage?: string
|
||||||
// Custom
|
// Custom
|
||||||
type?: string
|
type?: string
|
||||||
}
|
};
|
||||||
|
export type TextFieldT = FieldT & {
|
||||||
export type TextFieldT = FieldT | {
|
type?: TextFieldType
|
||||||
type?: TextFieldType,
|
prefix?: string
|
||||||
prefix?: string,
|
suffix?: string
|
||||||
suffix?: string,
|
iconTrailing?: string
|
||||||
iconTrailing?: string,
|
charCounter?: boolean
|
||||||
charCounter?: boolean,
|
helperPersistent?: boolean | string
|
||||||
helperPersistent?: boolean | string,
|
minLength?: number
|
||||||
minLength?: number,
|
maxLength?: number
|
||||||
maxLength?: number,
|
pattern?: string
|
||||||
pattern?: string,
|
min?: number | string
|
||||||
min?: number | string,
|
max?: number | string
|
||||||
max?: number | string,
|
size?: number | null
|
||||||
size?: number | null,
|
step?: number | null
|
||||||
step?: number | null,
|
autoValidate?: boolean
|
||||||
autoValidate?: boolean,
|
willValidate?: boolean
|
||||||
willValidate?: boolean,
|
name?: string
|
||||||
name?: string,
|
inputMode?: TextFieldInputMode
|
||||||
inputMode?: TextFieldInputMode,
|
readOnly?: boolean
|
||||||
readOnly?: boolean,
|
autocapitalize:
|
||||||
autocapitalize: 'on' | 'off' | 'sentences' | 'none' | 'words' | 'characters',
|
| 'on'
|
||||||
endAligned?: boolean,
|
| 'off'
|
||||||
|
| 'sentences'
|
||||||
|
| 'none'
|
||||||
|
| 'words'
|
||||||
|
| 'characters'
|
||||||
|
endAligned?: boolean
|
||||||
elementType: 'text-field'
|
elementType: 'text-field'
|
||||||
};
|
};
|
||||||
|
export type TextAreaT = FieldT & {
|
||||||
export type TextAreaT = FieldT | {
|
rows?: number
|
||||||
rows?: number,
|
cols?: number
|
||||||
cols?: number,
|
type?: TextFieldType
|
||||||
type?: TextFieldType,
|
iconTrailing?: string
|
||||||
iconTrailing?: string,
|
charCounter?: boolean | TextAreaCharCounter
|
||||||
charCounter?: boolean | TextAreaCharCounter,
|
willValidate?: boolean
|
||||||
willValidate?: boolean,
|
helperPersistent?: boolean | string
|
||||||
helperPersistent?: boolean | string,
|
maxLength?: number
|
||||||
maxLength?: number,
|
|
||||||
elementType: 'text-area'
|
elementType: 'text-area'
|
||||||
};
|
};
|
||||||
|
export type SelectT = FieldT & {
|
||||||
export type SelectT = FieldT | {
|
multiple?: boolean
|
||||||
multiple?: boolean,
|
naturalMenuWidth?: boolean
|
||||||
naturalMenuWidth?: boolean,
|
fixedMenuPosition?: boolean
|
||||||
fixedMenuPosition?: boolean,willValidate?: boolean,
|
willValidate?: boolean
|
||||||
elementType: 'material-select',
|
elementType: 'material-select'
|
||||||
selected?: ListItemBase | null,
|
selected?: ListItemBase | null
|
||||||
items?: ListItemBase[],
|
items?: ListItemBase[]
|
||||||
index?: number,
|
index?: number
|
||||||
options?: {label: string, value: string}[]
|
options?: {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MaterialIcons =
|
export type MaterialIcons =
|
||||||
| '10k'
|
| '10k'
|
||||||
| '10mp'
|
| '10mp'
|
||||||
@ -1870,4 +1891,4 @@ export type MaterialIcons =
|
|||||||
| 'youtube_searched_for'
|
| 'youtube_searched_for'
|
||||||
| 'zoom_in'
|
| 'zoom_in'
|
||||||
| 'zoom_out'
|
| 'zoom_out'
|
||||||
| 'zoom_out_map'
|
| 'zoom_out_map';
|
@ -1,16 +1,16 @@
|
|||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
import '@material/mwc-snackbar';
|
import '@material/mwc-snackbar';
|
||||||
|
import 'mithril';
|
||||||
|
|
||||||
import type {Cash} from 'cash-dom/dist/cash';
|
import type {Cash} from 'cash-dom/dist/cash';
|
||||||
import {type Vnode} from 'mithril';
|
import type {Vnode} from 'mithril';
|
||||||
import {sync as render} from 'mithril-node-render';
|
import {sync as render} from 'mithril-node-render';
|
||||||
|
|
||||||
|
type GenericObject = object & {prototype: any};
|
||||||
/**
|
/**
|
||||||
* Check if class/object A is the same as or a subclass of class B.
|
* Check if class/object A is the same as or a subclass of class B.
|
||||||
*/
|
*/
|
||||||
export function subclassOf(A: { ... }, B: { ... }): boolean {
|
export function subclassOf(A: GenericObject, B: any): boolean {
|
||||||
// noinspection JSUnresolvedVariable
|
|
||||||
return A && (A === B || A.prototype instanceof B);
|
return A && (A === B || A.prototype instanceof B);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,15 +31,23 @@ export function containsHTML(string_: string): boolean {
|
|||||||
* thrown. Defaults to 5000 (5 seconds).
|
* thrown. Defaults to 5000 (5 seconds).
|
||||||
* @param {string} actionText Text of the action button
|
* @param {string} actionText Text of the action button
|
||||||
* @param {string} cancelText Text of the cancel button
|
* @param {string} cancelText Text of the cancel button
|
||||||
* @param {boolean} closeOtherSnackbars Whether to close other snackbars before showing this one
|
* @param {boolean} closeOtherSnackbars Whether to close other snackbars before showing this one.
|
||||||
*/
|
*/
|
||||||
export async function showSnackbar(labelText: string, timeoutMs: number | false = 5000, actionText = 'OK', cancelText: ?string, closeOtherSnackbars = true): Promise<boolean> {
|
export async function showSnackbar(
|
||||||
|
labelText: string,
|
||||||
|
timeoutMs: number | false = 5000,
|
||||||
|
actionText = 'OK',
|
||||||
|
cancelText: string | false = false,
|
||||||
|
closeOtherSnackbars = true
|
||||||
|
): Promise<unknown> {
|
||||||
if (closeOtherSnackbars) {
|
if (closeOtherSnackbars) {
|
||||||
const snackbars = document.querySelectorAll('mwc-snackbar');
|
const snackbars = document.querySelectorAll('mwc-snackbar');
|
||||||
|
|
||||||
for (const snackbar of snackbars) {
|
for (const snackbar of snackbars) {
|
||||||
if (snackbar.open) {
|
if (snackbar.open) {
|
||||||
snackbar.close();
|
snackbar.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbar.remove();
|
snackbar.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,18 +55,21 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
|||||||
const snackbar = document.createElement('mwc-snackbar');
|
const snackbar = document.createElement('mwc-snackbar');
|
||||||
snackbar.labelText = labelText;
|
snackbar.labelText = labelText;
|
||||||
snackbar.timeoutMs = timeoutMs || -1;
|
snackbar.timeoutMs = timeoutMs || -1;
|
||||||
|
|
||||||
if (actionText) {
|
if (actionText) {
|
||||||
const button = document.createElement('mwc-button');
|
const button = document.createElement('mwc-button');
|
||||||
button.label = actionText;
|
button.label = actionText;
|
||||||
button.slot = 'action';
|
button.slot = 'action';
|
||||||
snackbar.append(button);
|
snackbar.append(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelText) {
|
if (cancelText) {
|
||||||
const button = document.createElement('mwc-button');
|
const button = document.createElement('mwc-button');
|
||||||
button.label = cancelText;
|
button.label = cancelText;
|
||||||
button.slot = 'cancel';
|
button.slot = 'cancel';
|
||||||
snackbar.append(button);
|
snackbar.append(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.append(snackbar);
|
document.body.append(snackbar);
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
@ -66,7 +77,7 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
|||||||
|
|
||||||
// noinspection JSUnusedLocalSymbols
|
// noinspection JSUnusedLocalSymbols
|
||||||
const reasonPromise = new Promise((resolve, reject) => {});
|
const reasonPromise = new Promise((resolve, reject) => {});
|
||||||
snackbar.addEventListener('MDCSnackbar:closed', (event) => {
|
snackbar.addEventListener('MDCSnackbar:closed', (event: Event & Partial<{detail: {reason?: string}}>) => {
|
||||||
response(event?.detail?.reason === 'action' ?? false);
|
response(event?.detail?.reason === 'action' ?? false);
|
||||||
});
|
});
|
||||||
snackbar.show();
|
snackbar.show();
|
||||||
@ -75,11 +86,9 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
|||||||
});
|
});
|
||||||
return reasonPromise;
|
return reasonPromise;
|
||||||
}
|
}
|
||||||
|
export function getFormData(form: Cash) {
|
||||||
export function getFormData(form: Cash): {...} {
|
return Object.fromEntries<string | File>(new FormData(form[0] as HTMLFormElement));
|
||||||
return Object.fromEntries(new FormData(form[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFormValid(element: Cash | HTMLFormElement): boolean {
|
export function isFormValid(element: Cash | HTMLFormElement): boolean {
|
||||||
let form = element;
|
let form = element;
|
||||||
|
|
||||||
@ -88,55 +97,77 @@ export function isFormValid(element: Cash | HTMLFormElement): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isValid: boolean = true;
|
let isValid: boolean = true;
|
||||||
|
form
|
||||||
form.find('text-field, text-area')
|
.find('text-field, text-area')
|
||||||
.each((index: number, field: HTMLInputElement) => {
|
.each((index: number, field: HTMLElement & Partial<HTMLInputElement>) => {
|
||||||
if (!field.reportValidity()) {
|
if (!(field as HTMLInputElement).reportValidity()) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validatePassword(password: HTMLInputElement, passwordConfirm: HTMLInputElement) {
|
||||||
|
if (password && passwordConfirm) {
|
||||||
|
(passwordConfirm).setCustomValidity(
|
||||||
|
(password).value !== (passwordConfirm).value
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
? __('Le password non corrispondono')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplaceObject = Record<string, string | Vnode | number | boolean>;
|
||||||
/**
|
/**
|
||||||
* Ritorna una traduzione
|
* Ritorna una traduzione
|
||||||
*
|
*
|
||||||
* @param {string|Vnode} key Stringa di cui prelevare la traduzione
|
* @param {string} key Stringa di cui prelevare la traduzione
|
||||||
* @param {Object|boolean} replace Eventuali parametri da rimpiazzare.
|
* @param {Object|boolean} replace Eventuali parametri da rimpiazzare.
|
||||||
* Se il parametro è "true" (valore booleano), verrà ritornato il valore come stringa
|
* Se il parametro è "true" (valore booleano), verrà ritornato il valore come stringa
|
||||||
* (stesso funzionamento del parametro dedicato (sotto ↓))
|
* (stesso funzionamento del parametro dedicato (sotto ↓))
|
||||||
* @param {boolean} returnAsString Se impostato a "true" vien ritornata una stringa invece di
|
* @param {boolean} returnAsString Se impostato a "true" vien ritornata una stringa invece di
|
||||||
* un Vnode di Mithril
|
* un Vnode di Mithril
|
||||||
*
|
*
|
||||||
* @returns {Vnode}
|
* @returns {string} Stringa se non contiene HTML, altrimenti Vnode
|
||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export function __(
|
export function __(
|
||||||
key: string | Vnode,
|
key: string,
|
||||||
replace: { [string]: string | Vnode | any } | boolean = {},
|
replace: ReplaceObject | boolean = {},
|
||||||
returnAsString: boolean = false
|
returnAsString: boolean = false
|
||||||
): Vnode | string {
|
): string {
|
||||||
let translation = key;
|
let translation = key;
|
||||||
|
|
||||||
// noinspection JSUnresolvedVariable
|
// noinspection JSUnresolvedVariable
|
||||||
if (window.translations && window.translations[key]) {
|
if (translations && translations[key]) {
|
||||||
translation = window.translations[key];
|
translation = translations[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns translation as string (no parameters replacement)
|
// Returns translation as string (no parameters replacement)
|
||||||
if ((typeof replace === 'boolean' && replace) || (replace.length === 0 && !containsHTML(translation))) {
|
if (replace === true
|
||||||
|
|| (typeof replace === 'object' && !containsHTML(translation))
|
||||||
|
) {
|
||||||
return translation;
|
return translation;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const k of Object.keys(replace)) {
|
for (const k of Object.keys(replace)) {
|
||||||
// `'attrs' in replace[k]` checks if `replace[k]` is a Mithril Vnode
|
const replacement = (replace as ReplaceObject)[k];
|
||||||
translation = translation.replace(`:${k}`, ((typeof replace[k] === 'object' && 'attrs' in replace[k]) ? render(replace[k]) : replace[k]));
|
|
||||||
|
// `'attrs' in replacement` checks if `replacement` is a Mithril Vnode.
|
||||||
|
translation = translation.replace(
|
||||||
|
`:${k}`,
|
||||||
|
typeof replacement === 'object' && 'attrs' in replacement
|
||||||
|
? render(replacement)
|
||||||
|
: replacement as string
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnAsString || !containsHTML(translation)) {
|
if (returnAsString || !containsHTML(translation)) {
|
||||||
return translation;
|
return translation;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.m.trust(translation);
|
return translation;
|
||||||
}
|
}
|
@ -31,7 +31,7 @@
|
|||||||
@include('layouts.top-app-bar-menus')
|
@include('layouts.top-app-bar-menus')
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.import_path = '{{Str::contains(vite_asset(''), config('vite.dev_url')) ? config('vite.dev_url') : '.'}}';
|
window.importPath = '{{Str::contains(vite_asset(''), config('vite.dev_url')) ? config('vite.dev_url') : '.'}}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@routes
|
@routes
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"esnext",
|
"esnext",
|
||||||
"dom"
|
"dom",
|
||||||
|
"dom.iterable"
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"vite/client"
|
"vite/client"
|
||||||
],
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"paths": []
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"resources/**/*"
|
"resources/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user