mirror of
https://github.com/devcode-it/openstamanager.git
synced 2025-02-22 22:37:37 +01: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:
parent
8621a6aa88
commit
636c7ac68e
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>
|
||||
<option name="WRAP_ON_TYPING" value="1" />
|
||||
</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">
|
||||
<option name="RIGHT_MARGIN" value="100" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
@ -36,5 +46,21 @@
|
||||
<option name="INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</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>
|
||||
</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"?>
|
||||
<project version="4">
|
||||
<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" />
|
||||
</component>
|
||||
</project>
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="FLOW" />
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectResources">
|
||||
<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>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<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" />
|
||||
</component>
|
||||
</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) {
|
||||
$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;
|
||||
|
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.
|
||||
*/
|
||||
'commands' => [
|
||||
'vite:aliases',
|
||||
//'vite:aliases',
|
||||
// 'typescript:generate'
|
||||
],
|
||||
];
|
||||
|
20
package.json
20
package.json
@ -15,7 +15,7 @@
|
||||
"dependencies": {
|
||||
"@inertiajs/inertia": "^0.10.1",
|
||||
"@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-layout-grid": "^0.25.3-1",
|
||||
"@material/data-table": "^13.0.0",
|
||||
@ -42,31 +42,31 @@
|
||||
"@material/theme": "^13.0.0",
|
||||
"@material/typography": "^13.0.0",
|
||||
"@mdi/font": "^6.5.95",
|
||||
"async-wait-until": "2.0.9",
|
||||
"async-wait-until": "2.0.12",
|
||||
"cash-dom": "^8.1.0",
|
||||
"classnames": "^2.3.1",
|
||||
"collect.js": "^4.29.3",
|
||||
"coloquent": "^2.4.0",
|
||||
"collect.js": "^4.30.3",
|
||||
"coloquent": "^2.4.1",
|
||||
"include-media": "^1.4.10",
|
||||
"lit": "^2.0.2",
|
||||
"locale-code": "^2.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash": "npm:lodash-es",
|
||||
"lottie-web": "^5.8.1",
|
||||
"mithril": "^2.0.4",
|
||||
"mithril-node-render": "^3.0.2",
|
||||
"modern-normalize": "^1.1.0",
|
||||
"prop-types": "^15.8.0",
|
||||
"redaxios": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adeira/babel-preset-adeira": "^4.0.0",
|
||||
"@maicol07/eslint-config": "^1.1.3",
|
||||
"@maicol07/eslint-config": "^2.1.2",
|
||||
"@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",
|
||||
"concurrently": "^6.5.1",
|
||||
"concurrently": "^7.0.0",
|
||||
"csstype": "^3.0.10",
|
||||
"laravel-vite": "^0.0.23",
|
||||
"sass": "^1.45.1",
|
||||
"sass": "^1.45.2",
|
||||
"stylelint": "^14.2.0",
|
||||
"stylelint-config-html": "^1.0.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 {inRange} from 'lodash-es';
|
||||
import PropTypes from 'prop-types';
|
||||
import type {Cash} from 'cash-dom/dist/cash';
|
||||
import {inRange} from 'lodash';
|
||||
import type {
|
||||
Vnode,
|
||||
VnodeDOM
|
||||
} from 'mithril';
|
||||
|
||||
import Component from '../Component.jsx';
|
||||
import Component from '../Component';
|
||||
|
||||
export default class TableCell extends Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string
|
||||
};
|
||||
declare global {
|
||||
namespace JSX {
|
||||
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', {
|
||||
[`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') {
|
||||
vnode.children = <mwc-checkbox className="mdc-data-table__row-checkbox"/>;
|
||||
if (
|
||||
(!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>;
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
oncreate(vnode: VnodeDOM<Attributes>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
const checkboxes = (): Cash => $(this.element)
|
||||
@ -32,8 +46,9 @@ export default class TableCell extends Component {
|
||||
cell.children('mwc-checkbox').on('change', () => {
|
||||
const row = cell.parent();
|
||||
row.toggleClass('mdc-data-table__row--selected');
|
||||
|
||||
const headerCheckbox = cell.closest('.mdc-data-table').find('thead th mwc-checkbox');
|
||||
const headerCheckbox = cell
|
||||
.closest('.mdc-data-table')
|
||||
.find('thead th mwc-checkbox');
|
||||
const checks = checkboxes();
|
||||
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 PropTypes from 'prop-types';
|
||||
import Component from './Component';
|
||||
|
||||
import Component from './Component.jsx';
|
||||
type Attributes = {
|
||||
icon?: string
|
||||
};
|
||||
|
||||
export default class Mdi extends Component implements ClassComponent<{icon?: string}> {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string
|
||||
};
|
||||
|
||||
view(vnode) {
|
||||
this.attrs.addClassNames('mdi', `mdi-${this.attrs.pull('icon')}`);
|
||||
return <i {...this.attrs.all()} />;
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
Mdi: Mdi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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ì:
|
||||
import * as mdi from '@mdi/js';
|
||||
import {camelCase} from 'lodash-es/string';
|
||||
import {camelCase} from 'lodash/string';
|
||||
|
||||
return <svg class={`mdi ${vnode.attrs.class ?? ''}`}
|
||||
{...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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
* every time it is called.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @example <caption>Example usage of extending one method.</caption>
|
||||
@ -23,25 +37,30 @@
|
||||
* // something that needs to be run on creation and update
|
||||
* });
|
||||
*
|
||||
* @param {object} proto The prototype of the object/class that owns the method
|
||||
* @param {string|string[]} methods The name or names of the method(s) to extend
|
||||
* @param {function} callback A callback which mutates the method's output
|
||||
* @param object The object that owns the method
|
||||
* @param methods The name or names of the method(s) to extend
|
||||
* @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];
|
||||
|
||||
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;
|
||||
|
||||
Reflect.apply(callback, this, [value, ...arguments_]);
|
||||
|
||||
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
|
||||
* });
|
||||
*
|
||||
* @param {object} object The object that owns the method
|
||||
* @param {string|string[]} methods The name or names of the method(s) to override
|
||||
* @param {function} newMethod The method to replace it with
|
||||
* @param object The object that owns the method
|
||||
* @param methods The name or names of the method(s) to override
|
||||
* @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];
|
||||
|
||||
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_]);
|
||||
};
|
||||
} as T[K];
|
||||
|
||||
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,
|
||||
Model as BaseModel
|
||||
} from 'coloquent';
|
||||
import {snakeCase} from 'lodash-es';
|
||||
import {snakeCase} from 'lodash';
|
||||
|
||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
||||
/**
|
||||
* The base model for all models.
|
||||
*
|
||||
* @property {number} id
|
||||
* @abstract
|
||||
*/
|
||||
export default class Model extends BaseModel {
|
||||
jsonApiType: string;
|
||||
export default abstract class Model extends BaseModel {
|
||||
jsonApiType: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 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, {
|
||||
get(target: this, property, receiver) {
|
||||
get(target, property: string, receiver): any {
|
||||
const snakeCasedProperty = snakeCase(property);
|
||||
if (snakeCasedProperty in target.getAttributes()) {
|
||||
return target.getAttribute(snakeCasedProperty);
|
||||
@ -28,7 +24,7 @@ export default class Model extends BaseModel {
|
||||
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
set(target: this, property, value, receiver) {
|
||||
set(target, property: string, value) {
|
||||
target.setAttribute(snakeCase(property), value);
|
||||
return true;
|
||||
}
|
||||
@ -39,14 +35,15 @@ export default class Model extends BaseModel {
|
||||
* Just an alias to the get() method.
|
||||
*
|
||||
* Returns all the instances of the model.
|
||||
*/
|
||||
*/ // @ts-ignore
|
||||
static all(): Promise<PluralResponse<InstanceType<Model>>> {
|
||||
// @ts-ignore
|
||||
return this.get();
|
||||
}
|
||||
|
||||
setAttributes(attributes: { [string]: any }): void {
|
||||
setAttributes(attributes: Record<string, any>): void {
|
||||
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);
|
||||
}
|
||||
|
||||
getAttributes(): { [p: string]: any } {
|
||||
getAttributes(): {[p: string]: any} {
|
||||
return super.getAttributes();
|
||||
}
|
||||
|
||||
@ -69,4 +66,8 @@ export default class Model extends BaseModel {
|
||||
getJsonApiType(): string {
|
||||
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 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.jsx';
|
||||
import Mdi from '../Components/Mdi.jsx';
|
||||
import Page from '../Components/Page.jsx';
|
||||
import {
|
||||
getFormData,
|
||||
isFormValid,
|
||||
showSnackbar
|
||||
} from '../utils';
|
||||
import LoadingButton from '../Components/LoadingButton';
|
||||
import Mdi from '../Components/Mdi';
|
||||
import Page from '../Components/Page';
|
||||
import {ErrorResponse} from '../types';
|
||||
import {getFormData, isFormValid, showSnackbar} from '../utils';
|
||||
|
||||
export default class LoginPage extends Page {
|
||||
loading: Cash;
|
||||
forgotPasswordLoading: Cash;
|
||||
|
||||
view(vnode) {
|
||||
view(vnode: Vnode) {
|
||||
return (
|
||||
<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;">
|
||||
<h3 style="margin-top: 0;">{__('Accedi')}</h3>
|
||||
<text-field label={__('Nome utente/email')} id="username" name="username" required style="margin-bottom: 16px;">
|
||||
<Mdi icon="account-outline" slot="icon"/>
|
||||
<text-field
|
||||
label={__('Nome utente/email')}
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
style="margin-bottom: 16px;"
|
||||
>
|
||||
<Mdi icon="account-outline" slot="icon" />
|
||||
</text-field>
|
||||
<text-field label={__('Password')} id="password" name="password" required type="password">
|
||||
<Mdi icon="lock-outline" slot="icon"/>
|
||||
<text-field
|
||||
label={__('Password')}
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
>
|
||||
<Mdi icon="lock-outline" slot="icon" />
|
||||
</text-field>
|
||||
<mwc-formfield label={__('Ricordami')} style="display: block;">
|
||||
<mwc-checkbox id="remember" name="remember"/>
|
||||
<mwc-checkbox id="remember" name="remember" />
|
||||
</mwc-formfield>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
@ -60,17 +77,17 @@ export default class LoginPage extends Page {
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
oncreate(vnode: VnodeDOM) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
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) {
|
||||
event.preventDefault();
|
||||
this.loading.show();
|
||||
|
||||
const form = $(this.element).find('#login');
|
||||
|
||||
if (!isFormValid(form)) {
|
||||
@ -79,48 +96,49 @@ export default class LoginPage extends Page {
|
||||
}
|
||||
|
||||
const formData = getFormData(form);
|
||||
|
||||
formData._token = $('meta[name="csrf-token"]').attr('content');
|
||||
formData._token = $('meta[name="csrf-token"]').attr('content') as string;
|
||||
|
||||
try {
|
||||
await redaxios(window.route('auth.authenticate'), {
|
||||
await redaxios(route('auth.authenticate'), {
|
||||
method: 'POST',
|
||||
data: formData
|
||||
});
|
||||
} catch (error) {
|
||||
// noinspection ES6MissingAwait
|
||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
||||
} catch (error: any) {
|
||||
this.loading.hide();
|
||||
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = window.route('dashboard');
|
||||
window.location.href = route('dashboard');
|
||||
}
|
||||
|
||||
async onForgotPasswordButtonClicked() {
|
||||
this.forgotPasswordLoading.show();
|
||||
const field: HTMLFormElement = document.querySelector('#username');
|
||||
field.type = 'email';
|
||||
if (!field.reportValidity()) {
|
||||
const field: HTMLInputElement | null = this.element.querySelector('#username');
|
||||
if (field) {
|
||||
field.type = 'email';
|
||||
|
||||
if (!field.reportValidity()) {
|
||||
field.type = 'text';
|
||||
return;
|
||||
}
|
||||
|
||||
field.type = 'text';
|
||||
return;
|
||||
}
|
||||
field.type = 'text';
|
||||
|
||||
try {
|
||||
await redaxios.post(window.route('password.forgot'), {
|
||||
email: field.value,
|
||||
_token: $('meta[name="csrf-token"]').attr('content')
|
||||
});
|
||||
} catch (error) {
|
||||
// noinspection ES6MissingAwait
|
||||
showSnackbar(Object.values(error.data.errors).join(' '), false);
|
||||
try {
|
||||
await redaxios.post(route('password.forgot'), {
|
||||
email: field.value,
|
||||
_token: $('meta[name="csrf-token"]')
|
||||
.attr('content')
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.loading.hide();
|
||||
await showSnackbar(Object.values((error as ErrorResponse).data.errors).join(' '), false);
|
||||
return;
|
||||
}
|
||||
|
||||
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 {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 {
|
||||
static styles = [MWCDrawer.styles, css`
|
||||
static styles = [
|
||||
...MWCDrawer.styles,
|
||||
css`
|
||||
:first-child {
|
||||
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 {waitUntil} from 'async-wait-until';
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
type TemplateResult
|
||||
} from 'lit';
|
||||
import type {TemplateResult} from 'lit';
|
||||
import {css, html} from 'lit';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import {customElement} from 'lit/decorators.js';
|
||||
|
||||
// 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 {
|
||||
static styles = [MWCSelect.styles, css`
|
||||
static styles = [
|
||||
...MWCSelect.styles,
|
||||
css`
|
||||
.mdc-select__anchor {
|
||||
width: var(--mdc-select-width, 200px) !important;
|
||||
height: var(--mdc-select-height, 56px) !important;
|
||||
}
|
||||
`];
|
||||
`
|
||||
];
|
||||
|
||||
private _initialValidationMessage: string | undefined;
|
||||
|
||||
get nativeValidationMessage() {
|
||||
return this.formElement.validationMessage;
|
||||
@ -21,36 +35,45 @@ export default class Select extends MWCSelect {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// 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]');
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotClass = 'mdc-select__icon';
|
||||
const rootClass = 'mdc-select--with-leading-icon';
|
||||
// noinspection DuplicatedCode
|
||||
|
||||
const slotParent = slot.parentElement;
|
||||
const rootElement = this.shadowRoot.firstElementChild;
|
||||
|
||||
// Check if slot has content
|
||||
if (slot.assignedNodes().length > 0) {
|
||||
slotParent.classList.add(slotClass);
|
||||
rootElement.classList.add(rootClass);
|
||||
if ((slot as HTMLSlotElement).assignedNodes().length > 0) {
|
||||
slotParent?.classList.add(slotClass);
|
||||
rootElement?.classList.add(rootClass);
|
||||
}
|
||||
|
||||
// Listen for changes in slot (added/removed)
|
||||
slot.addEventListener('slotchange', () => {
|
||||
if (slot.assignedNodes().length > 0) {
|
||||
slotParent.classList.add(slotClass);
|
||||
rootElement.classList.add(rootClass);
|
||||
if ((slot as HTMLSlotElement).assignedNodes().length > 0) {
|
||||
slotParent?.classList.add(slotClass);
|
||||
rootElement?.classList.add(rootClass);
|
||||
} else {
|
||||
slotParent.classList.remove(slotClass);
|
||||
rootElement.classList.remove(rootClass);
|
||||
slotParent?.classList.remove(slotClass);
|
||||
rootElement?.classList.remove(rootClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderLeadingIcon(): TemplateResult {
|
||||
renderLeadingIcon(): TemplateResult<1> {
|
||||
return html`
|
||||
<span>
|
||||
<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
|
||||
*
|
||||
*/
|
||||
firstUpdated() {
|
||||
async firstUpdated() {
|
||||
if (this.validationMessage) {
|
||||
this._initialValidationMessage = this.validationMessage;
|
||||
}
|
||||
super.firstUpdated();
|
||||
|
||||
await super.firstUpdated();
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
const isValid = super.reportValidity();
|
||||
|
||||
// Note(cg): override validationMessage only if no initial message set.
|
||||
if (!this._initialValidationMessage && !isValid) {
|
||||
this.validationMessage = this.nativeValidationMessage;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('material-select', Select);
|
@ -1,6 +1,21 @@
|
||||
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 {
|
||||
private _initialValidationMessage: string | undefined;
|
||||
|
||||
get nativeValidationMessage() {
|
||||
return this.formElement.validationMessage;
|
||||
}
|
||||
@ -9,17 +24,18 @@ export default class TextArea extends MWCTextArea {
|
||||
if (this.validationMessage) {
|
||||
this._initialValidationMessage = this.validationMessage;
|
||||
}
|
||||
|
||||
super.firstUpdated();
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
const isValid = super.reportValidity();
|
||||
|
||||
// Note(cg): override validationMessage only if no initial message set.
|
||||
if (!this._initialValidationMessage && !isValid) {
|
||||
this.validationMessage = this.nativeValidationMessage;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('text-area', TextArea);
|
@ -1,59 +1,69 @@
|
||||
import {TextField as MWCTextField} from '@material/mwc-textfield';
|
||||
import {waitUntil} from 'async-wait-until';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
type TemplateResult,
|
||||
unsafeCSS
|
||||
type TemplateResult, css, html, unsafeCSS
|
||||
} from 'lit';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import {customElement, property} from 'lit/decorators.js';
|
||||
|
||||
import styles from '../../scss/material/text-field.scss';
|
||||
import classnames from 'classnames';
|
||||
import type {JSXElement} from '../types';
|
||||
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
export default class TextField extends MWCTextField {
|
||||
static styles = [MWCTextField.styles, css`${unsafeCSS(styles)}`];
|
||||
|
||||
static properties = {
|
||||
...MWCTextField.properties,
|
||||
comfortable: {
|
||||
type: Boolean
|
||||
},
|
||||
dense: {
|
||||
type: Boolean
|
||||
},
|
||||
compact: {
|
||||
type: Boolean
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'text-field': JSXElement<TextField>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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() {
|
||||
super.connectedCallback();
|
||||
|
||||
// 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]');
|
||||
for (const slot: HTMLSlotElement 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`;
|
||||
if (!this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 rootElement = this.shadowRoot.firstElementChild;
|
||||
|
||||
// Check if slot has content
|
||||
if (slot.assignedNodes().length > 0) {
|
||||
slotParent.classList.add(slotClass);
|
||||
rootElement.classList.add(rootClass);
|
||||
slotParent?.classList.add(slotClass);
|
||||
rootElement?.classList.add(rootClass);
|
||||
}
|
||||
|
||||
// Listen for changes in slot (added/removed)
|
||||
slot.addEventListener('slotchange', () => {
|
||||
if (slot.assignedNodes().length > 0) {
|
||||
slotParent.classList.add(slotClass);
|
||||
rootElement.classList.add(rootClass);
|
||||
slotParent?.classList.add(slotClass);
|
||||
rootElement?.classList.add(rootClass);
|
||||
} else {
|
||||
slotParent.classList.remove(slotClass);
|
||||
rootElement.classList.remove(rootClass);
|
||||
slotParent?.classList.remove(slotClass);
|
||||
rootElement?.classList.remove(rootClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -61,7 +71,9 @@ export default class TextField extends MWCTextField {
|
||||
|
||||
render(): TemplateResult {
|
||||
const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1;
|
||||
const shouldRenderHelperText = !!this.helper || !!this.validationMessage || shouldRenderCharCounter;
|
||||
const shouldRenderHelperText = !!this.helper
|
||||
|| !!this.validationMessage
|
||||
|| shouldRenderCharCounter;
|
||||
|
||||
/** @classMap */
|
||||
const classes = {
|
||||
@ -76,7 +88,6 @@ export default class TextField extends MWCTextField {
|
||||
'mdc-text-field--dense': this.dense,
|
||||
'mdc-text-field--compact': this.compact
|
||||
};
|
||||
|
||||
return html`
|
||||
<label class="mdc-text-field ${classnames(classes)}">
|
||||
${this.renderRipple()}
|
||||
@ -93,14 +104,14 @@ export default class TextField extends MWCTextField {
|
||||
}
|
||||
|
||||
renderLeadingIcon() {
|
||||
return this.renderIcon();
|
||||
return this.renderIcon('');
|
||||
}
|
||||
|
||||
renderTrailingIcon() {
|
||||
return this.renderIcon(true);
|
||||
return this.renderIcon('', true);
|
||||
}
|
||||
|
||||
renderIcon(isTrailingIcon: boolean = false): TemplateResult {
|
||||
renderIcon(icon: string, isTrailingIcon: boolean = false): TemplateResult {
|
||||
return html`
|
||||
<span class="mdc-text-field__icon">
|
||||
<slot name="icon${isTrailingIcon ? 'Trailing' : ''}"></slot>
|
||||
@ -124,17 +135,18 @@ export default class TextField extends MWCTextField {
|
||||
if (this.validationMessage) {
|
||||
this._initialValidationMessage = this.validationMessage;
|
||||
}
|
||||
|
||||
super.firstUpdated();
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
const isValid = super.reportValidity();
|
||||
|
||||
// Note(cg): override validationMessage only if no initial message set.
|
||||
if (!this._initialValidationMessage && !isValid) {
|
||||
this.validationMessage = this.nativeValidationMessage;
|
||||
}
|
||||
|
||||
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,
|
||||
TextFieldType
|
||||
} 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 = {
|
||||
id?: string,
|
||||
name?: string,
|
||||
value?: string,
|
||||
label?: string,
|
||||
outlined?: boolean,
|
||||
helper?: string,
|
||||
icon?: string | MaterialIcons,
|
||||
placeholder?: string,
|
||||
disabled?: boolean,
|
||||
required?: boolean,
|
||||
validity?: ValidityState,
|
||||
validityTransform?: (value: string, nativeValidity: ValidityState) => Partial<ValidityState> |
|
||||
null,
|
||||
validateOnInitialRender?: boolean,
|
||||
validationMessage?: string,
|
||||
|
||||
id?: string
|
||||
name?: string
|
||||
value?: string
|
||||
label?: string
|
||||
outlined?: boolean
|
||||
helper?: string
|
||||
icon?: string | MaterialIcons
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
validity?: ValidityState
|
||||
validityTransform?: (
|
||||
value: string,
|
||||
nativeValidity: ValidityState,
|
||||
) => Partial<ValidityState> | null
|
||||
validateOnInitialRender?: boolean
|
||||
validationMessage?: string
|
||||
// Custom
|
||||
type?: string
|
||||
}
|
||||
|
||||
export type TextFieldT = FieldT | {
|
||||
type?: TextFieldType,
|
||||
prefix?: string,
|
||||
suffix?: string,
|
||||
iconTrailing?: string,
|
||||
charCounter?: boolean,
|
||||
helperPersistent?: boolean | string,
|
||||
minLength?: number,
|
||||
maxLength?: number,
|
||||
pattern?: string,
|
||||
min?: number | string,
|
||||
max?: number | string,
|
||||
size?: number | null,
|
||||
step?: number | null,
|
||||
autoValidate?: boolean,
|
||||
willValidate?: boolean,
|
||||
name?: string,
|
||||
inputMode?: TextFieldInputMode,
|
||||
readOnly?: boolean,
|
||||
autocapitalize: 'on' | 'off' | 'sentences' | 'none' | 'words' | 'characters',
|
||||
endAligned?: boolean,
|
||||
};
|
||||
export type TextFieldT = FieldT & {
|
||||
type?: TextFieldType
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
iconTrailing?: string
|
||||
charCounter?: boolean
|
||||
helperPersistent?: boolean | string
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
pattern?: string
|
||||
min?: number | string
|
||||
max?: number | string
|
||||
size?: number | null
|
||||
step?: number | null
|
||||
autoValidate?: boolean
|
||||
willValidate?: boolean
|
||||
name?: string
|
||||
inputMode?: TextFieldInputMode
|
||||
readOnly?: boolean
|
||||
autocapitalize:
|
||||
| 'on'
|
||||
| 'off'
|
||||
| 'sentences'
|
||||
| 'none'
|
||||
| 'words'
|
||||
| 'characters'
|
||||
endAligned?: boolean
|
||||
elementType: 'text-field'
|
||||
};
|
||||
|
||||
export type TextAreaT = FieldT | {
|
||||
rows?: number,
|
||||
cols?: number,
|
||||
type?: TextFieldType,
|
||||
iconTrailing?: string,
|
||||
charCounter?: boolean | TextAreaCharCounter,
|
||||
willValidate?: boolean,
|
||||
helperPersistent?: boolean | string,
|
||||
maxLength?: number,
|
||||
export type TextAreaT = FieldT & {
|
||||
rows?: number
|
||||
cols?: number
|
||||
type?: TextFieldType
|
||||
iconTrailing?: string
|
||||
charCounter?: boolean | TextAreaCharCounter
|
||||
willValidate?: boolean
|
||||
helperPersistent?: boolean | string
|
||||
maxLength?: number
|
||||
elementType: 'text-area'
|
||||
};
|
||||
|
||||
export type SelectT = FieldT | {
|
||||
multiple?: boolean,
|
||||
naturalMenuWidth?: boolean,
|
||||
fixedMenuPosition?: boolean,willValidate?: boolean,
|
||||
elementType: 'material-select',
|
||||
selected?: ListItemBase | null,
|
||||
items?: ListItemBase[],
|
||||
index?: number,
|
||||
options?: {label: string, value: string}[]
|
||||
export type SelectT = FieldT & {
|
||||
multiple?: boolean
|
||||
naturalMenuWidth?: boolean
|
||||
fixedMenuPosition?: boolean
|
||||
willValidate?: boolean
|
||||
elementType: 'material-select'
|
||||
selected?: ListItemBase | null
|
||||
items?: ListItemBase[]
|
||||
index?: number
|
||||
options?: {
|
||||
label: string
|
||||
value: string
|
||||
}[]
|
||||
};
|
||||
|
||||
export type MaterialIcons =
|
||||
| '10k'
|
||||
| '10mp'
|
||||
@ -1870,4 +1891,4 @@ export type MaterialIcons =
|
||||
| 'youtube_searched_for'
|
||||
| 'zoom_in'
|
||||
| 'zoom_out'
|
||||
| 'zoom_out_map'
|
||||
| 'zoom_out_map';
|
@ -1,16 +1,16 @@
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
import '@material/mwc-snackbar';
|
||||
import 'mithril';
|
||||
|
||||
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';
|
||||
|
||||
type GenericObject = object & {prototype: any};
|
||||
/**
|
||||
* Check if class/object A is the same as or a subclass of class B.
|
||||
*/
|
||||
export function subclassOf(A: { ... }, B: { ... }): boolean {
|
||||
// noinspection JSUnresolvedVariable
|
||||
export function subclassOf(A: GenericObject, B: any): boolean {
|
||||
return A && (A === B || A.prototype instanceof B);
|
||||
}
|
||||
|
||||
@ -31,15 +31,23 @@ export function containsHTML(string_: string): boolean {
|
||||
* thrown. Defaults to 5000 (5 seconds).
|
||||
* @param {string} actionText Text of the action 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) {
|
||||
const snackbars = document.querySelectorAll('mwc-snackbar');
|
||||
|
||||
for (const snackbar of snackbars) {
|
||||
if (snackbar.open) {
|
||||
snackbar.close();
|
||||
}
|
||||
|
||||
snackbar.remove();
|
||||
}
|
||||
}
|
||||
@ -47,18 +55,21 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
||||
const snackbar = document.createElement('mwc-snackbar');
|
||||
snackbar.labelText = labelText;
|
||||
snackbar.timeoutMs = timeoutMs || -1;
|
||||
|
||||
if (actionText) {
|
||||
const button = document.createElement('mwc-button');
|
||||
button.label = actionText;
|
||||
button.slot = 'action';
|
||||
snackbar.append(button);
|
||||
}
|
||||
|
||||
if (cancelText) {
|
||||
const button = document.createElement('mwc-button');
|
||||
button.label = cancelText;
|
||||
button.slot = 'cancel';
|
||||
snackbar.append(button);
|
||||
}
|
||||
|
||||
document.body.append(snackbar);
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
@ -66,7 +77,7 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
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);
|
||||
});
|
||||
snackbar.show();
|
||||
@ -75,11 +86,9 @@ export async function showSnackbar(labelText: string, timeoutMs: number | false
|
||||
});
|
||||
return reasonPromise;
|
||||
}
|
||||
|
||||
export function getFormData(form: Cash): {...} {
|
||||
return Object.fromEntries(new FormData(form[0]));
|
||||
export function getFormData(form: Cash) {
|
||||
return Object.fromEntries<string | File>(new FormData(form[0] as HTMLFormElement));
|
||||
}
|
||||
|
||||
export function isFormValid(element: Cash | HTMLFormElement): boolean {
|
||||
let form = element;
|
||||
|
||||
@ -88,55 +97,77 @@ export function isFormValid(element: Cash | HTMLFormElement): boolean {
|
||||
}
|
||||
|
||||
let isValid: boolean = true;
|
||||
|
||||
form.find('text-field, text-area')
|
||||
.each((index: number, field: HTMLInputElement) => {
|
||||
if (!field.reportValidity()) {
|
||||
form
|
||||
.find('text-field, text-area')
|
||||
.each((index: number, field: HTMLElement & Partial<HTMLInputElement>) => {
|
||||
if (!(field as HTMLInputElement).reportValidity()) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
*
|
||||
* @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.
|
||||
* Se il parametro è "true" (valore booleano), verrà ritornato il valore come stringa
|
||||
* (stesso funzionamento del parametro dedicato (sotto ↓))
|
||||
* @param {boolean} returnAsString Se impostato a "true" vien ritornata una stringa invece di
|
||||
* un Vnode di Mithril
|
||||
*
|
||||
* @returns {Vnode}
|
||||
* @returns {string} Stringa se non contiene HTML, altrimenti Vnode
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function __(
|
||||
key: string | Vnode,
|
||||
replace: { [string]: string | Vnode | any } | boolean = {},
|
||||
key: string,
|
||||
replace: ReplaceObject | boolean = {},
|
||||
returnAsString: boolean = false
|
||||
): Vnode | string {
|
||||
): string {
|
||||
let translation = key;
|
||||
|
||||
// noinspection JSUnresolvedVariable
|
||||
if (window.translations && window.translations[key]) {
|
||||
translation = window.translations[key];
|
||||
if (translations && translations[key]) {
|
||||
translation = translations[key];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (const k of Object.keys(replace)) {
|
||||
// `'attrs' in replace[k]` checks if `replace[k]` is a Mithril Vnode
|
||||
translation = translation.replace(`:${k}`, ((typeof replace[k] === 'object' && 'attrs' in replace[k]) ? render(replace[k]) : replace[k]));
|
||||
const replacement = (replace as ReplaceObject)[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)) {
|
||||
return translation;
|
||||
}
|
||||
|
||||
return window.m.trust(translation);
|
||||
return translation;
|
||||
}
|
@ -31,7 +31,7 @@
|
||||
@include('layouts.top-app-bar-menus')
|
||||
|
||||
<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>
|
||||
|
||||
@routes
|
||||
|
@ -1,24 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": []
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"resources/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user