Repository: rjaros/kvision-examples Branch: master Commit: c28cc78c0c12 Files: 538 Total size: 10.4 MB Directory structure: gitextract_l9t_x8j8/ ├── .gitignore ├── LICENSE ├── README.md ├── addressbook/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── EditPanel.kt │ │ │ │ ├── ListPanel.kt │ │ │ │ └── Model.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ └── messages-pl.po │ │ └── jsTest/ │ │ ├── kotlin/ │ │ │ └── test/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── AppSpec.kt │ │ └── resources/ │ │ └── css/ │ │ └── kvapp.css │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── addressbook-fullstack-ktor/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── logs/ │ │ └── ktor.log │ ├── settings.gradle.kts │ ├── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Model.kt │ │ │ └── Service.kt │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── EditPanel.kt │ │ │ │ ├── ListPanel.kt │ │ │ │ ├── MainPanel.kt │ │ │ │ ├── Model.kt │ │ │ │ └── Security.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jvmMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Dao.kt │ │ │ ├── Db.kt │ │ │ ├── Main.kt │ │ │ └── Service.kt │ │ └── resources/ │ │ ├── application.conf │ │ └── logback.xml │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── proxy.js │ ├── tailwind.js │ └── webpack.js ├── addressbook-fullstack-spring-boot/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── application/ │ │ └── build.gradle.kts │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Model.kt │ │ │ └── Service.kt │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── EditPanel.kt │ │ │ │ ├── ListPanel.kt │ │ │ │ ├── MainPanel.kt │ │ │ │ ├── Model.kt │ │ │ │ └── Security.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jvmMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Main.kt │ │ │ ├── Security.kt │ │ │ └── Service.kt │ │ └── resources/ │ │ ├── application.yml │ │ ├── logback.xml │ │ └── schema.sql │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── proxy.js │ ├── tailwind.js │ └── webpack.js ├── addressbook-fullstack-spring-boot-oauth/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── application/ │ │ └── build.gradle.kts │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Model.kt │ │ │ └── Service.kt │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── EditPanel.kt │ │ │ │ ├── ListPanel.kt │ │ │ │ ├── MainPanel.kt │ │ │ │ ├── Model.kt │ │ │ │ └── Security.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jvmMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Main.kt │ │ │ ├── Security.kt │ │ │ └── Service.kt │ │ └── resources/ │ │ ├── application.yml │ │ ├── logback.xml │ │ └── schema.sql │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── proxy.js │ ├── tailwind.js │ └── webpack.js ├── addressbook-tabulator/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── EditPanel.kt │ │ │ │ ├── ListPanel.kt │ │ │ │ └── Model.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ └── messages-pl.po │ │ └── jsTest/ │ │ ├── kotlin/ │ │ │ └── test/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── AppSpec.kt │ │ └── resources/ │ │ └── css/ │ │ └── kvapp.css │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── desktop/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── Calculator.kt │ │ │ │ ├── DesktopIcon.kt │ │ │ │ ├── DesktopWindow.kt │ │ │ │ ├── Paint.kt │ │ │ │ ├── TextEditor.kt │ │ │ │ └── WebBrowser.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ └── css/ │ │ │ └── kvapp.css │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── docs/ │ ├── addressbook/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ └── main.bundle.js.LICENSE.txt │ ├── addressbook-tabulator/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ └── main.bundle.js.LICENSE.txt │ ├── desktop/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ └── main.bundle.js.LICENSE.txt │ ├── fomantic/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ └── main.bundle.js.LICENSE.txt │ ├── helloworld/ │ │ ├── index.html │ │ └── main.bundle.js │ ├── patternfly/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ └── main.bundle.js.LICENSE.txt │ ├── pokedex/ │ │ ├── index.html │ │ ├── main.bundle.js │ │ ├── main.bundle.js.LICENSE.txt │ │ ├── manifest.json │ │ ├── service-worker.js │ │ └── workbox-a7df7adf.js │ ├── showcase/ │ │ ├── index.html │ │ ├── showcase.js │ │ └── showcase.js.LICENSE.txt │ ├── template/ │ │ ├── index.html │ │ ├── template.js │ │ └── template.js.LICENSE.txt │ ├── template-tailwindcss/ │ │ ├── index.html │ │ └── template-tailwindcss.js │ └── todomvc/ │ ├── index.html │ ├── main.bundle.js │ └── package.json ├── encoder-fullstack-ktor/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── Service.kt │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── App.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jvmMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── Main.kt │ │ │ └── Service.kt │ │ └── resources/ │ │ ├── application.conf │ │ └── logback.xml │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── proxy.js │ ├── tailwind.js │ └── webpack.js ├── fomantic/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── CardView.kt │ │ │ │ ├── Components.kt │ │ │ │ ├── State.kt │ │ │ │ ├── Toolbar.kt │ │ │ │ └── User.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── jquery.js │ ├── tailwind.js │ └── webpack.js ├── helloworld/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── Helloworld.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── helloworld.css │ │ │ └── i18n/ │ │ │ ├── messages-de.po │ │ │ ├── messages-en.po │ │ │ ├── messages-es.po │ │ │ ├── messages-fr.po │ │ │ ├── messages-ja.po │ │ │ ├── messages-ko.po │ │ │ ├── messages-pl.po │ │ │ ├── messages-ru.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── HelloworldSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── mini-template/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── src/ │ │ └── jsMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── App.kt │ │ └── resources/ │ │ └── index.html │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── tailwind.js │ └── webpack.js ├── patternfly/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── CardView.kt │ │ │ │ ├── Components.kt │ │ │ │ ├── ListView.kt │ │ │ │ ├── Model.kt │ │ │ │ ├── Redux.kt │ │ │ │ ├── TableView.kt │ │ │ │ ├── Toolbar.kt │ │ │ │ └── User.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── pokedex/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── App.kt │ │ │ │ ├── Model.kt │ │ │ │ └── PokeBox.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ ├── manifest.json │ │ │ └── modules/ │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── pwa.js │ ├── tailwind.js │ └── webpack.js ├── showcase/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── BasicTab.kt │ │ │ │ ├── ButtonsTab.kt │ │ │ │ ├── ChartTab.kt │ │ │ │ ├── ContainersTab.kt │ │ │ │ ├── DataTab.kt │ │ │ │ ├── DragDropTab.kt │ │ │ │ ├── DropDownTab.kt │ │ │ │ ├── FormTab.kt │ │ │ │ ├── LayoutsTab.kt │ │ │ │ ├── ModalsTab.kt │ │ │ │ ├── RestTab.kt │ │ │ │ ├── Showcase.kt │ │ │ │ └── TabulatorTab.kt │ │ │ ├── ktml/ │ │ │ │ ├── rest.en.ktml │ │ │ │ ├── rest.pl.ktml │ │ │ │ ├── template1.en.ktml │ │ │ │ └── template1.pl.ktml │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── showcase.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── ShowcaseSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── template/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── App.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── modules/ │ │ │ ├── css/ │ │ │ │ └── kvapp.css │ │ │ └── i18n/ │ │ │ ├── messages-en.po │ │ │ ├── messages-pl.po │ │ │ └── messages.pot │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── template-tailwindcss/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── App.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ ├── modules/ │ │ │ │ ├── css/ │ │ │ │ │ └── kvapp.css │ │ │ │ └── i18n/ │ │ │ │ ├── messages-en.po │ │ │ │ ├── messages-pl.po │ │ │ │ └── messages.pot │ │ │ └── tailwind/ │ │ │ ├── tailwind.config.js │ │ │ └── tailwind.css │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── todomvc/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── Todomvc.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── package.json │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js ├── todomvc-ballast/ │ ├── .gettext.json │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ ├── src/ │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── TodoContract.kt │ │ │ │ ├── TodoEventHandler.kt │ │ │ │ ├── TodoInputHandler.kt │ │ │ │ ├── TodoModel.kt │ │ │ │ ├── TodoModule.kt │ │ │ │ ├── TodoSavedStateAdapter.kt │ │ │ │ ├── TodoViewModel.kt │ │ │ │ └── Todomvc.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── package.json │ │ └── jsTest/ │ │ └── kotlin/ │ │ └── test/ │ │ └── com/ │ │ └── example/ │ │ └── AppSpec.kt │ └── webpack.config.d/ │ ├── bootstrap.js │ ├── css.js │ ├── file.js │ ├── handlebars.js │ ├── tailwind.js │ └── webpack.js └── todomvc-signal/ ├── .gettext.json ├── .gitignore ├── README.md ├── build.gradle.kts ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── src/ │ ├── jsMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── Todomvc.kt │ │ └── resources/ │ │ ├── index.html │ │ └── package.json │ └── jsTest/ │ └── kotlin/ │ └── test/ │ └── com/ │ └── example/ │ └── AppSpec.kt └── webpack.config.d/ ├── bootstrap.js ├── css.js ├── file.js ├── handlebars.js ├── tailwind.js └── webpack.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea heroku-addressbook.sh heroku-numbers.sh heroku-tweets.sh heroku-encoder.sh build.sh build-fullstack.sh *.imp *.ipr *.iws /.idea/ copytodocs.sh /encoder-fullstack-ktor/logs/ktor.log ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2017-present Robert Jaros Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # KVision examples A set of examples for [KVision](https://github.com/rjaros/kvision) framework. ## Mini template A minimal KVision application with simplified build configuration. A perfect project for a quick start. ## Template An application template. It includes all dependencies to develop KVision applications with all supported components (including unit tests). A perfect starting point for a new application. [See live demo](https://rjaros.github.io/kvision-examples/template/) ## Hello World A very simple application with internationalization. [See live demo](https://rjaros.github.io/kvision-examples/helloworld/) ## Showcase A simple application presenting all main features of KVision framework. [See live demo](https://rjaros.github.io/kvision-examples/showcase/) ## Address book An address book application presenting a classic [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) project with Material-like CSS template from [Bootswatch](https://bootswatch.com/materia/). [See live demo](https://rjaros.github.io/kvision-examples/addressbook/) ## Address book with Tabulator An address book application rewritten with a [Tabulator](http://tabulator.info) module. [See live demo](https://rjaros.github.io/kvision-examples/addressbook-tabulator/) ## Desktop A very simple desktop with four mini applications - a calculator, a text editor, a paint program and a web browser. [See live demo](https://rjaros.github.io/kvision-examples/desktop/) ## Pokedex PWA The list of Pokémon with live search, build with Redux module. It's also a fully compatible [PWA](https://developers.google.com/web/progressive-web-apps/). [See live demo](https://kvision-pokedex.netlify.app/) ## TodoMVC A complete implementation of [TodoMVC](https://todomvc.com/) demo application. [See live demo](https://rjaros.github.io/kvision-examples/todomvc/) ## TodoMVC-Ballast A complete implementation of [TodoMVC](https://todomvc.com/) demo application developed with [Ballast](https://github.com/copper-leaf/ballast) application state management framework. ## TodoMVC-Signal A complete implementation of [TodoMVC](https://todomvc.com/) demo application developed with [Signal](https://github.com/Fenrur/Signal) reactive state library. ## TailwindCSS demo A demo application created with [TailwindCSS](https://tailwindcss.com/). It shows how to create KVision application with a modern design system alternative to Bootstrap. [See live demo](https://rjaros.github.io/kvision-examples/template-tailwindcss/) ## Patternfly demo A demo application created with [Patternfly UI toolkit](https://www.patternfly.org/). It shows how to create KVision application with a modern design system alternative to Bootstrap. The application is heavily inspired by [Patternfly Kotlin project](https://patternfly-kotlin.github.io/patternfly-fritz2-showcase/#user-demo). [See live demo](https://rjaros.github.io/kvision-examples/patternfly/) ## Fomantic-UI demo A demo application created with [Fomantic-UI toolkit](https://fomantic-ui.com/). It shows how to create KVision application with a modern design system alternative to Bootstrap. It also presents state management based on Kotlin flows (using both StateFlow and SharedFlow). [See live demo](https://rjaros.github.io/kvision-examples/fomantic/) ## Address book - fullstack A complete, fullstack address book application. ## Encoder - fullstack A simple application to encode the given text, based on the overview chapter from the [KVision guide](https://kvision.gitbook.io/kvision-guide/part-3-server-side-interface/overview). ================================================ FILE: addressbook/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: addressbook/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: addressbook/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Running * run - Starts a webpack dev server on port 3000. ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * zip - Packages a zip archive with all required files into `build/libs/*.zip` ================================================ FILE: addressbook/build.gradle.kts ================================================ plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" repositories { mavenCentral() mavenLocal() } // Versions val kvisionVersion: String by System.getProperties() kotlin { js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets["jsMain"].dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-i18n:$kvisionVersion") implementation("io.kvision:kvision-state:$kvisionVersion") } sourceSets["jsTest"].dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } ================================================ FILE: addressbook/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: addressbook/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 #Dependencies systemProp.kvisionVersion=9.5.0 org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: addressbook/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: addressbook/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: addressbook/settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } rootProject.name = "addressbook" ================================================ FILE: addressbook/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.i18n.DefaultI18nManager import io.kvision.i18n.I18n import io.kvision.panel.root import io.kvision.panel.splitPanel import io.kvision.startApplication import io.kvision.utils.perc import io.kvision.utils.useModule import io.kvision.utils.vh @JsModule("./modules/css/kvapp.css") external object kvappCss @JsModule("./modules/i18n/messages-en.json") external val messagesEn: dynamic @JsModule("./modules/i18n/messages-pl.json") external val messagesPl: dynamic class App : Application() { init { useModule(kvappCss) } override fun start() { I18n.manager = DefaultI18nManager( mapOf( "en" to messagesEn, "pl" to messagesPl ) ) root("kvapp") { splitPanel { height = 100.vh width = 100.perc listPanel() editPanel() } } Model.loadAddresses() } } fun main() { startApplication( ::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, FontAwesomeModule, CoreModule ) } ================================================ FILE: addressbook/src/jsMain/kotlin/com/example/EditPanel.kt ================================================ package com.example import io.kvision.core.Container import io.kvision.core.onEvent import io.kvision.form.check.CheckBox import io.kvision.form.formPanel import io.kvision.form.text.Text import io.kvision.html.ButtonStyle import io.kvision.html.InputType import io.kvision.html.button import io.kvision.i18n.I18n.tr import io.kvision.panel.hPanel import io.kvision.panel.simplePanel import io.kvision.state.bind import io.kvision.utils.ENTER_KEY import io.kvision.utils.px import kotlinx.browser.window fun Container.editPanel() { simplePanel().bind(Model.addressBook) { state -> padding = 10.px if (state.editMode != null) { val formPanel = formPanel
{ add(Address::firstName, Text(label = tr("First name:"))) add(Address::lastName, Text(label = tr("Last name:"))) add(Address::email, Text(InputType.EMAIL, label = tr("E-mail:"))) add(Address::favourite, CheckBox(label = tr("Mark as favourite"))) hPanel(spacing = 10) { button(tr("Save"), "fas fa-check", ButtonStyle.PRIMARY).onClick { Model.save(this@formPanel.getData()) } button(tr("Cancel"), "fas fa-times", ButtonStyle.SECONDARY).onClick { Model.cancel() } } onEvent { keydown = { e -> if (e.keyCode == ENTER_KEY) { Model.save(this@formPanel.getData()) } } } } if (state.editMode == EditMode.NEW) { formPanel.clearData() } else if (state.editAddress != null) { formPanel.setData(state.editAddress) } window.setTimeout({ formPanel.getControl(Address::firstName)?.focus() }, 0) } else { simplePanel { button(tr("Add new address"), "fas fa-plus", style = ButtonStyle.PRIMARY).onClick { Model.add() } } } } } ================================================ FILE: addressbook/src/jsMain/kotlin/com/example/ListPanel.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.Container import io.kvision.core.FontStyle import io.kvision.core.onClick import io.kvision.core.onEvent import io.kvision.form.check.radioGroup import io.kvision.form.text.text import io.kvision.html.InputType import io.kvision.html.div import io.kvision.html.icon import io.kvision.html.link import io.kvision.i18n.I18n.tr import io.kvision.modal.Confirm import io.kvision.panel.hPanel import io.kvision.panel.simplePanel import io.kvision.state.bind import io.kvision.table.TableType import io.kvision.table.cell import io.kvision.table.headerCell import io.kvision.table.row import io.kvision.table.table import io.kvision.utils.px fun Container.listPanel() { simplePanel { padding = 5.px hPanel(alignItems = AlignItems.CENTER, spacing = 20) { text(InputType.SEARCH) { placeholder = tr("Search ...") onEvent { input = { Model.setSearch(self.value) } } } radioGroup( listOf(Filter.ALL.name to tr("All"), Filter.FAVOURITE.name to tr("Favourites")), Filter.ALL.name, inline = true ).onEvent { change = { Model.setFilter(Filter.valueOf(self.value!!)) } } } div().bind(Model.addressBook) { state -> table(types = setOf(TableType.STRIPED, TableType.HOVER)) { headerCell(tr("First name")).onClick { Model.setSort(Sort.FN) } headerCell(tr("Last name")).onClick { Model.setSort(Sort.LN) } headerCell(tr("E-mail")).onClick { Model.setSort(Sort.E) } headerCell("").onClick { Model.setSort(Sort.F) } headerCell("") state.addresses.mapIndexed { index, address -> index to address } .filter { it.second.match(state.search) && (state.filter == Filter.ALL || it.second.favourite ?: false) }.sortedBy { when (state.sort) { Sort.FN -> it.second.firstName?.lowercase() Sort.LN -> it.second.lastName?.lowercase() Sort.E -> it.second.email?.lowercase() Sort.F -> it.second.favourite.toString() } }.forEach { (index, address) -> row { cell(address.firstName) cell(address.lastName) cell { address.email?.let { link(it, "mailto:$it") { fontStyle = FontStyle.ITALIC } } } cell { address.favourite?.let { if (it) icon("far fa-heart") { title = tr("Favourite") } } } cell { icon("fas fa-times") { title = tr("Delete") onEvent { click = { e -> e.stopPropagation() Confirm.show( tr("Are you sure?"), tr("Do you want to delete this address?") ) { Model.delete(index) } } } } } onEvent { click = { Model.edit(index) } } } } } } } } ================================================ FILE: addressbook/src/jsMain/kotlin/com/example/Model.kt ================================================ package com.example import kotlinx.browser.localStorage import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.w3c.dom.get import org.w3c.dom.set import io.kvision.state.ObservableValue @Serializable data class Address( val firstName: String? = null, val lastName: String? = null, val email: String? = null, val favourite: Boolean? = false ) fun Address.match(search: String?): Boolean { return search?.let { firstName?.contains(it, true) ?: false || lastName?.contains(it, true) ?: false || email?.contains(it, true) ?: false } ?: true } enum class Sort { FN, LN, E, F } enum class Filter { ALL, FAVOURITE } enum class EditMode { NEW, EDIT } data class AddressBookState( val addresses: List
, val search: String? = null, val sort: Sort = Sort.FN, val filter: Filter = Filter.ALL, val editMode: EditMode? = null, val editIndex: Int? = null, val editAddress: Address? = null ) object Model { val addressBook = ObservableValue( AddressBookState( listOf( Address("John", "Smith", "john.smith@mail.com", true), Address("Karen", "Kowalsky", "kkowalsky@mail.com", true), Address("William", "Gordon", "w.gordon@mail.com", false) ) ) ) fun setSort(sort: Sort) { addressBook.value = addressBook.value.copy(sort = sort) } fun setSearch(search: String?) { addressBook.value = addressBook.value.copy(search = search) } fun setFilter(filter: Filter) { addressBook.value = addressBook.value.copy(filter = filter) } fun add() { addressBook.value = addressBook.value.copy(editMode = EditMode.NEW, editIndex = null, editAddress = null) } fun edit(index: Int?) { val state = addressBook.value val editAddress = index?.let { state.addresses[it] } addressBook.value = state.copy(editMode = EditMode.EDIT, editIndex = index, editAddress = editAddress) } fun cancel() { addressBook.value = addressBook.value.copy(editMode = null, editIndex = null, editAddress = null) } fun delete(index: Int) { val state = addressBook.value val newAddresses = state.addresses.filterIndexed { i, _ -> i != index } val editIndex = state.editIndex ?: -1 addressBook.value = if (editIndex == index) { state.copy(editMode = null, addresses = newAddresses, editIndex = null, editAddress = null) } else if (editIndex > index) { state.copy(addresses = newAddresses, editIndex = editIndex - 1) } else { state.copy(addresses = newAddresses) } storeAddresses() } fun save(newAddress: Address) { val state = addressBook.value val newAddresses = if (state.editMode == EditMode.EDIT) { state.addresses.mapIndexed { i, address -> if (i == state.editIndex) newAddress else address } } else { state.addresses + newAddress } addressBook.value = state.copy(addresses = newAddresses, editMode = null, editIndex = null, editAddress = null) storeAddresses() } fun storeAddresses() { val jsonString = Json.encodeToString(addressBook.value.addresses) localStorage["addresses"] = jsonString } fun loadAddresses() { localStorage["addresses"]?.let { addressesAsString -> addressBook.value = addressBook.value.copy(addresses = Json.decodeFromString(addressesAsString)) } } } ================================================ FILE: addressbook/src/jsMain/resources/index.html ================================================ KVision Address Book
================================================ FILE: addressbook/src/jsMain/resources/modules/css/kvapp.css ================================================ ================================================ FILE: addressbook/src/jsMain/resources/modules/i18n/messages-en.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: ../src/main/kotlin/com/example/App.kt:20 msgid "This is a localized message." msgstr "" ================================================ FILE: addressbook/src/jsMain/resources/modules/i18n/messages-pl.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: ../src/main/kotlin/com/example/App.kt:20 msgid "This is a localized message." msgstr "To jest przetłumaczona wiadomość." ================================================ FILE: addressbook/src/jsTest/kotlin/test/com/example/AppSpec.kt ================================================ package test.com.example import io.kvision.test.DomSpec import kotlin.test.Test import kotlin.test.assertTrue class AppSpec : DomSpec { @Test fun render() { run { assertTrue(true, "Dummy test") } } } ================================================ FILE: addressbook/src/jsTest/resources/css/kvapp.css ================================================ ================================================ FILE: addressbook/webpack.config.d/bootstrap.js ================================================ config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource'}); ================================================ FILE: addressbook/webpack.config.d/css.js ================================================ config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); ================================================ FILE: addressbook/webpack.config.d/file.js ================================================ config.module.rules.push( { test: /\.(jpe?g|png|gif|svg)$/i, type: 'asset/resource' } ); ================================================ FILE: addressbook/webpack.config.d/handlebars.js ================================================ config.module.rules.push( { test: /\.hbs$/i, loader: 'handlebars-loader' } ); ================================================ FILE: addressbook/webpack.config.d/tailwind.js ================================================ ;(function() { config.module.rules.push({ test: /tailwind\.css$/, use: [ '@tailwindcss/webpack' ] }); })(); ================================================ FILE: addressbook/webpack.config.d/webpack.js ================================================ config.resolve.modules.push("kotlin"); if (config.devServer) { config.devServer.client = { overlay: false }; config.devServer.hot = true; config.devServer.open = false; config.devServer.port = 3000; config.devServer.historyApiFallback = true; config.devtool = 'eval-cheap-source-map'; } else { config.devtool = undefined; } // disable bundle size warning config.performance = { assetFilter: function (assetFilename) { return !assetFilename.endsWith('.js'); }, }; ================================================ FILE: addressbook-fullstack-ktor/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: addressbook-fullstack-ktor/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: addressbook-fullstack-ktor/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Compiling * compileKotlinJs - Compiles frontend sources. * compileKotlinJvm - Compiles backend sources. ### Running * jsBrowserDevelopmentRun - Starts a webpack dev server on port 3000 * jvmRun - Starts a dev server on port 8080 ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * jsJar - Packages a standalone "web" frontend jar with all required files into `build/libs/*.jar` * jvmJar - Packages a backend jar with compiled source files into `build/libs/*.jar` * jarWithJs - Packages a "fat" jar with all backend sources and dependencies while also embedding frontend resources into `build/libs/*.jar` ================================================ FILE: addressbook-fullstack-ktor/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion val kspVersion: String by System.getProperties() id("com.google.devtools.ksp") version kspVersion val kiluaRpcVersion: String by System.getProperties() id("dev.kilua.rpc") version kiluaRpcVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" repositories { mavenCentral() mavenLocal() } // Versions val kvisionVersion: String by System.getProperties() val kiluaRpcVersion: String by System.getProperties() val ktorVersion: String by project val exposedVersion: String by project val hikariVersion: String by project val h2Version: String by project val pgsqlVersion: String by project val kweryVersion: String by project val logbackVersion: String by project val commonsCodecVersion: String by project val jdbcNamedParametersVersion: String by project val mainClassName = "io.ktor.server.netty.EngineMain" kotlin { jvmToolchain(21) jvm { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs = listOf("-Xjsr305=strict") } @OptIn(ExperimentalKotlinGradlePluginApi::class) mainRun { mainClass.set(mainClassName) } } js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets { val commonMain by getting { dependencies { implementation("dev.kilua:kilua-rpc-ktor:$kiluaRpcVersion") implementation("io.kvision:kvision-common-remote:$kvisionVersion") } } val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } val jvmMain by getting { dependencies { implementation(kotlin("reflect")) implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-auth:$ktorVersion") implementation("io.ktor:ktor-server-compression:$ktorVersion") implementation("io.ktor:ktor-server-default-headers:$ktorVersion") implementation("io.ktor:ktor-server-compression:$ktorVersion") implementation("io.ktor:ktor-server-call-logging:$ktorVersion") implementation("ch.qos.logback:logback-classic:$logbackVersion") implementation("com.h2database:h2:$h2Version") implementation("org.jetbrains.exposed:exposed:$exposedVersion") implementation("org.postgresql:postgresql:$pgsqlVersion") implementation("com.zaxxer:HikariCP:$hikariVersion") implementation("commons-codec:commons-codec:$commonsCodecVersion") implementation("com.axiomalaska:jdbc-named-parameters:$jdbcNamedParametersVersion") implementation("com.github.andrewoma.kwery:core:$kweryVersion") } } val jvmTest by getting { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit")) } } val jsMain by getting { dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-state:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-i18n:$kvisionVersion") implementation("io.kvision:kvision-rest:$kvisionVersion") } } val jsTest by getting { dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } } } ================================================ FILE: addressbook-fullstack-ktor/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: addressbook-fullstack-ktor/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 systemProp.kspVersion=2.3.6 systemProp.kiluaRpcVersion=0.0.43 #Dependencies systemProp.kvisionVersion=9.5.0 ktorVersion=3.4.1 hikariVersion=3.2.0 commonsCodecVersion=1.10 jdbcNamedParametersVersion=1.1 exposedVersion=0.17.14 logbackVersion=1.5.32 h2Version=1.4.197 pgsqlVersion=42.2.2 kweryVersion=0.17 org.gradle.jvmargs=-Xmx2g org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: addressbook-fullstack-ktor/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: addressbook-fullstack-ktor/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: addressbook-fullstack-ktor/logs/ktor.log ================================================ [2023-08-13 15:32:37,087]-[main] INFO Application - Autoreload is disabled because the development mode is off. [2023-08-13 15:32:37,314]-[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... [2023-08-13 15:32:37,415]-[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. [2023-08-13 15:32:37,771]-[main] INFO Application - Application started in 0.712 seconds. [2023-08-13 15:32:37,771]-[main] INFO Application - Application started: io.ktor.server.application.Application@cb39552 [2023-08-13 15:32:37,885]-[DefaultDispatcher-worker-1] INFO Application - Responding at http://0.0.0.0:8080 [2023-08-13 15:32:46,755]-[eventLoopGroupProxy-4-1] INFO Application - 401 Unauthorized: POST - /kv/routeAddressServiceManager0 in 67ms [2023-08-13 15:32:59,406]-[eventLoopGroupProxy-4-2] INFO Application - 401 Unauthorized: POST - /login in 16ms [2023-08-13 15:33:11,945]-[eventLoopGroupProxy-4-3] INFO Application - 200 OK: POST - /kv/routeRegisterProfileServiceManager0 in 67ms [2023-08-13 15:33:21,753]-[eventLoopGroupProxy-4-4] INFO Application - 200 OK: POST - /login in 22ms [2023-08-13 15:33:21,783]-[eventLoopGroupProxy-4-5] INFO Application - 200 OK: POST - /kv/routeProfileServiceManager0 in 14ms [2023-08-13 15:33:21,814]-[eventLoopGroupProxy-4-6] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager0 in 14ms [2023-08-13 15:33:25,667]-[eventLoopGroupProxy-4-7] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager1 in 58ms [2023-08-13 15:33:25,684]-[eventLoopGroupProxy-4-8] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager0 in 5ms [2023-08-13 15:33:26,886]-[eventLoopGroupProxy-4-9] INFO Application - 302 Found: GET - /logout in 4ms -> / [2023-08-13 15:33:27,631]-[eventLoopGroupProxy-4-10] INFO Application - 401 Unauthorized: POST - /kv/routeAddressServiceManager0 in 3ms [2023-08-13 15:33:30,624]-[eventLoopGroupProxy-4-11] INFO Application - 200 OK: POST - /login in 5ms [2023-08-13 15:33:30,658]-[eventLoopGroupProxy-4-12] INFO Application - 200 OK: POST - /kv/routeProfileServiceManager0 in 3ms [2023-08-13 15:33:30,679]-[eventLoopGroupProxy-4-1] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager0 in 4ms [2023-08-13 15:33:34,071]-[eventLoopGroupProxy-4-2] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager2 in 15ms [2023-08-13 15:33:34,087]-[eventLoopGroupProxy-4-3] INFO Application - 200 OK: POST - /kv/routeAddressServiceManager0 in 4ms [2023-08-13 15:33:35,067]-[eventLoopGroupProxy-4-4] INFO Application - 302 Found: GET - /logout in 3ms -> / [2023-08-13 15:33:35,831]-[eventLoopGroupProxy-4-5] INFO Application - 401 Unauthorized: POST - /kv/routeAddressServiceManager0 in 3ms [2023-08-13 15:33:43,455]-[KtorShutdownHook] INFO Application - Application stopping: io.ktor.server.application.Application@cb39552 [2023-08-13 15:33:43,457]-[KtorShutdownHook] INFO Application - Application stopped: io.ktor.server.application.Application@cb39552 ================================================ FILE: addressbook-fullstack-ktor/settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } rootProject.name = "addressbook-fullstack-ktor" ================================================ FILE: addressbook-fullstack-ktor/src/commonMain/kotlin/com/example/Model.kt ================================================ @file:UseContextualSerialization(LocalDateTime::class) package com.example import kotlinx.serialization.Serializable import kotlinx.serialization.UseContextualSerialization import io.kvision.types.LocalDateTime @Serializable data class Profile( val id: Int? = null, val name: String? = null, val username: String? = null, val password: String? = null, val password2: String? = null ) @Serializable data class Address( val id: Int? = 0, val firstName: String? = null, val lastName: String? = null, val email: String? = null, val phone: String? = null, val postalAddress: String? = null, val favourite: Boolean? = false, val createdAt: LocalDateTime? = null, val userId: Int? = null ) ================================================ FILE: addressbook-fullstack-ktor/src/commonMain/kotlin/com/example/Service.kt ================================================ package com.example import dev.kilua.rpc.annotations.RpcService import kotlinx.serialization.Serializable @Serializable enum class Sort { FN, LN, E, F } @RpcService interface IAddressService { suspend fun getAddressList(search: String?, types: String, sort: Sort): List
suspend fun addAddress(address: Address): Address suspend fun updateAddress(address: Address): Address suspend fun deleteAddress(id: Int): Boolean } @RpcService interface IProfileService { suspend fun getProfile(): Profile } @RpcService interface IRegisterProfileService { suspend fun registerProfile(profile: Profile, password: String): Boolean } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.i18n.DefaultI18nManager import io.kvision.i18n.I18n import io.kvision.panel.root import io.kvision.panel.splitPanel import io.kvision.remote.registerRemoteTypes import io.kvision.startApplication import io.kvision.utils.perc import io.kvision.utils.useModule import io.kvision.utils.vh import kotlinx.browser.window import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch val AppScope = CoroutineScope(window.asCoroutineDispatcher()) @JsModule("./modules/css/kvapp.css") external object kvappCss @JsModule("./modules/i18n/messages-en.json") external val messagesEn: dynamic @JsModule("./modules/i18n/messages-pl.json") external val messagesPl: dynamic class App : Application() { init { useModule(kvappCss) } override fun start() { I18n.manager = DefaultI18nManager( mapOf( "en" to messagesEn, "pl" to messagesPl ) ) root("kvapp") { splitPanel { width = 100.perc height = 100.vh add(ListPanel) add(EditPanel) } } AppScope.launch { Model.getAddressList() } } } fun main() { registerRemoteTypes() startApplication(::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, FontAwesomeModule, CoreModule) } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/EditPanel.kt ================================================ package com.example import io.kvision.core.onEvent import io.kvision.form.FormPanel import io.kvision.form.check.CheckBox import io.kvision.form.formPanel import io.kvision.form.text.Text import io.kvision.html.ButtonStyle import io.kvision.html.InputType import io.kvision.html.button import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.panel.StackPanel import io.kvision.utils.ENTER_KEY import io.kvision.utils.px import kotlinx.coroutines.launch object EditPanel : StackPanel() { private var editingId: Int? = null private val formPanel: FormPanel
init { padding = 10.px formPanel = formPanel { add(Address::firstName, Text(label = "${tr("First name")}:").apply { maxlength = 255 }) add(Address::lastName, Text(label = "${tr("Last name")}:").apply { maxlength = 255 }) add(Address::email, Text(InputType.EMAIL, label = "${tr("E-mail")}:").apply { maxlength = 255 }) { it.getValue() ?.let { "(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])".toRegex() .matches(it) } } add(Address::phone, Text(label = "${tr("Phone number")}:").apply { maxlength = 255 }) add(Address::postalAddress, Text(label = "${tr("Postal address")}:").apply { maxlength = 255 }) add(Address::favourite, CheckBox(label = tr("Mark as favourite"))) add(HPanel(spacing = 10) { button(tr("Save"), "fas fa-check", ButtonStyle.PRIMARY).onClick { this@EditPanel.save() } button(tr("Cancel"), "fas fa-times", ButtonStyle.SECONDARY).onClick { this@EditPanel.close() } }) onEvent { keydown = { if (it.keyCode == ENTER_KEY) { this@EditPanel.save() } } } } add(MainPanel) } fun add() { formPanel.clearData() open(null) } fun edit(index: Int) { val address = Model.addresses[index] formPanel.setData(address) open(address.id) } private fun save() { AppScope.launch { if (formPanel.validate()) { val address = formPanel.getData() if (editingId != null) { Model.updateAddress(address.copy(id = editingId)) } else { Model.addAddress(address) } close() } } } fun delete(index: Int) { AppScope.launch { close() Model.addresses[index].id?.let { Model.deleteAddress(it) } } } private fun open(editingId: Int?) { this.editingId = editingId activeChild = formPanel formPanel.validate() formPanel.getControl(Address::firstName)?.focus() } private fun close() { editingId = null activeChild = MainPanel } } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/ListPanel.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.FontStyle import io.kvision.core.onEvent import io.kvision.form.check.RadioGroup import io.kvision.form.check.radioGroup import io.kvision.form.text.TextInput import io.kvision.form.text.text import io.kvision.html.InputType import io.kvision.html.icon import io.kvision.html.link import io.kvision.i18n.I18n.tr import io.kvision.modal.Confirm import io.kvision.panel.SimplePanel import io.kvision.panel.hPanel import io.kvision.state.bind import io.kvision.table.HeaderCell import io.kvision.table.TableType import io.kvision.table.cell import io.kvision.table.row import io.kvision.table.table import io.kvision.utils.px object ListPanel : SimplePanel() { init { padding = 5.px hPanel(alignItems = AlignItems.CENTER, spacing = 20) { text(InputType.SEARCH) { placeholder = "${tr("Search")} ..." setEventListener { input = { Model.search = self.value } } } radioGroup(listOf("all" to tr("All"), "fav" to tr("Favourites")), "all", inline = true) { marginBottom = 0.px setEventListener { change = { Model.types = self.value ?: "all" } } } } table(types = setOf(TableType.STRIPED, TableType.HOVER)) { addHeaderCell(this@ListPanel.sortingHeaderCell(tr("First name"), Sort.FN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("Last name"), Sort.LN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("E-mail"), Sort.E)) addHeaderCell(this@ListPanel.sortingHeaderCell("", Sort.F)) addHeaderCell(HeaderCell("")) bind(Model.addresses) { addresses -> addresses.forEachIndexed { index, address -> row { cell(address.firstName) cell(address.lastName) cell { address.email?.let { link(it, "mailto:$it") { fontStyle = FontStyle.ITALIC } } } cell { address.favourite?.let { if (it) icon("far fa-heart") { title = tr("Favourite") } } } cell { icon("fas fa-times") { title = tr("Delete") onEvent { click = { e -> e.stopPropagation() Confirm.show("Are you sure?", "Do you want to delete this address?") { EditPanel.delete(index) } } } } } onEvent { click = { EditPanel.edit(index) } } } } } } } private fun sortingHeaderCell(title: String, sort: Sort) = HeaderCell(title) { onEvent { click = { Model.sort = sort } } } } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/MainPanel.kt ================================================ package com.example import io.kvision.core.JustifyContent import io.kvision.html.ButtonStyle import io.kvision.html.button import io.kvision.html.div import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.state.bind import kotlinx.browser.document object MainPanel : HPanel(justify = JustifyContent.SPACEBETWEEN) { init { button(tr("Add new address"), "fas fa-plus", style = ButtonStyle.PRIMARY).onClick { EditPanel.add() } div().bind(Model.profile) { profile -> if (profile.name != null) { button("Logout: ${profile.name}", "fas fa-sign-out-alt", style = ButtonStyle.WARNING).onClick { document.location?.href = "/logout" } } } } } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/Model.kt ================================================ package com.example import dev.kilua.rpc.getService import io.kvision.state.ObservableList import io.kvision.state.ObservableValue import io.kvision.state.observableListOf import io.kvision.utils.syncWithList import kotlinx.coroutines.launch object Model { private val addressService = getService() private val profileService = getService() private val registerProfileService = getService() val addresses: ObservableList
= observableListOf() val profile = ObservableValue(Profile()) var search: String? = null set(value) { field = value AppScope.launch { getAddressList() } } var types: String = "all" set(value) { field = value AppScope.launch { getAddressList() } } var sort = Sort.FN set(value) { field = value AppScope.launch { getAddressList() } } suspend fun getAddressList() { Security.withAuth { val newAddresses = addressService.getAddressList(search, types, sort) addresses.syncWithList(newAddresses) } } suspend fun addAddress(address: Address) { Security.withAuth { addressService.addAddress(address) getAddressList() } } suspend fun updateAddress(address: Address) { Security.withAuth { addressService.updateAddress(address) getAddressList() } } suspend fun deleteAddress(id: Int): Boolean { return Security.withAuth { val result = addressService.deleteAddress(id) getAddressList() result } } suspend fun readProfile() { Security.withAuth { profile.value = profileService.getProfile() } } suspend fun registerProfile(profile: Profile, password: String): Boolean { return try { registerProfileService.registerProfile(profile, password) } catch (e: Exception) { console.log(e) false } } } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/kotlin/com/example/Security.kt ================================================ package com.example import dev.kilua.rpc.SecurityException import io.kvision.core.onEvent import io.kvision.form.FormPanel import io.kvision.form.formPanel import io.kvision.form.text.Password import io.kvision.form.text.Text import io.kvision.html.Button import io.kvision.html.ButtonStyle import io.kvision.i18n.I18n.tr import io.kvision.modal.Alert import io.kvision.modal.Dialog import io.kvision.remote.SecurityMgr import io.kvision.rest.HttpMethod import io.kvision.rest.ResponseBodyType import io.kvision.rest.RestClient import io.kvision.rest.requestDynamic import io.kvision.utils.ENTER_KEY import io.kvision.utils.obj import kotlinx.coroutines.asDeferred import kotlinx.coroutines.launch import kotlinx.serialization.Serializable /** * Username and password credentials. */ @Serializable data class Credentials(val username: String? = null, val password: String? = null) /** * Form login dispatcher. */ class LoginService(val loginEndpoint: String) { val loginAgent = RestClient() /** * Login with a form. * @param credentials username and password credentials */ suspend fun login(credentials: Credentials?): Boolean = if (credentials?.username != null) { loginAgent.requestDynamic(loginEndpoint) { data = obj { this.username = credentials.username this.password = credentials.password } method = HttpMethod.POST contentType = "application/x-www-form-urlencoded" responseBodyType = ResponseBodyType.READABLE_STREAM }.then { _: dynamic -> true }.asDeferred().await() } else { throw SecurityException("Credentials cannot be empty") } } class LoginWindow : Dialog(closeButton = false, escape = false, animation = false) { private val loginPanel: FormPanel private val loginButton: Button private val userButton: Button private val registerPanel: FormPanel private val registerButton: Button private val cancelButton: Button init { loginPanel = formPanel { add(Credentials::username, Text(label = "${tr("Login")}:"), required = true) add(Credentials::password, Password(label = "${tr("Password")}:"), required = true) onEvent { keydown = { if (it.keyCode == ENTER_KEY) { this@LoginWindow.processCredentials() } } } } registerPanel = formPanel { add(Profile::name, Text(label = "${tr("Your name")}:"), required = true) add(Profile::username, Text(label = "Login:"), required = true) add( Profile::password, Password(label = "${tr("Password")}:"), required = true, validatorMessage = { "Password too short" }) { (it.getValue()?.length ?: 0) >= 8 } add( Profile::password2, Password(label = "${tr("Confirm password")}:"), required = true, validatorMessage = { tr("Password too short") }) { (it.getValue()?.length ?: 0) >= 8 } validator = { val result = it[Profile::password] == it[Profile::password2] if (!result) { it.getControl(Profile::password)?.validatorError = tr("Passwords are not the same") it.getControl(Profile::password2)?.validatorError = tr("Passwords are not the same") } result } validatorMessage = { tr("Passwords are not the same") } } cancelButton = Button(tr("Cancel"), "fas fa-times") { onClick { this@LoginWindow.hideRegisterForm() } } registerButton = Button(tr("Register"), "fas fa-check", ButtonStyle.PRIMARY) { onClick { this@LoginWindow.processRegister() } } loginButton = Button(tr("Login"), "fas fa-check", ButtonStyle.PRIMARY) { onClick { this@LoginWindow.processCredentials() } } userButton = Button(tr("Register user"), "fas fa-user") { onClick { this@LoginWindow.showRegisterForm() } } addButton(userButton) addButton(loginButton) addButton(cancelButton) addButton(registerButton) hideRegisterForm() } private fun showRegisterForm() { loginPanel.hide() registerPanel.show() registerPanel.clearData() loginButton.hide() userButton.hide() cancelButton.show() registerButton.show() } private fun hideRegisterForm() { loginPanel.show() registerPanel.hide() loginButton.show() userButton.show() cancelButton.hide() registerButton.hide() } private fun processCredentials() { if (loginPanel.validate()) { setResult(loginPanel.getData()) loginPanel.clearData() } } private fun processRegister() { if (registerPanel.validate()) { val userData = registerPanel.getData() AppScope.launch { if (Model.registerProfile(userData, userData.password!!) ) { Alert.show(text = tr("User registered. You can now log in.")) { hideRegisterForm() } } else { Alert.show(text = tr("This login is not available. Please try again.")) } } } } } object Security : SecurityMgr() { private val loginService = LoginService("/login") private val loginWindow = LoginWindow() override suspend fun login(): Boolean { return loginService.login(loginWindow.getResult()) } override suspend fun afterLogin() { Model.readProfile() } } ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/resources/index.html ================================================ KVision Address Book
================================================ FILE: addressbook-fullstack-ktor/src/jsMain/resources/modules/css/kvapp.css ================================================ ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/resources/modules/i18n/messages-en.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/resources/modules/i18n/messages-pl.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-ktor/src/jsMain/resources/modules/i18n/messages.pot ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the addressbook-fullstack-ktor package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: addressbook-fullstack-ktor 1.0.0-SNAPSHOT\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-07-31 13:31+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/kotlin/com/example/Dao.kt ================================================ package com.example import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.Table object AddressDao : Table("address") { val id = integer("id").primaryKey().autoIncrement() val firstName = varchar("first_name", 255).nullable() val lastName = varchar("last_name", 255).nullable() val email = varchar("email", 255).nullable() val phone = varchar("phone", 255).nullable() val postalAddress = varchar("postal_address", 255).nullable() val favourite = bool("favourite") val createdAt = datetime("created_at").nullable() val userId = reference("user_id", UserDao.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) } object UserDao : Table("users") { val id = integer("id").primaryKey().autoIncrement() val name = varchar("name", 255) val username = varchar("username", 255).uniqueIndex() val password = varchar("password", 255) } ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/kotlin/com/example/Db.kt ================================================ package com.example import com.axiomalaska.jdbc.NamedParameterPreparedStatement import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.ktor.server.config.ApplicationConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils.create import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.transactions.transaction import java.math.BigDecimal import java.sql.* object Db { fun init(config: ApplicationConfig) { Database.connect(hikari(config)) transaction { create(UserDao) create(AddressDao) } } private fun hikari(config: ApplicationConfig): HikariDataSource { val hikariConfig = HikariConfig() hikariConfig.driverClassName = config.propertyOrNull("db.driver")?.getString() ?: "org.h2.Driver" hikariConfig.jdbcUrl = config.propertyOrNull("db.jdbcUrl")?.getString() ?: "jdbc:h2:mem:test" hikariConfig.username = config.propertyOrNull("db.username")?.getString() hikariConfig.password = config.propertyOrNull("db.password")?.getString() hikariConfig.maximumPoolSize = 3 hikariConfig.isAutoCommit = false hikariConfig.transactionIsolation = "TRANSACTION_REPEATABLE_READ" hikariConfig.validate() return HikariDataSource(hikariConfig) } suspend fun dbQuery(block: Transaction.() -> T): T = withContext(Dispatchers.IO) { transaction { block() } } fun Transaction.queryList( query: String, parameters: Map, transform: (ResultSet) -> T ): List { val statement = NamedParameterPreparedStatement.createNamedParameterPreparedStatement(connection, query) statement.setParameters(parameters) val result = arrayListOf() val resultSet = statement.executeQuery() resultSet.use { while (resultSet.next()) { result += transform(resultSet) } } return result } fun Transaction.queryObject( query: String, parameters: Map, transform: (ResultSet) -> T ): T? { val statement = NamedParameterPreparedStatement.createNamedParameterPreparedStatement(connection, query) statement.setParameters(parameters) val resultSet = statement.executeQuery() resultSet.use { if (resultSet.next()) { return transform(resultSet) } } return null } private fun NamedParameterPreparedStatement.setParameters(parameters: Map) { parameters.forEach { key, value -> when (value) { null -> setNull(key, Types.NULL) is String -> setString(key, value) is Boolean -> setBoolean(key, value) is Int -> setInt(key, value) is Byte -> setByte(key, value) is Long -> setLong(key, value) is Short -> setShort(key, value) is Float -> setFloat(key, value) is Double -> setDouble(key, value.toFloat()) is BigDecimal -> setBigDecimal(key, value) is Date -> setDate(key, value) is Time -> setTime(key, value) is Timestamp -> setTimestamp(key, value) is ByteArray -> setBytes(key, value) else -> setObject(key, value) } } } } ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/kotlin/com/example/Main.kt ================================================ package com.example import com.example.Db.dbQuery import dev.kilua.rpc.getServiceManager import dev.kilua.rpc.initRpc import dev.kilua.rpc.registerService import dev.kilua.rpc.applyRoutes import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.plugins.calllogging.* import io.ktor.server.plugins.compression.* import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.sessions.* import io.kvision.remote.registerRemoteTypes import org.apache.commons.codec.digest.DigestUtils import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select fun Application.main() { registerRemoteTypes() install(Compression) install(DefaultHeaders) install(CallLogging) install(Sessions) { cookie("KTSESSION", storage = SessionStorageMemory()) { cookie.path = "/" cookie.extensions["SameSite"] = "strict" } } Db.init(environment.config) install(Authentication) { form { userParamName = "username" passwordParamName = "password" validate { credentials -> dbQuery { UserDao.select { (UserDao.username eq credentials.name) and (UserDao.password eq DigestUtils.sha256Hex( credentials.password )) }.firstOrNull()?.let { UserIdPrincipal(credentials.name) } } } skipWhen { call -> call.sessions.get() != null } } } routing { applyRoutes(getServiceManager()) authenticate { post("login") { val principal = call.principal() val result = if (principal != null) { dbQuery { UserDao.select { UserDao.username eq principal.name }.firstOrNull()?.let { val profile = Profile(it[UserDao.id], it[UserDao.name], it[UserDao.username].toString(), null, null) call.sessions.set(profile) HttpStatusCode.OK } ?: HttpStatusCode.Unauthorized } } else { HttpStatusCode.Unauthorized } call.respond(result) } get("logout") { call.sessions.clear() call.respondRedirect("/") } applyRoutes(getServiceManager()) applyRoutes(getServiceManager()) } } initRpc { registerService { AddressService(it) } registerService { ProfileService(it) } registerService { RegisterProfileService() } } } ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/kotlin/com/example/Service.kt ================================================ package com.example import com.example.Db.dbQuery import com.example.Db.queryList import com.github.andrewoma.kwery.core.builder.query import io.ktor.server.application.ApplicationCall import io.ktor.server.sessions.get import io.ktor.server.sessions.sessions import org.apache.commons.codec.digest.DigestUtils import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.update import org.joda.time.DateTime import java.sql.ResultSet import java.time.ZoneId suspend fun ApplicationCall.withProfile(block: suspend (Profile) -> RESP): RESP { val profile = this.sessions.get() return profile?.let { block(profile) } ?: throw IllegalStateException("Profile not set!") } class AddressService(private val call: ApplicationCall) : IAddressService { override suspend fun getAddressList(search: String?, types: String, sort: Sort) = call.withProfile { profile -> dbQuery { val query = query { select("SELECT * FROM address") whereGroup { where("user_id = :user_id") parameter("user_id", profile.id) search?.let { where( """(lower(first_name) like :search OR lower(last_name) like :search OR lower(email) like :search OR lower(phone) like :search OR lower(postal_address) like :search)""".trimMargin() ) parameter("search", "%${it.lowercase()}%") } if (types == "fav") { where("favourite") } } when (sort) { Sort.FN -> orderBy("lower(first_name)") Sort.LN -> orderBy("lower(last_name)") Sort.E -> orderBy("lower(email)") Sort.F -> orderBy("favourite") } } queryList(query.sql, query.parameters) { toAddress(it) } } } override suspend fun addAddress(address: Address) = call.withProfile { profile -> val key = dbQuery { (AddressDao.insert { it[firstName] = address.firstName it[lastName] = address.lastName it[email] = address.email it[phone] = address.phone it[postalAddress] = address.postalAddress it[favourite] = address.favourite ?: false it[createdAt] = DateTime() it[userId] = profile.id!! } get AddressDao.id) } getAddress(key)!! } override suspend fun updateAddress(address: Address) = call.withProfile { profile -> address.id?.let { getAddress(it)?.let { oldAddress -> dbQuery { AddressDao.update({ AddressDao.id eq it }) { it[firstName] = address.firstName it[lastName] = address.lastName it[email] = address.email it[phone] = address.phone it[postalAddress] = address.postalAddress it[favourite] = address.favourite ?: false it[createdAt] = oldAddress.createdAt ?.let { DateTime(java.util.Date.from(it.atZone(ZoneId.systemDefault()).toInstant())) } it[userId] = profile.id!! } } } getAddress(it) } ?: throw IllegalArgumentException("The ID of the address not set") } override suspend fun deleteAddress(id: Int): Boolean = call.withProfile { profile -> dbQuery { AddressDao.deleteWhere { (AddressDao.userId eq profile.id!!) and (AddressDao.id eq id) } > 0 } } private suspend fun getAddress(id: Int): Address? = dbQuery { AddressDao.select { AddressDao.id eq id }.mapNotNull { toAddress(it) }.singleOrNull() } private fun toAddress(row: ResultRow): Address = Address( id = row[AddressDao.id], firstName = row[AddressDao.firstName], lastName = row[AddressDao.lastName], email = row[AddressDao.email], phone = row[AddressDao.phone], postalAddress = row[AddressDao.postalAddress], favourite = row[AddressDao.favourite], createdAt = row[AddressDao.createdAt]?.millis?.let { java.util.Date(it) }?.toInstant() ?.atZone(ZoneId.systemDefault())?.toLocalDateTime(), userId = row[AddressDao.userId] ) private fun toAddress(rs: ResultSet): Address = Address( id = rs.getInt(AddressDao.id.name), firstName = rs.getString(AddressDao.firstName.name), lastName = rs.getString(AddressDao.lastName.name), email = rs.getString(AddressDao.email.name), phone = rs.getString(AddressDao.phone.name), postalAddress = rs.getString(AddressDao.postalAddress.name), favourite = rs.getBoolean(AddressDao.favourite.name), createdAt = rs.getTimestamp(AddressDao.createdAt.name)?.toInstant() ?.atZone(ZoneId.systemDefault())?.toLocalDateTime(), userId = rs.getInt(AddressDao.userId.name) ) } class ProfileService(private val call: ApplicationCall) : IProfileService { override suspend fun getProfile() = call.withProfile { it } } class RegisterProfileService : IRegisterProfileService { override suspend fun registerProfile(profile: Profile, password: String): Boolean { try { dbQuery { UserDao.insert { it[this.name] = profile.name!! it[this.username] = profile.username!! it[this.password] = DigestUtils.sha256Hex(password) } } } catch (e: Exception) { throw Exception("Register operation failed!") } return true } } ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/resources/application.conf ================================================ ktor { deployment { port = 8080 watch = [build/classes/kotlin/jvm/main] } application { modules = [com.example.MainKt.main] } } db { driver = "org.h2.Driver" jdbcUrl = "jdbc:h2:file:////tmp/example_ktor" username = null password = null } ================================================ FILE: addressbook-fullstack-ktor/src/jvmMain/resources/logback.xml ================================================ [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n logs/ktor.log logs/archived/ktor.%d{yyyy-MM-dd}.%i.log.gz 10MB 20GB 60 [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/bootstrap.js ================================================ config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource'}); ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/css.js ================================================ config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/file.js ================================================ config.module.rules.push( { test: /\.(jpe?g|png|gif|svg)$/i, type: 'asset/resource' } ); ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/handlebars.js ================================================ config.module.rules.push( { test: /\.hbs$/i, loader: 'handlebars-loader' } ); ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/proxy.js ================================================ if (config.devServer) { config.devServer.proxy = [ { context: ["/rpc/*", "/rpcsse/*"], target: 'http://localhost:8080' }, { context: ["/login", "/logout"], target: 'http://localhost:8080' }, { context: ["/rpcws/*"], target: 'http://localhost:8080', ws: true } ] } ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/tailwind.js ================================================ ;(function() { config.module.rules.push({ test: /tailwind\.css$/, use: [ '@tailwindcss/webpack' ] }); })(); ================================================ FILE: addressbook-fullstack-ktor/webpack.config.d/webpack.js ================================================ config.resolve.modules.push("kotlin"); if (config.devServer) { config.devServer.client = { overlay: false }; config.devServer.hot = true; config.devServer.open = false; config.devServer.port = 3000; config.devServer.historyApiFallback = true; config.devtool = 'eval-cheap-source-map'; } else { config.devtool = undefined; } // disable bundle size warning config.performance = { assetFilter: function (assetFilename) { return !assetFilename.endsWith('.js'); }, }; ================================================ FILE: addressbook-fullstack-spring-boot/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: addressbook-fullstack-spring-boot/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: addressbook-fullstack-spring-boot/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Compiling * compileKotlinJs - Compiles frontend sources. * compileKotlinJvm - Compiles backend sources. ### Running * jsBrowserDevelopmentRun - Starts a webpack dev server on port 3000 * jvmRun - Starts a dev server on port 8080 ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * jsJar - Packages a standalone "web" frontend jar with all required files into `build/libs/*.jar` * jvmJar - Packages a backend jar with compiled source files into `build/libs/*.jar` * jarWithJs - Packages a "fat" jar with all backend sources and dependencies while also embedding frontend resources into `build/libs/*.jar` ================================================ FILE: addressbook-fullstack-spring-boot/application/build.gradle.kts ================================================ plugins { kotlin("jvm") id("org.springframework.boot") } dependencies { implementation(rootProject) implementation(project.dependencies.platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-devtools") } springBoot { mainClass.value(project.parent?.extra?.get("mainClassName")?.toString()) } ================================================ FILE: addressbook-fullstack-spring-boot/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion kotlin("plugin.spring") version kotlinVersion val kspVersion: String by System.getProperties() id("com.google.devtools.ksp") version kspVersion val kiluaRpcVersion: String by System.getProperties() id("dev.kilua.rpc") version kiluaRpcVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" // Versions val kvisionVersion: String by System.getProperties() val kiluaRpcVersion: String by System.getProperties() val coroutinesVersion: String by project val r2dbcPostgresqlVersion: String by project val r2dbcH2Version: String by project val e4kVersion: String by project extra["mainClassName"] = "com.example.MainKt" kotlin { jvmToolchain(21) jvm { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs = listOf("-Xjsr305=strict") } } js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets { val commonMain by getting { dependencies { implementation("dev.kilua:kilua-rpc-spring-boot:$kiluaRpcVersion") implementation("io.kvision:kvision-common-remote:$kvisionVersion") } } val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } val jvmMain by getting { dependencies { implementation(kotlin("reflect")) implementation(project.dependencies.platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.postgresql:r2dbc-postgresql:$r2dbcPostgresqlVersion") implementation("io.r2dbc:r2dbc-h2:$r2dbcH2Version") implementation("pl.treksoft:r2dbc-e4k:$e4kVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion") } } val jvmTest by getting { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit")) implementation("org.springframework.boot:spring-boot-starter-test") } } val jsMain by getting { dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-state:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-i18n:$kvisionVersion") implementation("io.kvision:kvision-rest:$kvisionVersion") } } val jsTest by getting { dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } } } ================================================ FILE: addressbook-fullstack-spring-boot/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: addressbook-fullstack-spring-boot/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 systemProp.kspVersion=2.3.6 systemProp.kiluaRpcVersion=0.0.43 systemProp.springBootVersion=4.0.4 #Dependencies systemProp.kvisionVersion=9.5.0 coroutinesVersion=1.10.2 r2dbcPostgresqlVersion=1.1.1.RELEASE r2dbcH2Version=1.0.0.RELEASE e4kVersion=0.9.0 org.gradle.jvmargs=-Xmx2g org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: addressbook-fullstack-spring-boot/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: addressbook-fullstack-spring-boot/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: addressbook-fullstack-spring-boot/settings.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } dependencyResolutionManagement { repositories { mavenCentral() mavenLocal() } } rootProject.name = "addressbook-fullstack-spring-boot" include(":application") ================================================ FILE: addressbook-fullstack-spring-boot/src/commonMain/kotlin/com/example/Model.kt ================================================ @file:UseContextualSerialization(OffsetDateTime::class) package com.example import kotlinx.serialization.Serializable import kotlinx.serialization.UseContextualSerialization import io.kvision.types.OffsetDateTime expect class Profile @Serializable data class Address( val id: Int? = 0, val firstName: String? = null, val lastName: String? = null, val email: String? = null, val phone: String? = null, val postalAddress: String? = null, val favourite: Boolean? = false, val createdAt: OffsetDateTime? = null, val userId: Int? = null ) ================================================ FILE: addressbook-fullstack-spring-boot/src/commonMain/kotlin/com/example/Service.kt ================================================ package com.example import dev.kilua.rpc.annotations.RpcService import kotlinx.serialization.Serializable @Serializable enum class Sort { FN, LN, E, F } @RpcService interface IAddressService { suspend fun getAddressList(search: String?, types: String, sort: Sort): List
suspend fun addAddress(address: Address): Address suspend fun updateAddress(address: Address): Address suspend fun deleteAddress(id: Int): Boolean } @RpcService interface IProfileService { suspend fun getProfile(): Profile } @RpcService interface IRegisterProfileService { suspend fun registerProfile(profile: Profile, password: String): Boolean } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.i18n.DefaultI18nManager import io.kvision.i18n.I18n import io.kvision.panel.root import io.kvision.panel.splitPanel import io.kvision.remote.registerRemoteTypes import io.kvision.startApplication import io.kvision.utils.perc import io.kvision.utils.useModule import io.kvision.utils.vh import kotlinx.browser.window import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch val AppScope = CoroutineScope(window.asCoroutineDispatcher()) @JsModule("./modules/css/kvapp.css") external object kvappCss @JsModule("./modules/i18n/messages-en.json") external val messagesEn: dynamic @JsModule("./modules/i18n/messages-pl.json") external val messagesPl: dynamic class App : Application() { init { useModule(kvappCss) } override fun start() { I18n.manager = DefaultI18nManager( mapOf( "en" to messagesEn, "pl" to messagesPl ) ) root("kvapp") { splitPanel { width = 100.perc height = 100.vh add(ListPanel) add(EditPanel) } } AppScope.launch { Model.getAddressList() } } } fun main() { registerRemoteTypes() startApplication(::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, FontAwesomeModule, CoreModule) } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/EditPanel.kt ================================================ package com.example import io.kvision.core.onEvent import io.kvision.form.FormPanel import io.kvision.form.check.CheckBox import io.kvision.form.formPanel import io.kvision.form.text.Text import io.kvision.html.ButtonStyle import io.kvision.html.InputType import io.kvision.html.button import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.panel.StackPanel import io.kvision.utils.ENTER_KEY import io.kvision.utils.px import kotlinx.coroutines.launch object EditPanel : StackPanel() { private var editingId: Int? = null private val formPanel: FormPanel
init { padding = 10.px formPanel = formPanel { add(Address::firstName, Text(label = "${tr("First name")}:").apply { maxlength = 255 }) add(Address::lastName, Text(label = "${tr("Last name")}:").apply { maxlength = 255 }) add(Address::email, Text(InputType.EMAIL, label = "${tr("E-mail")}:").apply { maxlength = 255 }) { it.getValue() ?.let { "(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])".toRegex() .matches(it) } } add(Address::phone, Text(label = "${tr("Phone number")}:").apply { maxlength = 255 }) add(Address::postalAddress, Text(label = "${tr("Postal address")}:").apply { maxlength = 255 }) add(Address::favourite, CheckBox(label = tr("Mark as favourite"))) add(HPanel(spacing = 10) { button(tr("Save"), "fas fa-check", ButtonStyle.PRIMARY).onClick { this@EditPanel.save() } button(tr("Cancel"), "fas fa-times", ButtonStyle.SECONDARY).onClick { this@EditPanel.close() } }) onEvent { keydown = { if (it.keyCode == ENTER_KEY) { this@EditPanel.save() } } } } add(MainPanel) } fun add() { formPanel.clearData() open(null) } fun edit(index: Int) { val address = Model.addresses[index] formPanel.setData(address) open(address.id) } private fun save() { AppScope.launch { if (formPanel.validate()) { val address = formPanel.getData() if (editingId != null) { Model.updateAddress(address.copy(id = editingId)) } else { Model.addAddress(address) } close() } } } fun delete(index: Int) { AppScope.launch { close() Model.addresses[index].id?.let { Model.deleteAddress(it) } } } private fun open(editingId: Int?) { this.editingId = editingId activeChild = formPanel formPanel.validate() formPanel.getControl(Address::firstName)?.focus() } private fun close() { editingId = null activeChild = MainPanel } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/ListPanel.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.FontStyle import io.kvision.core.onEvent import io.kvision.form.check.RadioGroup import io.kvision.form.check.radioGroup import io.kvision.form.text.TextInput import io.kvision.form.text.text import io.kvision.html.InputType import io.kvision.html.icon import io.kvision.html.link import io.kvision.i18n.I18n.tr import io.kvision.modal.Confirm import io.kvision.panel.SimplePanel import io.kvision.panel.hPanel import io.kvision.state.bind import io.kvision.table.HeaderCell import io.kvision.table.TableType import io.kvision.table.cell import io.kvision.table.row import io.kvision.table.table import io.kvision.utils.px object ListPanel : SimplePanel() { init { padding = 5.px hPanel(alignItems = AlignItems.CENTER, spacing = 20) { text(InputType.SEARCH) { placeholder = "${tr("Search")} ..." setEventListener { input = { Model.search = self.value } } } radioGroup(listOf("all" to tr("All"), "fav" to tr("Favourites")), "all", inline = true) { marginBottom = 0.px setEventListener { change = { Model.types = self.value ?: "all" } } } } table(types = setOf(TableType.STRIPED, TableType.HOVER)) { addHeaderCell(this@ListPanel.sortingHeaderCell(tr("First name"), Sort.FN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("Last name"), Sort.LN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("E-mail"), Sort.E)) addHeaderCell(this@ListPanel.sortingHeaderCell("", Sort.F)) addHeaderCell(HeaderCell("")) bind(Model.addresses) { addresses -> addresses.forEachIndexed { index, address -> row { cell(address.firstName) cell(address.lastName) cell { address.email?.let { link(it, "mailto:$it") { fontStyle = FontStyle.ITALIC } } } cell { address.favourite?.let { if (it) icon("far fa-heart") { title = tr("Favourite") } } } cell { icon("fas fa-times") { title = tr("Delete") onEvent { click = { e -> e.stopPropagation() Confirm.show("Are you sure?", "Do you want to delete this address?") { EditPanel.delete(index) } } } } } onEvent { click = { EditPanel.edit(index) } } } } } } } private fun sortingHeaderCell(title: String, sort: Sort) = HeaderCell(title) { onEvent { click = { Model.sort = sort } } } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/MainPanel.kt ================================================ package com.example import io.kvision.core.JustifyContent import io.kvision.html.ButtonStyle import io.kvision.html.button import io.kvision.html.div import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.state.bind import kotlinx.browser.document object MainPanel : HPanel(justify = JustifyContent.SPACEBETWEEN) { init { button(tr("Add new address"), "fas fa-plus", style = ButtonStyle.PRIMARY).onClick { EditPanel.add() } div().bind(Model.profile) { profile -> if (profile.name != null) { button("Logout: ${profile.name}", "fas fa-sign-out-alt", style = ButtonStyle.WARNING).onClick { document.location?.href = "/logout" } } } } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/Model.kt ================================================ package com.example import dev.kilua.rpc.getService import io.kvision.state.ObservableList import io.kvision.state.ObservableValue import io.kvision.state.observableListOf import io.kvision.utils.syncWithList import kotlinx.coroutines.launch object Model { private val addressService = getService() private val profileService = getService() private val registerProfileService = getService() val addresses: ObservableList
= observableListOf() val profile = ObservableValue(Profile()) var search: String? = null set(value) { field = value AppScope.launch { getAddressList() } } var types: String = "all" set(value) { field = value AppScope.launch { getAddressList() } } var sort = Sort.FN set(value) { field = value AppScope.launch { getAddressList() } } suspend fun getAddressList() { Security.withAuth { val newAddresses = addressService.getAddressList(search, types, sort) addresses.syncWithList(newAddresses) } } suspend fun addAddress(address: Address) { Security.withAuth { addressService.addAddress(address) getAddressList() } } suspend fun updateAddress(address: Address) { Security.withAuth { addressService.updateAddress(address) getAddressList() } } suspend fun deleteAddress(id: Int): Boolean { return Security.withAuth { val result = addressService.deleteAddress(id) getAddressList() result } } suspend fun readProfile() { Security.withAuth { profile.value = profileService.getProfile() } } suspend fun registerProfile(profile: Profile, password: String): Boolean { return try { registerProfileService.registerProfile(profile, password) } catch (e: Exception) { console.log(e) false } } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/kotlin/com/example/Security.kt ================================================ package com.example import dev.kilua.rpc.SecurityException import io.kvision.core.onEvent import io.kvision.form.FormPanel import io.kvision.form.formPanel import io.kvision.form.text.Password import io.kvision.form.text.Text import io.kvision.html.Button import io.kvision.html.ButtonStyle import io.kvision.i18n.I18n.tr import io.kvision.modal.Alert import io.kvision.modal.Dialog import io.kvision.remote.SecurityMgr import io.kvision.rest.HttpMethod import io.kvision.rest.ResponseBodyType import io.kvision.rest.RestClient import io.kvision.rest.requestDynamic import io.kvision.utils.ENTER_KEY import io.kvision.utils.obj import kotlinx.coroutines.asDeferred import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @Serializable actual data class Profile( val name: String? = null, val username: String? = null, val password: String? = null, val password2: String? = null ) /** * Username and password credentials. */ @Serializable data class Credentials(val username: String? = null, val password: String? = null) /** * Form login dispatcher. */ class LoginService(val loginEndpoint: String) { val loginAgent = RestClient() /** * Login with a form. * @param credentials username and password credentials */ suspend fun login(credentials: Credentials?): Boolean = if (credentials?.username != null) { loginAgent.requestDynamic(loginEndpoint) { data = obj { this.username = credentials.username this.password = credentials.password } method = HttpMethod.POST contentType = "application/x-www-form-urlencoded" responseBodyType = ResponseBodyType.READABLE_STREAM }.then { _: dynamic -> true }.asDeferred().await() } else { throw SecurityException("Credentials cannot be empty") } } class LoginWindow : Dialog(closeButton = false, escape = false, animation = false) { private val loginPanel: FormPanel private val loginButton: Button private val userButton: Button private val registerPanel: FormPanel private val registerButton: Button private val cancelButton: Button init { loginPanel = formPanel { add(Credentials::username, Text(label = "${tr("Login")}:"), required = true) add(Credentials::password, Password(label = "${tr("Password")}:"), required = true) onEvent { keydown = { if (it.keyCode == ENTER_KEY) { this@LoginWindow.processCredentials() } } } } registerPanel = formPanel { add(Profile::name, Text(label = "${tr("Your name")}:"), required = true) add(Profile::username, Text(label = "Login:"), required = true) add( Profile::password, Password(label = "${tr("Password")}:"), required = true, validatorMessage = { "Password too short" }) { (it.getValue()?.length ?: 0) >= 8 } add(Profile::password2, Password(label = "${tr("Confirm password")}:"), required = true, validatorMessage = { tr("Password too short") }) { (it.getValue()?.length ?: 0) >= 8 } validator = { val result = it[Profile::password] == it[Profile::password2] if (!result) { it.getControl(Profile::password)?.validatorError = tr("Passwords are not the same") it.getControl(Profile::password2)?.validatorError = tr("Passwords are not the same") } result } validatorMessage = { tr("Passwords are not the same") } } cancelButton = Button(tr("Cancel"), "fas fa-times") { onClick { this@LoginWindow.hideRegisterForm() } } registerButton = Button(tr("Register"), "fas fa-check", ButtonStyle.PRIMARY) { onClick { this@LoginWindow.processRegister() } } loginButton = Button(tr("Login"), "fas fa-check", ButtonStyle.PRIMARY) { onClick { this@LoginWindow.processCredentials() } } userButton = Button(tr("Register user"), "fas fa-user") { onClick { this@LoginWindow.showRegisterForm() } } addButton(userButton) addButton(loginButton) addButton(cancelButton) addButton(registerButton) hideRegisterForm() } private fun showRegisterForm() { loginPanel.hide() registerPanel.show() registerPanel.clearData() loginButton.hide() userButton.hide() cancelButton.show() registerButton.show() } private fun hideRegisterForm() { loginPanel.show() registerPanel.hide() loginButton.show() userButton.show() cancelButton.hide() registerButton.hide() } private fun processCredentials() { if (loginPanel.validate()) { setResult(loginPanel.getData()) loginPanel.clearData() } } private fun processRegister() { if (registerPanel.validate()) { val userData = registerPanel.getData() AppScope.launch { if (Model.registerProfile(userData, userData.password!!) ) { Alert.show(text = tr("User registered. You can now log in.")) { hideRegisterForm() } } else { Alert.show(text = tr("This login is not available. Please try again.")) } } } } } object Security : SecurityMgr() { private val loginService = LoginService("/login") private val loginWindow = LoginWindow() override suspend fun login(): Boolean { return loginService.login(loginWindow.getResult()) } override suspend fun afterLogin() { Model.readProfile() } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/resources/index.html ================================================ KVision Address Book
================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/resources/modules/css/kvapp.css ================================================ ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/resources/modules/i18n/messages-en.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/resources/modules/i18n/messages-pl.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot/src/jsMain/resources/modules/i18n/messages.pot ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the addressbook-fullstack-spring-boot package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: addressbook-fullstack-spring-boot 1.0.0-SNAPSHOT\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-08-01 17:14+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/kotlin/com/example/Main.kt ================================================ package com.example import dev.kilua.rpc.getAllServiceManagers import io.kvision.remote.registerRemoteTypes import io.r2dbc.spi.ConnectionFactory import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean import org.springframework.core.io.Resource import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator @EnableR2dbcRepositories @SpringBootApplication class KVApplication { @Value("classpath:schema.sql") lateinit var schema: Resource @Bean fun initializer(connectionFactory: ConnectionFactory): ConnectionFactoryInitializer { val initializer = ConnectionFactoryInitializer() initializer.setConnectionFactory(connectionFactory) initializer.setDatabasePopulator(ResourceDatabasePopulator(schema)) return initializer } @Bean fun getManagers() = getAllServiceManagers() } fun main(args: Array) { registerRemoteTypes() runApplication(*args) } ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/kotlin/com/example/Security.kt ================================================ package com.example import dev.kilua.rpc.getServiceManager import dev.kilua.rpc.serviceMatchers import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.relational.core.query.Criteria.where import org.springframework.data.relational.core.query.Query.query import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.MapReactiveUserDetailsService import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService import org.springframework.security.core.userdetails.ReactiveUserDetailsService import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.factory.PasswordEncoderFactories import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers import org.springframework.stereotype.Service import pl.treksoft.e4k.core.DbClient import reactor.core.publisher.Mono import java.net.URI @EnableWebFluxSecurity @Configuration class SecurityConfiguration { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http.authorizeExchange { it.serviceMatchers(getServiceManager(), getServiceManager()) .authenticated().pathMatchers("/**").permitAll() }.csrf { it.disable() }.exceptionHandling { it.authenticationEntryPoint { exchange, _ -> val response = exchange.response response.statusCode = HttpStatus.UNAUTHORIZED exchange.mutate().response(response) Mono.empty() } }.formLogin { it.loginPage("/login") .authenticationSuccessHandler(RedirectServerAuthenticationSuccessHandler().apply { this.setRedirectStrategy { exchange, _ -> Mono.fromRunnable { val response = exchange.response response.statusCode = HttpStatus.OK } } }).authenticationFailureHandler(RedirectServerAuthenticationFailureHandler("/login").apply { this.setRedirectStrategy { exchange, _ -> Mono.fromRunnable { val response = exchange.response response.statusCode = HttpStatus.UNAUTHORIZED } } }) }.logout { it.logoutUrl("/logout") .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")) .logoutSuccessHandler(RedirectServerLogoutSuccessHandler().apply { setLogoutSuccessUrl(URI.create("/")) }) }.build() } @Bean fun passwordEncoder(): PasswordEncoder { return PasswordEncoderFactories.createDelegatingPasswordEncoder() } } @Serializable actual data class Profile( val id: String? = null, val name: String? = null ) : UserDetails { @Transient private var password: String? = null @Transient var password2: String? = null private var username: String? = null override fun getUsername(): String { return username!! } fun setUsername(username: String) { this.username = username } override fun getPassword(): String? { return password } fun setPassword(password: String?) { this.password = password } override fun getAuthorities(): MutableCollection { return mutableListOf() } override fun isEnabled(): Boolean { return true } override fun isCredentialsNonExpired(): Boolean { return true } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } } @Table("USERS") data class User(@Id val id: Int? = null, val username: String, val password: String, val name: String) @Service class MyReactiveUserDetailsService(private val client: DbClient) : ReactiveUserDetailsService, ReactiveUserDetailsPasswordService { override fun findByUsername(username: String): Mono { return client.r2dbcEntityTemplate.select(User::class.java).matching(query(where("username").`is`(username))) .first().map { @Suppress("USELESS_CAST") Profile(it.id.toString(), it.name).apply { this.username = it.username this.password = it.password } as UserDetails }.switchIfEmpty( Mono.error(UsernameNotFoundException("User not found")) ) } override fun updatePassword( user: UserDetails, newPassword: String? ): Mono { throw IllegalStateException("Not implemented") } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/kotlin/com/example/Service.kt ================================================ package com.example import io.kvision.types.OffsetDateTime import kotlinx.coroutines.flow.toList import kotlinx.coroutines.reactive.awaitSingle import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.annotation.Scope import org.springframework.r2dbc.core.awaitOne import org.springframework.r2dbc.core.awaitOneOrNull import org.springframework.r2dbc.core.awaitRowsUpdated import org.springframework.r2dbc.core.flow import org.springframework.security.core.Authentication import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.web.reactive.function.server.ServerRequest import pl.treksoft.e4k.core.DbClient import pl.treksoft.e4k.core.delete import pl.treksoft.e4k.core.execute import pl.treksoft.e4k.core.insert import pl.treksoft.e4k.core.setNullable import pl.treksoft.e4k.core.update import pl.treksoft.e4k.core.valueNullable import pl.treksoft.e4k.query.parameterNullable import pl.treksoft.e4k.query.query interface WithProfile { val serverRequest: ServerRequest suspend fun getProfile(): Profile { return serverRequest.principal().ofType(Authentication::class.java).map { it.principal as Profile }.awaitSingle() } } @Service @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class AddressService(override val serverRequest: ServerRequest, private val dbClient: DbClient) : IAddressService, WithProfile { override suspend fun getAddressList(search: String?, types: String, sort: Sort): List
{ val profile = getProfile() val query = query { select("SELECT * FROM address") whereGroup { where("user_id = :user_id") parameterNullable("user_id", profile.id?.toInt()) search?.let { where( """(lower(first_name) like :search OR lower(last_name) like :search OR lower(email) like :search OR lower(phone) like :search OR lower(postal_address) like :search)""".trimMargin() ) parameter("search", "%${it.lowercase()}%") } if (types == "fav") { where("favourite") } } when (sort) { Sort.FN -> orderBy("lower(first_name)") Sort.LN -> orderBy("lower(last_name)") Sort.E -> orderBy("lower(email)") Sort.F -> orderBy("favourite") } } return dbClient.execute
(query).flow().toList() } override suspend fun addAddress(address: Address): Address { val profile = getProfile() val id = dbClient.insert().into("address", "id") .valueNullable("first_name", address.firstName) .valueNullable("last_name", address.lastName) .valueNullable("email", address.email) .valueNullable("phone", address.phone) .valueNullable("postal_address", address.postalAddress) .value("favourite", address.favourite == true) .value("created_at", OffsetDateTime.now()) .value("user_id", profile.id!!.toInt()) .awaitOne() return dbClient.execute
("SELECT * FROM address WHERE id = :id") .bind("id", id).fetch().awaitOne() } override suspend fun updateAddress(address: Address): Address { val profile = getProfile() val id = address.id ?: throw IllegalArgumentException("The ID of the address is not set") dbClient.execute
("SELECT * FROM address WHERE id = :id AND user_id = :userId") .bind("id", id).bind("userId", profile.id!!.toInt()) .fetch().awaitOneOrNull() ?: throw IllegalArgumentException("Address not found") dbClient.update().table("address").using { Update.setNullable("first_name", address.firstName) .setNullable("last_name", address.lastName) .setNullable("email", address.email) .setNullable("phone", address.phone) .setNullable("postal_address", address.postalAddress) .set("favourite", address.favourite == true) }.matching("id = :id", mapOf("id" to id)).fetch().awaitRowsUpdated() return dbClient.execute
("SELECT * FROM address WHERE id = :id") .bind("id", id).fetch().awaitOne() } override suspend fun deleteAddress(id: Int): Boolean { return dbClient.delete().from("address") .matching("id = :id", mapOf("id" to id)).fetch().awaitRowsUpdated() == 1L } } @Service @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class ProfileService(override val serverRequest: ServerRequest) : IProfileService, WithProfile { override suspend fun getProfile(): Profile { return super.getProfile() } } @Service @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class RegisterProfileService( private val dbClient: DbClient, private val passwordEncoder: PasswordEncoder ) : IRegisterProfileService { override suspend fun registerProfile(profile: Profile, password: String): Boolean { try { dbClient.insert().into(User::class.java).using( User( username = profile.username, name = profile.name!!, password = passwordEncoder.encode(password)!! ) ).awaitSingle() } catch (e: Exception) { throw Exception("Register operation failed!") } return true } } ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/resources/application.yml ================================================ spring: r2dbc: url: r2dbc:h2:file:////tmp/example_spring?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE server: compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/resources/logback.xml ================================================ %d [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: addressbook-fullstack-spring-boot/src/jvmMain/resources/schema.sql ================================================ CREATE TABLE IF NOT EXISTS users ( id serial NOT NULL, username varchar(255) NOT NULL, password varchar(255) NOT NULL, name varchar(255) NOT NULL, PRIMARY KEY (id), UNIQUE(username) ); CREATE TABLE IF NOT EXISTS address ( id serial NOT NULL, first_name varchar(255), last_name varchar(255), email varchar(255), phone varchar(255), postal_address varchar(255), favourite boolean NOT NULL DEFAULT false, created_at timestamp with time zone, user_id int NOT NULL, PRIMARY KEY (id), FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE ); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/bootstrap.js ================================================ config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource'}); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/css.js ================================================ config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/file.js ================================================ config.module.rules.push( { test: /\.(jpe?g|png|gif|svg)$/i, type: 'asset/resource' } ); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/handlebars.js ================================================ config.module.rules.push( { test: /\.hbs$/i, loader: 'handlebars-loader' } ); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/proxy.js ================================================ if (config.devServer) { config.devServer.proxy = [ { context: ["/rpc/*", "/rpcsse/*"], target: 'http://localhost:8080' }, { context: ["/login", "/logout"], target: 'http://localhost:8080' }, { context: ["/rpcws/*"], target: 'http://localhost:8080', ws: true } ] } ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/tailwind.js ================================================ ;(function() { config.module.rules.push({ test: /tailwind\.css$/, use: [ '@tailwindcss/webpack' ] }); })(); ================================================ FILE: addressbook-fullstack-spring-boot/webpack.config.d/webpack.js ================================================ config.resolve.modules.push("kotlin"); if (config.devServer) { config.devServer.client = { overlay: false }; config.devServer.hot = true; config.devServer.open = false; config.devServer.port = 3000; config.devServer.historyApiFallback = true; config.devtool = 'eval-cheap-source-map'; } else { config.devtool = undefined; } // disable bundle size warning config.performance = { assetFilter: function (assetFilename) { return !assetFilename.endsWith('.js'); }, }; ================================================ FILE: addressbook-fullstack-spring-boot-oauth/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: addressbook-fullstack-spring-boot-oauth/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Compiling * compileKotlinJs - Compiles frontend sources. * compileKotlinJvm - Compiles backend sources. ### Running * jsBrowserDevelopmentRun - Starts a webpack dev server on port 3000 * jvmRun - Starts a dev server on port 8080 You need to pass your Google oauth application's Client ID and Client Secret in: ``` gradle jvmRun -Dclient.id=yourclientid -Dclient.secret=yourclientsecret ``` ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * jsJar - Packages a standalone "web" frontend jar with all required files into `build/libs/*.jar` * jvmJar - Packages a backend jar with compiled source files into `build/libs/*.jar` * jarWithJs - Packages a "fat" jar with all backend sources and dependencies while also embedding frontend resources into `build/libs/*.jar` ================================================ FILE: addressbook-fullstack-spring-boot-oauth/application/build.gradle.kts ================================================ plugins { kotlin("jvm") id("org.springframework.boot") } dependencies { implementation(rootProject) implementation(project.dependencies.platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-devtools") } springBoot { mainClass.value(project.parent?.extra?.get("mainClassName")?.toString()) } tasks.named("bootRun") { jvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005") systemProperties = System.getProperties().toMap() as Map } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion kotlin("plugin.spring") version kotlinVersion val kspVersion: String by System.getProperties() id("com.google.devtools.ksp") version kspVersion val kiluaRpcVersion: String by System.getProperties() id("dev.kilua.rpc") version kiluaRpcVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" // Versions val kvisionVersion: String by System.getProperties() val kiluaRpcVersion: String by System.getProperties() val coroutinesVersion: String by project val r2dbcPostgresqlVersion: String by project val r2dbcH2Version: String by project val e4kVersion: String by project extra["mainClassName"] = "com.example.MainKt" kotlin { jvmToolchain(21) jvm { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs = listOf("-Xjsr305=strict") } } js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets { val commonMain by getting { dependencies { implementation("dev.kilua:kilua-rpc-spring-boot:$kiluaRpcVersion") implementation("io.kvision:kvision-common-remote:$kvisionVersion") } } val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } val jvmMain by getting { dependencies { implementation(kotlin("reflect")) implementation(project.dependencies.platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.springframework.security:spring-security-oauth2-client") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.postgresql:r2dbc-postgresql:$r2dbcPostgresqlVersion") implementation("io.r2dbc:r2dbc-h2:$r2dbcH2Version") implementation("pl.treksoft:r2dbc-e4k:$e4kVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutinesVersion") } } val jvmTest by getting { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit")) implementation("org.springframework.boot:spring-boot-starter-test") } } val jsMain by getting { dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-state:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-i18n:$kvisionVersion") implementation("io.kvision:kvision-rest:$kvisionVersion") } } val jsTest by getting { dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: addressbook-fullstack-spring-boot-oauth/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 systemProp.kspVersion=2.3.6 systemProp.kiluaRpcVersion=0.0.43 systemProp.springBootVersion=4.0.4 #Dependencies systemProp.kvisionVersion=9.5.0 coroutinesVersion=1.10.2 r2dbcPostgresqlVersion=1.1.1.RELEASE r2dbcH2Version=1.0.0.RELEASE e4kVersion=0.9.0 org.gradle.jvmargs=-Xmx2g org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: addressbook-fullstack-spring-boot-oauth/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: addressbook-fullstack-spring-boot-oauth/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: addressbook-fullstack-spring-boot-oauth/settings.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } dependencyResolutionManagement { repositories { mavenCentral() mavenLocal() } } rootProject.name = "addressbook-fullstack-spring-boot-oauth" include(":application") ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/commonMain/kotlin/com/example/Model.kt ================================================ @file:UseContextualSerialization(OffsetDateTime::class) package com.example import kotlinx.serialization.Serializable import kotlinx.serialization.UseContextualSerialization import io.kvision.types.OffsetDateTime expect class Profile @Serializable data class Address( val id: Int? = 0, val firstName: String? = null, val lastName: String? = null, val email: String? = null, val phone: String? = null, val postalAddress: String? = null, val favourite: Boolean? = false, val createdAt: OffsetDateTime? = null, val userId: Int? = null ) ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/commonMain/kotlin/com/example/Service.kt ================================================ package com.example import dev.kilua.rpc.annotations.RpcService import kotlinx.serialization.Serializable @Serializable enum class Sort { FN, LN, E, F } @RpcService interface IAddressService { suspend fun getAddressList(search: String?, types: String, sort: Sort): List
suspend fun addAddress(address: Address): Address suspend fun updateAddress(address: Address): Address suspend fun deleteAddress(id: Int): Boolean } @RpcService interface IProfileService { suspend fun getProfile(): Profile } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.i18n.DefaultI18nManager import io.kvision.i18n.I18n import io.kvision.panel.root import io.kvision.panel.splitPanel import io.kvision.remote.registerRemoteTypes import io.kvision.startApplication import io.kvision.utils.perc import io.kvision.utils.useModule import io.kvision.utils.vh import kotlinx.browser.window import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch val AppScope = CoroutineScope(window.asCoroutineDispatcher()) @JsModule("./modules/css/kvapp.css") external object kvappCss @JsModule("./modules/i18n/messages-en.json") external val messagesEn: dynamic @JsModule("./modules/i18n/messages-pl.json") external val messagesPl: dynamic class App : Application() { init { useModule(kvappCss) } override fun start() { I18n.manager = DefaultI18nManager( mapOf( "en" to messagesEn, "pl" to messagesPl ) ) root("kvapp") { splitPanel { width = 100.perc height = 100.vh add(ListPanel) add(EditPanel) } } AppScope.launch { Model.getAddressList() } } } fun main() { registerRemoteTypes() startApplication(::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, FontAwesomeModule, CoreModule) } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/EditPanel.kt ================================================ package com.example import io.kvision.core.onEvent import io.kvision.form.FormPanel import io.kvision.form.check.CheckBox import io.kvision.form.formPanel import io.kvision.form.text.Text import io.kvision.html.ButtonStyle import io.kvision.html.InputType import io.kvision.html.button import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.panel.StackPanel import io.kvision.utils.ENTER_KEY import io.kvision.utils.px import kotlinx.coroutines.launch object EditPanel : StackPanel() { private var editingId: Int? = null private val formPanel: FormPanel
init { padding = 10.px formPanel = formPanel { add(Address::firstName, Text(label = "${tr("First name")}:").apply { maxlength = 255 }) add(Address::lastName, Text(label = "${tr("Last name")}:").apply { maxlength = 255 }) add(Address::email, Text(InputType.EMAIL, label = "${tr("E-mail")}:").apply { maxlength = 255 }) { it.getValue() ?.let { "(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])".toRegex() .matches(it) } } add(Address::phone, Text(label = "${tr("Phone number")}:").apply { maxlength = 255 }) add(Address::postalAddress, Text(label = "${tr("Postal address")}:").apply { maxlength = 255 }) add(Address::favourite, CheckBox(label = tr("Mark as favourite"))) add(HPanel(spacing = 10) { button(tr("Save"), "fas fa-check", ButtonStyle.PRIMARY).onClick { this@EditPanel.save() } button(tr("Cancel"), "fas fa-times", ButtonStyle.SECONDARY).onClick { this@EditPanel.close() } }) onEvent { keydown = { if (it.keyCode == ENTER_KEY) { this@EditPanel.save() } } } } add(MainPanel) } fun add() { formPanel.clearData() open(null) } fun edit(index: Int) { val address = Model.addresses[index] formPanel.setData(address) open(address.id) } private fun save() { AppScope.launch { if (formPanel.validate()) { val address = formPanel.getData() if (editingId != null) { Model.updateAddress(address.copy(id = editingId)) } else { Model.addAddress(address) } close() } } } fun delete(index: Int) { AppScope.launch { close() Model.addresses[index].id?.let { Model.deleteAddress(it) } } } private fun open(editingId: Int?) { this.editingId = editingId activeChild = formPanel formPanel.validate() formPanel.getControl(Address::firstName)?.focus() } private fun close() { editingId = null activeChild = MainPanel } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/ListPanel.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.FontStyle import io.kvision.core.onEvent import io.kvision.form.check.RadioGroup import io.kvision.form.check.radioGroup import io.kvision.form.text.TextInput import io.kvision.form.text.text import io.kvision.html.InputType import io.kvision.html.icon import io.kvision.html.link import io.kvision.i18n.I18n.tr import io.kvision.modal.Confirm import io.kvision.panel.SimplePanel import io.kvision.panel.hPanel import io.kvision.state.bind import io.kvision.table.HeaderCell import io.kvision.table.TableType import io.kvision.table.cell import io.kvision.table.row import io.kvision.table.table import io.kvision.utils.px object ListPanel : SimplePanel() { init { padding = 5.px hPanel(alignItems = AlignItems.CENTER, spacing = 20) { text(InputType.SEARCH) { placeholder = "${tr("Search")} ..." setEventListener { input = { Model.search = self.value } } } radioGroup(listOf("all" to tr("All"), "fav" to tr("Favourites")), "all", inline = true) { marginBottom = 0.px setEventListener { change = { Model.types = self.value ?: "all" } } } } table(types = setOf(TableType.STRIPED, TableType.HOVER)) { addHeaderCell(this@ListPanel.sortingHeaderCell(tr("First name"), Sort.FN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("Last name"), Sort.LN)) addHeaderCell(this@ListPanel.sortingHeaderCell(tr("E-mail"), Sort.E)) addHeaderCell(this@ListPanel.sortingHeaderCell("", Sort.F)) addHeaderCell(HeaderCell("")) bind(Model.addresses) { addresses -> addresses.forEachIndexed { index, address -> row { cell(address.firstName) cell(address.lastName) cell { address.email?.let { link(it, "mailto:$it") { fontStyle = FontStyle.ITALIC } } } cell { address.favourite?.let { if (it) icon("far fa-heart") { title = tr("Favourite") } } } cell { icon("fas fa-times") { title = tr("Delete") onEvent { click = { e -> e.stopPropagation() Confirm.show("Are you sure?", "Do you want to delete this address?") { EditPanel.delete(index) } } } } } onEvent { click = { EditPanel.edit(index) } } } } } } } private fun sortingHeaderCell(title: String, sort: Sort) = HeaderCell(title) { onEvent { click = { Model.sort = sort } } } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/MainPanel.kt ================================================ package com.example import io.kvision.core.JustifyContent import io.kvision.html.ButtonStyle import io.kvision.html.button import io.kvision.html.div import io.kvision.i18n.I18n.tr import io.kvision.panel.HPanel import io.kvision.state.bind import kotlinx.browser.document object MainPanel : HPanel(justify = JustifyContent.SPACEBETWEEN) { init { button(tr("Add new address"), "fas fa-plus", style = ButtonStyle.PRIMARY).onClick { EditPanel.add() } div().bind(Model.profile) { profile -> if (profile.name != null) { button("Logout: ${profile.name}", "fas fa-sign-out-alt", style = ButtonStyle.WARNING).onClick { document.location?.href = "/logout" } } } } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/Model.kt ================================================ package com.example import dev.kilua.rpc.getService import io.kvision.state.ObservableList import io.kvision.state.ObservableValue import io.kvision.state.observableListOf import io.kvision.utils.syncWithList import kotlinx.coroutines.launch object Model { private val addressService = getService() private val profileService = getService() val addresses: ObservableList
= observableListOf() val profile = ObservableValue(Profile()) var search: String? = null set(value) { field = value AppScope.launch { getAddressList() } } var types: String = "all" set(value) { field = value AppScope.launch { getAddressList() } } var sort = Sort.FN set(value) { field = value AppScope.launch { getAddressList() } } suspend fun getAddressList() { Security.withAuth { val newAddresses = addressService.getAddressList(search, types, sort) addresses.syncWithList(newAddresses) } } suspend fun addAddress(address: Address) { Security.withAuth { addressService.addAddress(address) getAddressList() } } suspend fun updateAddress(address: Address) { Security.withAuth { addressService.updateAddress(address) getAddressList() } } suspend fun deleteAddress(id: Int): Boolean { return Security.withAuth { val result = addressService.deleteAddress(id) getAddressList() result } } suspend fun readProfile() { Security.withAuth { profile.value = profileService.getProfile() } } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/kotlin/com/example/Security.kt ================================================ package com.example import dev.kilua.rpc.SecurityException import io.kvision.html.Button import io.kvision.html.ButtonStyle import io.kvision.i18n.I18n.tr import io.kvision.modal.Dialog import io.kvision.remote.SecurityMgr import io.kvision.rest.HttpMethod import io.kvision.rest.ResponseBodyType import io.kvision.rest.RestClient import io.kvision.rest.requestDynamic import io.kvision.utils.obj import kotlinx.browser.document import kotlinx.coroutines.asDeferred import kotlinx.serialization.Serializable @Serializable actual data class Profile( val name: String? = null, val username: String? = null ) @Serializable data class Credentials(val username: String? = null, val password: String? = null) /** * Form login dispatcher. */ class LoginService(val loginEndpoint: String) { val loginAgent = RestClient() /** * Login with a form. * @param credentials username and password credentials */ suspend fun login(credentials: Credentials?): Boolean = if (credentials?.username != null) { loginAgent.requestDynamic(loginEndpoint) { data = obj { this.username = credentials.username this.password = credentials.password } method = HttpMethod.POST contentType = "application/x-www-form-urlencoded" responseBodyType = ResponseBodyType.READABLE_STREAM }.then { _: dynamic -> true }.asDeferred().await() } else { throw SecurityException("Credentials cannot be empty") } } class LoginWindow : Dialog(closeButton = false, escape = false, animation = false) { private val loginButton: Button init { loginButton = Button(tr("Login"), "fas fa-check", ButtonStyle.PRIMARY) { onClick { this@LoginWindow.processCredentials() } } addButton(loginButton) } private fun processCredentials() { document.location?.href = "/oauth2/authorization/google" } } object Security : SecurityMgr() { private val loginService = LoginService("/oauth2/authorization/google") private val loginWindow = LoginWindow() override suspend fun login(): Boolean { return loginService.login(loginWindow.getResult()) } override suspend fun afterLogin() { Model.readProfile() } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/resources/index.html ================================================ KVision Address Book
================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/resources/modules/css/kvapp.css ================================================ ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/resources/modules/i18n/messages-en.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/resources/modules/i18n/messages-pl.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jsMain/resources/modules/i18n/messages.pot ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the addressbook-fullstack-spring-boot package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: addressbook-fullstack-spring-boot 1.0.0-SNAPSHOT\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-08-01 17:14+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:36 msgid "Mark as favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:39 msgid "Save" msgstr "" #: ../src/frontendMain/kotlin/com/example/EditPanel.kt:42 #: ../src/frontendMain/kotlin/com/example/Security.kt:73 msgid "Cancel" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:31 msgid "First name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:32 msgid "Last name" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:33 msgid "E-mail" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "All" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:47 msgid "Favourites" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:72 msgid "Favourite" msgstr "" #: ../src/frontendMain/kotlin/com/example/ListPanel.kt:78 msgid "Delete" msgstr "" #: ../src/frontendMain/kotlin/com/example/MainPanel.kt:15 msgid "Add new address" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:59 msgid "Password too short" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:65 #: ../src/frontendMain/kotlin/com/example/Security.kt:66 #: ../src/frontendMain/kotlin/com/example/Security.kt:70 msgid "Passwords are not the same" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:76 msgid "Register" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:79 msgid "Login" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:82 msgid "Register user" msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:132 msgid "User registered. You can now log in." msgstr "" #: ../src/frontendMain/kotlin/com/example/Security.kt:136 msgid "This login is not available. Please try again." msgstr "" ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/kotlin/com/example/Main.kt ================================================ package com.example import dev.kilua.rpc.getAllServiceManagers import io.kvision.remote.registerRemoteTypes import io.r2dbc.spi.ConnectionFactory import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean import org.springframework.core.io.Resource import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity @EnableR2dbcRepositories @EnableWebFluxSecurity @SpringBootApplication( exclude = [ org.springframework.boot.security.autoconfigure.web.reactive.ReactiveWebSecurityAutoConfiguration::class, org.springframework.boot.security.autoconfigure.ReactiveUserDetailsServiceAutoConfiguration::class, ] ) class KVApplication { @Value("classpath:schema.sql") lateinit var schema: Resource @Bean fun initializer(connectionFactory: ConnectionFactory): ConnectionFactoryInitializer { val initializer = ConnectionFactoryInitializer() initializer.setConnectionFactory(connectionFactory) initializer.setDatabasePopulator(ResourceDatabasePopulator(schema)) return initializer } @Bean fun getManagers() = getAllServiceManagers() } fun main(args: Array) { registerRemoteTypes() runApplication(*args) } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/kotlin/com/example/Security.kt ================================================ package com.example import dev.kilua.rpc.RpcServiceManager import dev.kilua.rpc.serviceMatchers import kotlinx.serialization.Serializable import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.relational.core.query.Criteria.where import org.springframework.data.relational.core.query.Query.query import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.core.Authentication import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService import org.springframework.security.core.userdetails.ReactiveUserDetailsService import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.web.server.DefaultServerRedirectStrategy import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.ServerRedirectStrategy import org.springframework.security.web.server.WebFilterExchange import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers import org.springframework.stereotype.Component import org.springframework.stereotype.Service import pl.treksoft.e4k.core.DbClient import reactor.core.publisher.Mono import java.net.URI @EnableWebFluxSecurity @Configuration class SecurityConfiguration { //https://github.com/rjaros/kvision/issues/160 @Bean fun securityWebFilterChain(http: ServerHttpSecurity, serviceManagers : List>, successHandler: OAuth2LoginSuccessHandler): SecurityWebFilterChain { return http .authorizeExchange { serviceManagers.forEach { sm -> it.serviceMatchers(sm).authenticated().pathMatchers("/**").permitAll() } } .csrf { it.disable() } .exceptionHandling { it.authenticationEntryPoint { exchange, _ -> val response = exchange.response response.statusCode = HttpStatus.UNAUTHORIZED exchange.mutate().response(response) Mono.empty() } } .oauth2Login{oauth2 -> oauth2.authenticationSuccessHandler(successHandler) } .logout { it.logoutUrl("/logout") .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")) .logoutSuccessHandler(RedirectServerLogoutSuccessHandler().apply { setLogoutSuccessUrl(URI.create("/")) }) }.build() } } @Serializable actual data class Profile( val id: String? = null, val name: String? = null ) : UserDetails { private var username: String? = null override fun getUsername(): String { return username!! } fun setUsername(username: String) { this.username = username } override fun getPassword(): String = throw UnsupportedOperationException() override fun getAuthorities(): MutableCollection { return mutableListOf() } override fun isEnabled(): Boolean { return true } override fun isCredentialsNonExpired(): Boolean { return true } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } } @Table("USERS") data class User(@Id val id: Int? = null, val username: String, val name: String) @Service class MyReactiveUserDetailsService(private val client: DbClient) : ReactiveUserDetailsService, ReactiveUserDetailsPasswordService { override fun findByUsername(username: String): Mono { return client.r2dbcEntityTemplate.select(User::class.java).matching(query(where("username").`is`(username))) .first().map { @Suppress("USELESS_CAST") Profile(it.id.toString(), it.name).apply { this.username = it.username } as UserDetails }.switchIfEmpty( Mono.error(UsernameNotFoundException("User not found")) ) } override fun updatePassword( user: UserDetails, newPassword: String? ): Mono { throw IllegalStateException("Not implemented") } } @Component class OAuth2LoginSuccessHandler(private val client: DbClient) : ServerAuthenticationSuccessHandler { private val redirectStrategy: ServerRedirectStrategy = DefaultServerRedirectStrategy() override fun onAuthenticationSuccess( webFilterExchange: WebFilterExchange, authentication: Authentication ): Mono { return if (authentication is OAuth2AuthenticationToken) { val oauth2User = authentication.principal as OAuth2User val attributes = oauth2User.attributes val email = attributes["email"] as String val name = attributes["name"] as String client.r2dbcEntityTemplate.select(User::class.java) .matching(query(where("username").`is`(email))) .first() .flatMap { existingUser -> Mono.just(Profile(existingUser.id.toString(), existingUser.name).apply { username = existingUser.username } as UserDetails) } .switchIfEmpty(Mono.defer { val newUser = User(username = email, name = name) client.r2dbcEntityTemplate.insert(newUser) .map { savedUser -> Profile(savedUser.id.toString(), savedUser.name).apply { username = savedUser.username } } }) .flatMap { val redirectUri = URI.create("http://localhost:3000") redirectStrategy.sendRedirect(webFilterExchange.exchange, redirectUri) } .doOnError { throwable -> println("Error in OAuth2 flow: " + throwable.message) throwable.printStackTrace(); } } else { Mono.error(IllegalStateException("Unsupported authentication type")) } } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/kotlin/com/example/Service.kt ================================================ package com.example import io.kvision.types.OffsetDateTime import kotlinx.coroutines.flow.toList import kotlinx.coroutines.reactive.awaitSingle import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.annotation.Scope import org.springframework.data.relational.core.query.Criteria.where import org.springframework.data.relational.core.query.Query.query import org.springframework.r2dbc.core.awaitOne import org.springframework.r2dbc.core.awaitOneOrNull import org.springframework.r2dbc.core.awaitRowsUpdated import org.springframework.r2dbc.core.flow import org.springframework.security.core.Authentication import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.stereotype.Service import org.springframework.web.reactive.function.server.ServerRequest import pl.treksoft.e4k.core.DbClient import pl.treksoft.e4k.core.delete import pl.treksoft.e4k.core.execute import pl.treksoft.e4k.core.insert import pl.treksoft.e4k.core.setNullable import pl.treksoft.e4k.core.update import pl.treksoft.e4k.core.valueNullable import pl.treksoft.e4k.query.parameterNullable import pl.treksoft.e4k.query.query interface WithProfile { val serverRequest: ServerRequest val dbClient: DbClient suspend fun getProfile(): Profile { return serverRequest.principal() .ofType(Authentication::class.java).flatMap { val email = (it.principal as OidcUser).attributes["email"] as String dbClient.r2dbcEntityTemplate.select(User::class.java) .matching(query(where("username").`is`(email))) .first() .map { existingUser -> Profile(existingUser.id.toString(), existingUser.name).apply { username = existingUser.username } } }.awaitSingle() } } @Service @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class AddressService(override val serverRequest: ServerRequest, override val dbClient: DbClient) : IAddressService, WithProfile { override suspend fun getAddressList(search: String?, types: String, sort: Sort): List
{ val profile = getProfile() val query = query { select("SELECT * FROM address") whereGroup { where("user_id = :user_id") parameterNullable("user_id", profile.id?.toInt()) search?.let { where( """(lower(first_name) like :search OR lower(last_name) like :search OR lower(email) like :search OR lower(phone) like :search OR lower(postal_address) like :search)""".trimMargin() ) parameter("search", "%${it.lowercase()}%") } if (types == "fav") { where("favourite") } } when (sort) { Sort.FN -> orderBy("lower(first_name)") Sort.LN -> orderBy("lower(last_name)") Sort.E -> orderBy("lower(email)") Sort.F -> orderBy("favourite") } } return dbClient.execute
(query).flow().toList() } override suspend fun addAddress(address: Address): Address { val profile = getProfile() val id = dbClient.insert().into("address", "id") .valueNullable("first_name", address.firstName) .valueNullable("last_name", address.lastName) .valueNullable("email", address.email) .valueNullable("phone", address.phone) .valueNullable("postal_address", address.postalAddress) .value("favourite", address.favourite == true) .value("created_at", OffsetDateTime.now()) .value("user_id", profile.id!!.toInt()) .awaitOne() return dbClient.execute
("SELECT * FROM address WHERE id = :id") .bind("id", id).fetch().awaitOne() } override suspend fun updateAddress(address: Address): Address { val profile = getProfile() val id = address.id ?: throw IllegalArgumentException("The ID of the address is not set") dbClient.execute
("SELECT * FROM address WHERE id = :id AND user_id = :userId") .bind("id", id).bind("userId", profile.id!!.toInt()) .fetch().awaitOneOrNull() ?: throw IllegalArgumentException("Address not found") dbClient.update().table("address").using { Update.setNullable("first_name", address.firstName) .setNullable("last_name", address.lastName) .setNullable("email", address.email) .setNullable("phone", address.phone) .setNullable("postal_address", address.postalAddress) .set("favourite", address.favourite == true) }.matching("id = :id", mapOf("id" to id)).fetch().awaitRowsUpdated() return dbClient.execute
("SELECT * FROM address WHERE id = :id") .bind("id", id).fetch().awaitOne() } override suspend fun deleteAddress(id: Int): Boolean { return dbClient.delete().from("address") .matching("id = :id", mapOf("id" to id)).fetch().awaitRowsUpdated() == 1L } } @Service @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class ProfileService(override val serverRequest: ServerRequest, override val dbClient: DbClient) : IProfileService, WithProfile { override suspend fun getProfile(): Profile { return super.getProfile() } } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/resources/application.yml ================================================ spring: r2dbc: url: r2dbc:h2:file:////tmp/example_spring?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE security: oauth2: client: registration: google: redirect-uri: http://localhost:8080/login/oauth2/code/google client-id: ${client.id} client-secret: ${client.secret} scopes: - openid - https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.profile server: compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/resources/logback.xml ================================================ %d [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: addressbook-fullstack-spring-boot-oauth/src/jvmMain/resources/schema.sql ================================================ CREATE TABLE IF NOT EXISTS users ( id serial NOT NULL, username varchar(255) NOT NULL, name varchar(255) NOT NULL, PRIMARY KEY (id), UNIQUE(username) ); CREATE TABLE IF NOT EXISTS address ( id serial NOT NULL, first_name varchar(255), last_name varchar(255), email varchar(255), phone varchar(255), postal_address varchar(255), favourite boolean NOT NULL DEFAULT false, created_at timestamp with time zone, user_id int NOT NULL, PRIMARY KEY (id), FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE ); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/bootstrap.js ================================================ config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource'}); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/css.js ================================================ config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/file.js ================================================ config.module.rules.push( { test: /\.(jpe?g|png|gif|svg)$/i, type: 'asset/resource' } ); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/handlebars.js ================================================ config.module.rules.push( { test: /\.hbs$/i, loader: 'handlebars-loader' } ); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/proxy.js ================================================ if (config.devServer) { config.devServer.proxy = [ { context: ["/rpc/*", "/rpcsse/*"], target: 'http://localhost:8080' }, { context: ["/login", "/logout", "/oauth2/authorization/google"], target: 'http://localhost:8080' }, { context: ["/rpcws/*"], target: 'http://localhost:8080', ws: true } ] } ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/tailwind.js ================================================ ;(function() { config.module.rules.push({ test: /tailwind\.css$/, use: [ '@tailwindcss/webpack' ] }); })(); ================================================ FILE: addressbook-fullstack-spring-boot-oauth/webpack.config.d/webpack.js ================================================ config.resolve.modules.push("kotlin"); if (config.devServer) { config.devServer.client = { overlay: false }; config.devServer.hot = true; config.devServer.open = false; config.devServer.port = 3000; config.devServer.historyApiFallback = true; config.devtool = 'eval-cheap-source-map'; } else { config.devtool = undefined; } // disable bundle size warning config.performance = { assetFilter: function (assetFilename) { return !assetFilename.endsWith('.js'); }, }; ================================================ FILE: addressbook-tabulator/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: addressbook-tabulator/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: addressbook-tabulator/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Running * run - Starts a webpack dev server on port 3000. ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * zip - Packages a zip archive with all required files into `build/libs/*.zip` ================================================ FILE: addressbook-tabulator/build.gradle.kts ================================================ plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" repositories { mavenCentral() mavenLocal() } // Versions val kvisionVersion: String by System.getProperties() kotlin { js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets["jsMain"].dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-i18n:$kvisionVersion") implementation("io.kvision:kvision-tabulator:$kvisionVersion") } sourceSets["jsTest"].dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } ================================================ FILE: addressbook-tabulator/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: addressbook-tabulator/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 #Dependencies systemProp.kvisionVersion=9.5.0 org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: addressbook-tabulator/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: addressbook-tabulator/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: addressbook-tabulator/settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } rootProject.name = "addressbook-tabulator" ================================================ FILE: addressbook-tabulator/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.TabulatorCssMaterializeModule import io.kvision.TabulatorModule import io.kvision.i18n.DefaultI18nManager import io.kvision.i18n.I18n import io.kvision.panel.root import io.kvision.panel.splitPanel import io.kvision.startApplication import io.kvision.utils.perc import io.kvision.utils.useModule import io.kvision.utils.vh @JsModule("./modules/css/kvapp.css") external object kvappCss @JsModule("./modules/i18n/messages-en.json") external val messagesEn: dynamic @JsModule("./modules/i18n/messages-pl.json") external val messagesPl: dynamic class App : Application() { init { useModule(kvappCss) } override fun start() { I18n.manager = DefaultI18nManager( mapOf( "en" to messagesEn, "pl" to messagesPl ) ) root("kvapp") { splitPanel { height = 100.vh width = 100.perc listPanel() editPanel() } } Model.loadAddresses() } } fun main() { startApplication( ::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, FontAwesomeModule, TabulatorModule, TabulatorCssMaterializeModule, CoreModule ) } ================================================ FILE: addressbook-tabulator/src/jsMain/kotlin/com/example/EditPanel.kt ================================================ package com.example import io.kvision.core.Container import io.kvision.core.onEvent import io.kvision.form.check.CheckBox import io.kvision.form.formPanel import io.kvision.form.text.Text import io.kvision.html.ButtonStyle import io.kvision.html.InputType import io.kvision.html.button import io.kvision.i18n.I18n.tr import io.kvision.panel.hPanel import io.kvision.panel.simplePanel import io.kvision.state.bind import io.kvision.utils.ENTER_KEY import io.kvision.utils.px import kotlinx.browser.window fun Container.editPanel() { simplePanel().bind(Model.addressBook) { state -> padding = 10.px if (state.editMode != null) { val formPanel = formPanel
{ add(Address::firstName, Text(label = tr("First name:"))) add(Address::lastName, Text(label = tr("Last name:"))) add(Address::email, Text(InputType.EMAIL, label = tr("E-mail:"))) add(Address::favourite, CheckBox(label = tr("Mark as favourite"))) hPanel(spacing = 10) { button(tr("Save"), "fas fa-check", ButtonStyle.PRIMARY).onClick { Model.save(this@formPanel.getData()) } button(tr("Cancel"), "fas fa-times", ButtonStyle.SECONDARY).onClick { Model.cancel() } } onEvent { keydown = { e -> if (e.keyCode == ENTER_KEY) { Model.save(this@formPanel.getData()) } } } } if (state.editMode == EditMode.NEW) { formPanel.clearData() } else if (state.editAddress != null) { formPanel.setData(state.editAddress) } window.setTimeout({ formPanel.getControl(Address::firstName)?.focus() }, 0) } else { simplePanel { button(tr("Add new address"), "fas fa-plus", style = ButtonStyle.PRIMARY).onClick { Model.add() } } } } } ================================================ FILE: addressbook-tabulator/src/jsMain/kotlin/com/example/ListPanel.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.Container import io.kvision.core.JustifyContent import io.kvision.core.onEvent import io.kvision.form.check.radioGroup import io.kvision.form.text.text import io.kvision.html.InputType import io.kvision.i18n.I18n.tr import io.kvision.modal.Confirm import io.kvision.panel.hPanel import io.kvision.panel.simplePanel import io.kvision.tabulator.Align import io.kvision.tabulator.ColumnDefinition import io.kvision.tabulator.Formatter import io.kvision.tabulator.Layout import io.kvision.tabulator.RenderType import io.kvision.tabulator.Tabulator import io.kvision.tabulator.TabulatorOptions import io.kvision.tabulator.tabulator import io.kvision.utils.obj import io.kvision.utils.px import kotlinx.serialization.serializer import org.w3c.dom.events.Event fun Container.listPanel() { simplePanel { lateinit var tabulator: Tabulator
hPanel(justify = JustifyContent.SPACEAROUND, alignItems = AlignItems.CENTER) { width = 410.px text(InputType.SEARCH) { placeholder = tr("Search ...") onEvent { input = { Model.setSearch(self.value) tabulator.applyFilter() } } } radioGroup( listOf(Filter.ALL.name to tr("All"), Filter.FAVOURITE.name to tr("Favourites")), Filter.ALL.name, inline = true ).onEvent { change = { Model.setFilter(Filter.valueOf(self.value!!)) tabulator.applyFilter() } } } tabulator = tabulator( Model.addressBook, { it.addresses }, options = TabulatorOptions( renderVertical = RenderType.VIRTUAL, height = "calc(100vh - 90px)", layout = Layout.FITCOLUMNS, columns = listOf( ColumnDefinition(tr("First name"), "firstName"), ColumnDefinition(tr("Last name"), "lastName"), ColumnDefinition(tr("E-mail"), "email", formatterFunction = { cell, _, _ -> cell.getValue()?.let { "$it" } }), ColumnDefinition( "", "favourite", hozAlign = Align.CENTER, width = "40", formatter = Formatter.TICKCROSS, formatterParams = obj { crossElement = false } ), ColumnDefinition( "", hozAlign = Align.CENTER, width = "40", formatter = Formatter.BUTTONCROSS, headerSort = false, cellClick = { evt, cell -> evt.unsafeCast().preventDefault() Confirm.show(tr("Are you sure?"), tr("Do you want to delete this address?")) { Model.delete(cell.getRow().getIndex() as Int) } } ) ), persistenceMode = false ), serializer = serializer() ) { marginBottom = 0.px setEventListener> { rowClickTabulator = { e -> @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") Model.edit((e.detail as io.kvision.tabulator.js.Tabulator.RowComponent).getIndex() as Int) } } setFilter { address -> val state = Model.addressBook.value address.match(state.search) && (state.filter == Filter.ALL || address.favourite ?: false) } } } } ================================================ FILE: addressbook-tabulator/src/jsMain/kotlin/com/example/Model.kt ================================================ package com.example import io.kvision.state.ObservableValue import kotlinx.browser.localStorage import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.w3c.dom.get import org.w3c.dom.set @Serializable data class Address( val id: Int? = null, val firstName: String? = null, val lastName: String? = null, val email: String? = null, val favourite: Boolean? = false ) fun Address.match(search: String?): Boolean { return search?.let { firstName?.contains(it, true) ?: false || lastName?.contains(it, true) ?: false || email?.contains(it, true) ?: false } ?: true } enum class Filter { ALL, FAVOURITE } enum class EditMode { NEW, EDIT } data class AddressBookState( val addresses: List
, val search: String? = null, val filter: Filter = Filter.ALL, val editMode: EditMode? = null, val editAddress: Address? = null ) object Model { private var counter = 0 val addressBook = ObservableValue( AddressBookState( listOf( Address(counter++, "John", "Smith", "john.smith@mail.com", true), Address(counter++, "Karen", "Kowalsky", "kkowalsky@mail.com", true), Address(counter++, "William", "Gordon", "w.gordon@mail.com", false) ) ) ) fun setSearch(search: String?) { addressBook.value = addressBook.value.copy(search = search) } fun setFilter(filter: Filter) { addressBook.value = addressBook.value.copy(filter = filter) } fun add() { addressBook.value = addressBook.value.copy(editMode = EditMode.NEW, editAddress = null) } fun edit(id: Int) { val state = addressBook.value val editAddress = state.addresses.find { it.id == id } if (editAddress != null) { addressBook.value = state.copy(editMode = EditMode.EDIT, editAddress = editAddress) } } fun cancel() { addressBook.value = addressBook.value.copy(editMode = null, editAddress = null) } fun delete(id: Int) { val state = addressBook.value val newAddresses = state.addresses.filter { it.id != id } addressBook.value = if (state.editAddress?.id == id) { state.copy(editMode = null, addresses = newAddresses, editAddress = null) } else { state.copy(addresses = newAddresses) } storeAddresses() } fun save(newAddress: Address) { val state = addressBook.value val newAddresses = if (state.editMode == EditMode.EDIT) { state.addresses.map { if (it.id == state.editAddress?.id) newAddress.copy(id = it.id) else it } } else { state.addresses + newAddress.copy(id = counter++) } addressBook.value = state.copy(addresses = newAddresses, editMode = null, editAddress = null) storeAddresses() } fun storeAddresses() { val jsonString = Json.encodeToString(addressBook.value.addresses) localStorage["addressesTabulator"] = jsonString } fun loadAddresses() { localStorage["addressesTabulator"]?.let { addressesAsString -> addressBook.value = addressBook.value.copy(addresses = Json.decodeFromString(addressesAsString)) counter = (addressBook.value.addresses.maxByOrNull { it.id ?: 0 }?.id ?: 0) + 1 } } } ================================================ FILE: addressbook-tabulator/src/jsMain/resources/index.html ================================================ KVision Address Book
================================================ FILE: addressbook-tabulator/src/jsMain/resources/modules/css/kvapp.css ================================================ ================================================ FILE: addressbook-tabulator/src/jsMain/resources/modules/i18n/messages-en.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: ../src/main/kotlin/com/example/App.kt:20 msgid "This is a localized message." msgstr "" ================================================ FILE: addressbook-tabulator/src/jsMain/resources/modules/i18n/messages-pl.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the KVision package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: KVision\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-18 01:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: Polish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: ../src/main/kotlin/com/example/App.kt:20 msgid "This is a localized message." msgstr "To jest przetłumaczona wiadomość." ================================================ FILE: addressbook-tabulator/src/jsTest/kotlin/test/com/example/AppSpec.kt ================================================ package test.com.example import io.kvision.test.DomSpec import kotlin.test.Test import kotlin.test.assertTrue class AppSpec : DomSpec { @Test fun render() { run { assertTrue(true, "Dummy test") } } } ================================================ FILE: addressbook-tabulator/src/jsTest/resources/css/kvapp.css ================================================ ================================================ FILE: addressbook-tabulator/webpack.config.d/bootstrap.js ================================================ config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, type: 'asset'}); config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, type: 'asset/resource'}); ================================================ FILE: addressbook-tabulator/webpack.config.d/css.js ================================================ config.module.rules.push({ test: /\.css$/, use: ["style-loader", { loader: "css-loader", options: {sourceMap: false} } ] }); ================================================ FILE: addressbook-tabulator/webpack.config.d/file.js ================================================ config.module.rules.push( { test: /\.(jpe?g|png|gif|svg)$/i, type: 'asset/resource' } ); ================================================ FILE: addressbook-tabulator/webpack.config.d/handlebars.js ================================================ config.module.rules.push( { test: /\.hbs$/i, loader: 'handlebars-loader' } ); ================================================ FILE: addressbook-tabulator/webpack.config.d/tailwind.js ================================================ ;(function() { config.module.rules.push({ test: /tailwind\.css$/, use: [ '@tailwindcss/webpack' ] }); })(); ================================================ FILE: addressbook-tabulator/webpack.config.d/webpack.js ================================================ config.resolve.modules.push("kotlin"); if (config.devServer) { config.devServer.client = { overlay: false }; config.devServer.hot = true; config.devServer.open = false; config.devServer.port = 3000; config.devServer.historyApiFallback = true; config.devtool = 'eval-cheap-source-map'; } else { config.devtool = undefined; } // disable bundle size warning config.performance = { assetFilter: function (assetFilename) { return !assetFilename.endsWith('.js'); }, }; ================================================ FILE: desktop/.gettext.json ================================================ { "js": { "parsers": [ { "expression": "tr", "arguments": { "text": 0 } }, { "expression": "ntr", "arguments": { "text": 0, "textPlural": 1 } }, { "expression": "gettext", "arguments": { "text": 0 } }, { "expression": "ngettext", "arguments": { "text": 0, "textPlural": 1 } } ], "glob": { "pattern": "src/jsMain/**/*.kt" } }, "headers": { "Language": "" }, "output": "src/jsMain/resources/modules/i18n/messages.pot" } ================================================ FILE: desktop/.gitignore ================================================ .*/ build/ out/ /refresh.sh *.imp *.ipr *.iws *.idea ================================================ FILE: desktop/README.md ================================================ ## Gradle Tasks ### Resource Processing * generatePotFile - Generates a `src/jsMain/resources/modules/i18n/messages.pot` translation template file. ### Running * run - Starts a webpack dev server on port 3000. ### Packaging * jsBrowserDistribution - Bundles the compiled js files into `build/dist/js/productionExecutable` * zip - Packages a zip archive with all required files into `build/libs/*.zip` ================================================ FILE: desktop/build.gradle.kts ================================================ plugins { val kotlinVersion: String by System.getProperties() kotlin("plugin.serialization") version kotlinVersion kotlin("multiplatform") version kotlinVersion val kvisionVersion: String by System.getProperties() id("io.kvision") version kvisionVersion } version = "1.0.0-SNAPSHOT" group = "com.example" repositories { mavenCentral() mavenLocal() } // Versions val kvisionVersion: String by System.getProperties() kotlin { js(IR) { browser { useEsModules() commonWebpackConfig { outputFileName = "main.bundle.js" sourceMaps = false } testTask { useKarma { useChromeHeadless() } } } binaries.executable() compilerOptions { target.set("es2015") } } sourceSets["jsMain"].dependencies { implementation("io.kvision:kvision:$kvisionVersion") implementation("io.kvision:kvision-bootstrap:$kvisionVersion") implementation("io.kvision:kvision-fontawesome:$kvisionVersion") implementation("io.kvision:kvision-richtext:$kvisionVersion") } sourceSets["jsTest"].dependencies { implementation(kotlin("test-js")) implementation("io.kvision:kvision-testutils:$kvisionVersion") } } ================================================ FILE: desktop/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: desktop/gradle.properties ================================================ #Plugins systemProp.kotlinVersion=2.3.20 #Dependencies systemProp.kvisionVersion=9.5.0 org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true ================================================ FILE: desktop/gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: desktop/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: desktop/settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() mavenLocal() } } rootProject.name = "desktop" ================================================ FILE: desktop/src/jsMain/kotlin/com/example/App.kt ================================================ package com.example import io.kvision.Application import io.kvision.BootstrapCssModule import io.kvision.BootstrapModule import io.kvision.CoreModule import io.kvision.FontAwesomeModule import io.kvision.Hot import io.kvision.core.Border import io.kvision.core.BorderStyle import io.kvision.core.Component import io.kvision.core.FlexDirection import io.kvision.core.FlexWrap import io.kvision.core.onEvent import io.kvision.dropdown.ddLink import io.kvision.dropdown.dropDown import io.kvision.dropdown.separator import io.kvision.html.Link import io.kvision.modal.Alert import io.kvision.navbar.Nav import io.kvision.navbar.NavbarType import io.kvision.navbar.nav import io.kvision.navbar.navbar import io.kvision.panel.flexPanel import io.kvision.panel.root import io.kvision.startApplication import io.kvision.utils.px import io.kvision.utils.useModule import io.kvision.utils.vh import kotlinx.browser.document @JsModule("./modules/css/kvapp.css") external object kvappCss class App : Application() { init { useModule(kvappCss) } override fun start() { root("kvapp") { navbar(type = NavbarType.FIXEDTOP) { nav { dropDown("Menu", icon = "fab fa-windows", forNavbar = true, arrowVisible = false) { ddLink("Calculator", "#", icon = "fas fa-calculator").onClick { Calculator.run(this@root) } ddLink("Text Editor", "#", icon = "fas fa-edit").onClick { TextEditor.run(this@root) } ddLink("Paint", "#", icon = "fas fa-paint-brush").onClick { Paint.run(this@root) } ddLink("Web Browser", "#", icon = "fab fa-firefox").onClick { WebBrowser.run(this@root) } separator() ddLink("About", "#", icon = "fas fa-info-circle").onClick { Alert.show("KVision Desktop", "KVision example application.") } ddLink("Shutdown", "#", icon = "fas fa-power-off").onClick { document.location?.reload() } } } taskBar = nav() } flexPanel(FlexDirection.COLUMN, FlexWrap.WRAP, spacing = 20) { padding = 20.px paddingTop = 70.px height = 100.vh add(DesktopIcon("fas fa-calculator", "Calculator").apply { onEvent { dblclick = { Calculator.run(this@root) } touchstart = { Calculator.run(this@root) } } }) add(DesktopIcon("fas fa-edit", "Text Editor").apply { onEvent { dblclick = { TextEditor.run(this@root) } touchstart = { TextEditor.run(this@root) } } }) add(DesktopIcon("fas fa-paint-brush", "Paint").apply { onEvent { dblclick = { Paint.run(this@root) } touchstart = { Paint.run(this@root) } } }) add(DesktopIcon("fab fa-firefox", "Web Browser").apply { onEvent { dblclick = { WebBrowser.run(this@root) } touchstart = { WebBrowser.run(this@root) } } }) } } } companion object { lateinit var taskBar: Nav fun addTask(window: DesktopWindow): Component { val task = Link(window.caption ?: "Window", icon = window.icon, className = "nav-item nav-link") { border = Border(1.px, BorderStyle.SOLID) marginLeft = 5.px onClick { if (window.minimized) window.toggleMinimize() window.toFront() window.focus() } } taskBar.add(task) return task } fun removeTask(task: Component) { taskBar.remove(task) task.dispose() } } } fun main() { startApplication( ::App, js("import.meta.webpackHot").unsafeCast(), BootstrapModule, BootstrapCssModule, FontAwesomeModule, CoreModule ) } ================================================ FILE: desktop/src/jsMain/kotlin/com/example/Calculator.kt ================================================ package com.example import io.kvision.core.Border import io.kvision.core.BorderStyle import io.kvision.core.Container import io.kvision.core.JustifyItems import io.kvision.html.Align import io.kvision.html.Button import io.kvision.html.ButtonStyle import io.kvision.html.Div import io.kvision.html.div import io.kvision.panel.gridPanel import io.kvision.utils.px enum class Operator { PLUS, MINUS, DIVIDE, MULTIPLY } class Calculator : DesktopWindow("Calculator", "fas fa-calculator", 280, 290) { val inputDiv: Div var input: String = "0" var cleared = true var isDivider = false var first: Double = 0.0 var operator: Operator? = null init { isResizable = false maximizeButton = false minimizeButton = false inputDiv = div("0", align = Align.RIGHT) { padding = 5.px marginTop = 15.px marginLeft = 15.px marginRight = 15.px border = Border(2.px, BorderStyle.SOLID) } gridPanel(columnGap = 5, rowGap = 5, justifyItems = JustifyItems.CENTER) { padding = 10.px add(CalcButton("AC").apply { onClick { this@Calculator.clear() } }, 4, 1) add(CalcButton("7").apply { onClick { this@Calculator.number(7) } }, 1, 2) add(CalcButton("8").apply { onClick { this@Calculator.number(8) } }, 2, 2) add(CalcButton("9").apply { onClick { this@Calculator.number(9) } }, 3, 2) add(CalcButton("4").apply { onClick { this@Calculator.number(4) } }, 1, 3) add(CalcButton("5").apply { onClick { this@Calculator.number(5) } }, 2, 3) add(CalcButton("6").apply { onClick { this@Calculator.number(6) } }, 3, 3) add(CalcButton("1").apply { onClick { this@Calculator.number(1) } }, 1, 4) add(CalcButton("2").apply { onClick { this@Calculator.number(2) } }, 2, 4) add(CalcButton("3").apply { onClick { this@Calculator.number(3) } }, 3, 4) add(CalcButton("0").apply { onClick { this@Calculator.number(0) } }, 1, 5) add(CalcButton(".").apply { onClick { this@Calculator.divider() } }, 2, 5) add(CalcButton("=").apply { onClick { this@Calculator.calculate() } }, 3, 5) add(CalcButton("/").apply { onClick { this@Calculator.operator(Operator.DIVIDE) } }, 4, 2) add(CalcButton("*").apply { onClick { this@Calculator.operator(Operator.MULTIPLY) } }, 4, 3) add(CalcButton("-").apply { onClick { this@Calculator.operator(Operator.MINUS) } }, 4, 4) add(CalcButton("+").apply { onClick { this@Calculator.operator(Operator.PLUS) } }, 4, 5) } } private fun clear() { input = "0" cleared = true isDivider = false first = 0.0 operator = null printInput() } private fun number(num: Int) { if (input == "0" || cleared) { input = "$num" } else { input += "$num" } cleared = false printInput() } private fun divider() { if (!isDivider) { if (input == "0" || cleared) { input = "0." } else { input += "." } isDivider = true } cleared = false printInput() } private fun operator(op: Operator) { if (operator != null) { calculate() } first = input.toDouble() operator = op cleared = true isDivider = false } private fun calculate() { val second = input.toDouble() val result = when (operator) { Operator.PLUS -> first + second Operator.MINUS -> first - second Operator.MULTIPLY -> first * second Operator.DIVIDE -> first / second else -> input.toDouble() } input = result.toString() printInput() cleared = true operator = null isDivider = false } private fun printInput() { inputDiv.content = "$input" } companion object { fun run(container: Container) { container.add(Calculator()) } } } class CalcButton(label: String) : Button(label, style = ButtonStyle.SECONDARY) { init { width = 50.px } } ================================================ FILE: desktop/src/jsMain/kotlin/com/example/DesktopIcon.kt ================================================ package com.example import io.kvision.core.AlignItems import io.kvision.core.WhiteSpace import io.kvision.html.icon import io.kvision.html.span import io.kvision.panel.VPanel import io.kvision.utils.px class DesktopIcon(icon: String, content: String) : VPanel(alignItems = AlignItems.CENTER) { init { width = 64.px height = 64.px icon(icon) { addCssClass("fa-3x") } span(content) { whiteSpace = WhiteSpace.NOWRAP } } } ================================================ FILE: desktop/src/jsMain/kotlin/com/example/DesktopWindow.kt ================================================ package com.example import com.example.App.Companion.addTask import com.example.App.Companion.removeTask import io.kvision.core.Component import io.kvision.core.CssSize import io.kvision.core.UNIT import io.kvision.core.Widget import io.kvision.utils.px import io.kvision.utils.vh import io.kvision.utils.vw import io.kvision.window.Window import kotlin.random.Random open class DesktopWindow(caption: String, icon: String, width: Int, height: Int) : Window( caption, width.px, height.px, closeButton = true, maximizeButton = true, minimizeButton = true, icon = icon ) { override var top: CssSize? get() = super.top set(value) { if (maximized || value?.first?.toInt() ?: 0 > 50 && value?.second == UNIT.px) { super.top = value } } val task: Component var storedWidth: CssSize? = null var storedHeight: CssSize? = null var storedTop: CssSize? = null var storedLeft: CssSize? = null var maximized: Boolean = false var minimized: Boolean = false init { left = ((Random.nextDouble() * 800).toInt()).px top = (51 + (Random.nextDouble() * 100).toInt()).px task = addTask(this) } override fun hide() { super.hide() removeTask(task) this.dispose() } override fun toggleMaximize() { if (!maximized) { maximized = true storedWidth = width storedHeight = height storedTop = top storedLeft = left top = 0.px left = 0.px height = 100.vh width = 100.vw height = 100.vh zIndex = zIndex?.plus(10000) } else { width = storedWidth height = storedHeight top = storedTop left = storedLeft maximized = false zIndex = zIndex?.minus(10000) } } override fun toggleMinimize() { if (!minimized) { visible = false minimized = true } else { visible = true minimized = false } } } ================================================ FILE: desktop/src/jsMain/kotlin/com/example/Paint.kt ================================================ package com.example import org.w3c.dom.CanvasRenderingContext2D import io.kvision.core.AlignItems import io.kvision.core.Background import io.kvision.core.Border import io.kvision.core.BorderStyle import io.kvision.core.Col import io.kvision.core.Color import io.kvision.core.Container import io.kvision.core.onEvent import io.kvision.html.Button import io.kvision.html.ButtonStyle import io.kvision.html.Canvas import io.kvision.html.button import io.kvision.html.icon import io.kvision.modal.Confirm import io.kvision.panel.Side import io.kvision.panel.VPanel import io.kvision.panel.dockPanel import io.kvision.panel.hPanel import io.kvision.utils.perc import io.kvision.utils.px import org.w3c.dom.TouchEvent import org.w3c.dom.get import kotlin.math.PI import kotlin.math.abs val colorTable = arrayOf(Col.WHITE, Col.BLACK, Col.RED, Col.GREEN, Col.BLUE, Col.YELLOW) class Paint : DesktopWindow("Paint", "fas fa-paint-brush", 700, 400) { lateinit var buttonPoint: Button lateinit var buttonPencil: Button lateinit var buttonLine: Button lateinit var buttonRectangle: Button lateinit var buttonCircle: Button var lineColorButtons = mutableListOf