1
0
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:
Maicol Battistini 2022-01-06 15:45:35 +01:00
parent 8621a6aa88
commit 636c7ac68e
No known key found for this signature in database
GPG Key ID: 4FDB0F87CDB1D34A
71 changed files with 4930 additions and 3817 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
[ignore]
[include]
resources/js
resources/js/**
[libs]
[lints]
[options]
[strict]

View File

@ -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
View 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>

View File

@ -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
View File

@ -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
View File

@ -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>

View File

@ -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

View File

@ -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
View File

@ -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'
}
]
]
};
};

View File

@ -117,7 +117,7 @@ return [
| the development server starts.
*/
'commands' => [
'vite:aliases',
//'vite:aliases',
// 'typescript:generate'
],
];

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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();
});
}
}
}

View 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();
});
}
}
}

View File

@ -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 {}
}

View 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 {}
}

View File

@ -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]);
}
}
}

View 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']();
}
}
}

View File

@ -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]');

View File

@ -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>
&nbsp;
<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();
}
}

View 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>
&nbsp;
<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();
}
}

View File

@ -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>;
}
}

View 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>;
}
}

View File

@ -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"/>;
}
}

View 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" />;
}
}

View File

@ -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';

View 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';

View File

@ -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;

View 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;

View File

@ -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'}>

View File

@ -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'));
}

View 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;
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -1 +0,0 @@
export * from './RecordsPage.jsx';

View File

@ -0,0 +1 @@
export * from './RecordsPage';

View File

@ -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);
}

View File

@ -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';

View 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';

View File

@ -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();
}
}

View File

@ -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.'));
}
}

View 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.'));
}
}

View File

@ -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>
);
}
}

View 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>
);
}
}

View File

@ -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();
}
}

View File

@ -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.'));
}
}

View 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.')
);
}
}

View File

@ -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');
}
}

View 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');
}
}

View File

@ -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';

View 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';

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View 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;
}
`];
}

View File

@ -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
View 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
View File

@ -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
View 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
View 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;
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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

View File

@ -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/**/*"
]
}
}