Initial Commit für das Grundgerüst der Anwendung, bestehend aus der Dev-Container Konfiguration, der Docker und docker-Compose Definitionen sowie der Verzeichnisse App_Server für die Backend-Programme sowie Client für die browserseitigen GUI-Komponenten
This commit is contained in:
1 parent
10549934ea
commit
1ba78992a6
63 files changed
+5410
No files matched your search
@@ -0,0 +1,616 @@
|
||||
import m from "mithril"
|
||||
import { Treetable, iTreetable, iTreenode, Treenode, TopbarButton, iTopbarButton, changeTreenodes } from "../widgets/Treetable_V2"
|
||||
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
|
||||
import { iApplication} from "./models/applications"
|
||||
import { iRole } from "./models/roles"
|
||||
import {gridMountpoints} from "../mainlayout"
|
||||
import {WrongEditSelectionMessage, iWrongEditSelectionMessage, showServerError} from "./authmessages"
|
||||
|
||||
const MOVERIGHT = "Die markierten und nicht markierten Rollen werden den markierten Anwendungen zugeordnet"
|
||||
|
||||
const localDataModel = {
|
||||
applist: [] as iApplication[],
|
||||
rolelist: [] as iRole[],
|
||||
appnodes: [] as iTreenode[],
|
||||
rolenodes: [] as iTreenode[],
|
||||
loadData: () => {
|
||||
Promise.all([
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/getapplications"
|
||||
}).then((result) => localDataModel.applist = result as iApplication[]),
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/getroles"
|
||||
}).then((result) => {localDataModel.rolelist = result as iRole[]})
|
||||
])
|
||||
.then (() => {
|
||||
localDataModel.appnodes = []
|
||||
localDataModel.rolenodes = []
|
||||
localDataModel.applist.map((app: iApplication) => {
|
||||
let appnode: iTreenode = new Treenode(app, false, [])
|
||||
app.roles.forEach((role: iRole) => {
|
||||
let newChildnode = new Treenode(role)
|
||||
newChildnode.isChecked = true
|
||||
appnode.children.push(newChildnode)
|
||||
})
|
||||
appnode.children.sort(sortRoleNodes)
|
||||
localDataModel.appnodes.push(appnode)
|
||||
})
|
||||
localDataModel.rolelist.map((role: iRole) => {
|
||||
let rolenode: iTreenode = new Treenode(role, false, [])
|
||||
localDataModel.rolenodes.push(rolenode)
|
||||
})
|
||||
localDataModel.rolenodes.sort(sortRoleNodes)
|
||||
changeTreenodes(localDataModel.appnodes, true)
|
||||
})
|
||||
},
|
||||
updateRole: (role: iRole) => {
|
||||
m.request ({
|
||||
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
|
||||
url: "/auth/updaterole",
|
||||
body: role
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
deleteRoles: (roles: iRole[]) => {
|
||||
let roleids: string[] = []
|
||||
roles.forEach((role: iRole) => {
|
||||
roleids.push(role.roleid)
|
||||
})
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/auth/deleteroles",
|
||||
body: roleids
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
updateApplication: (app: iApplication) => {
|
||||
m.request ({
|
||||
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
|
||||
url: "/auth/updateapplication",
|
||||
body: app
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
deleteApplications: (apps: iApplication[]) => {
|
||||
let appids: string[] = []
|
||||
apps.forEach((app: iApplication) => {
|
||||
appids.push(app.appid)
|
||||
})
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/auth/deleteapplications",
|
||||
body: appids
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
updateApplicationRoles: () => {
|
||||
interface iApplicationRole {
|
||||
appid: string
|
||||
roles: {roleid: string, isChecked: boolean}[]
|
||||
}
|
||||
let applicationroles: iApplicationRole[] = []
|
||||
localDataModel.appnodes.forEach((appnode: iTreenode) => {
|
||||
let app: iApplication = appnode.nodevalue
|
||||
let approle: iApplicationRole = {
|
||||
appid: app.appid,
|
||||
roles: []
|
||||
}
|
||||
appnode.children.forEach((rolenode: iTreenode) => {
|
||||
let role: iRole = rolenode.nodevalue
|
||||
approle.roles.push({roleid: role.roleid, isChecked: rolenode.isChecked})
|
||||
})
|
||||
applicationroles.push(approle)
|
||||
})
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/auth/updateapplicationroles",
|
||||
body: applicationroles
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
}
|
||||
}
|
||||
|
||||
export const applicationpage: m.ClosureComponent = (vnode) => {
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
localDataModel.loadData()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-content", {
|
||||
style: {'max-width': '80%', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}
|
||||
},
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m("div.w3-cell-row",
|
||||
m("div.w3-cell.w3-padding.w3-mobile",
|
||||
m("h1", "Rollen"),
|
||||
m(Treetable,
|
||||
{
|
||||
treenodes: localDataModel.rolenodes,
|
||||
startexpanded: false,
|
||||
onRowMarked: (node: iTreenode) => {
|
||||
//alert(node.nodevalue['roleid'])
|
||||
},
|
||||
columnmapper: (node: iTreenode) => {
|
||||
let role = node.nodevalue as iRole
|
||||
return [
|
||||
m("span.w3-button",
|
||||
{
|
||||
style: {padding: '0px'},
|
||||
onclick: () => {
|
||||
node.isChecked = !node.isChecked
|
||||
}
|
||||
},
|
||||
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
|
||||
),
|
||||
m("div", role.level),
|
||||
m("div", role.roleid),
|
||||
m("div", role.name)
|
||||
]
|
||||
}
|
||||
} as iTreetable,
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: applyRoles,
|
||||
tooltip: MOVERIGHT
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "keyboard_double_arrow_right")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {m.mount(gridMountpoints.modalhook, {
|
||||
view: (vnode) => {
|
||||
return m(RoleForm, {role: {id: -1, level: 0, name: "", roleid: ""} as iRole})
|
||||
}
|
||||
})}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "add")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteRolesMessage)}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "delete_forever")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {
|
||||
let roles: iRole[] = []
|
||||
localDataModel.rolenodes.forEach((rolenode: Treenode) => {
|
||||
if (rolenode.isChecked) {
|
||||
roles.push(rolenode.nodevalue)
|
||||
}
|
||||
})
|
||||
if (roles.length != 1) {
|
||||
m.mount(gridMountpoints.modalhook, WrongRoleEditSelectionMessage)
|
||||
}
|
||||
else {
|
||||
m.mount(gridMountpoints.modalhook, {
|
||||
view: (vnode) => {
|
||||
return m(RoleForm, {
|
||||
role: roles[0] as iRole
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "edit")
|
||||
)
|
||||
)
|
||||
),
|
||||
m("div.w3-cell.w3-padding.w3-mobile",
|
||||
m("h1", "Anwendungen"),
|
||||
m(Treetable,
|
||||
{
|
||||
treenodes: localDataModel.appnodes,
|
||||
startexpanded: true,
|
||||
columnmapper: (node: iTreenode) => {
|
||||
let cols: any[] = []
|
||||
// Hier wird "Duck-Typing" genutzt, um den Typ von node.nodevalue zu ermitteln
|
||||
// Je nach Typ werden die Spalten der Tabelle dann anders befüllt
|
||||
if (node.nodevalue['appid'] != undefined) {
|
||||
let app = node.nodevalue as iApplication
|
||||
cols[0] = "enabled"
|
||||
cols[1] = app.appid
|
||||
cols[2] = app.name
|
||||
}
|
||||
else if (node.nodevalue['roleid'] != undefined) {
|
||||
let role = node.nodevalue as iRole
|
||||
cols[0] = "disabled"
|
||||
cols[1] = role.roleid
|
||||
cols[2] = role.name
|
||||
}
|
||||
|
||||
return [
|
||||
m(cols[0] == "enabled"?"span.w3-button":"span",
|
||||
{
|
||||
style: {padding: '0px'},
|
||||
onclick: () => {
|
||||
if (cols[0] == "enabled") {
|
||||
node.isChecked = !node.isChecked
|
||||
}
|
||||
}
|
||||
},
|
||||
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
|
||||
),
|
||||
m("div", cols[1]),
|
||||
m("div", cols[2])
|
||||
]
|
||||
|
||||
}
|
||||
} as iTreetable,
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: applyRoles,
|
||||
tooltip: MOVERIGHT
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "keyboard_double_arrow_right")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {m.mount(gridMountpoints.modalhook,
|
||||
{
|
||||
view: (vnode) => {
|
||||
return m(ApplicationForm, {application: {id: -1} as iApplication} as iApplicationForm)
|
||||
}
|
||||
}
|
||||
)}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "add")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteApplicatonsMessage)}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "delete_forever")
|
||||
),
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: () => {
|
||||
let apps: iApplication[] = []
|
||||
localDataModel.appnodes.forEach((appnode: Treenode) => {
|
||||
if (appnode.isChecked) {
|
||||
apps.push(appnode.nodevalue)
|
||||
}
|
||||
})
|
||||
if (apps.length != 1) {
|
||||
m.mount(gridMountpoints.modalhook, WrongApplicationEditSelectionMessage)
|
||||
}
|
||||
else {
|
||||
m.mount(gridMountpoints.modalhook, {
|
||||
view: (vnode) => {
|
||||
return m(ApplicationForm, {
|
||||
application: apps[0] as iApplication
|
||||
} as iApplicationForm)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "edit")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortRoleNodes(node1: iTreenode, node2: iTreenode) {
|
||||
// es soll zuerst nach Level und dann nach rowid sortiert werden
|
||||
let v1 = (node1.nodevalue['level'].toString().padStart(6,'0') + node1.nodevalue['roleid']).toLocaleLowerCase('de-de')
|
||||
let v2 = (node2.nodevalue['level'].toString().padStart(6,'0') + node2.nodevalue['roleid']).toLocaleLowerCase('de-de')
|
||||
if (v1 < v2) {
|
||||
return -1
|
||||
}
|
||||
if (v1 > v2) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function applyRoles() {
|
||||
let checkedApps = localDataModel.appnodes.filter((appnode: iTreenode) => {
|
||||
return appnode.isChecked
|
||||
})
|
||||
if (checkedApps.length == 0) {
|
||||
m.mount(gridMountpoints.modalhook, ErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
m.mount(gridMountpoints.modalhook, {view: () => {return m(ApplyRolesWarning, {applyroles: doit})}})
|
||||
|
||||
function doit() {
|
||||
checkedApps.forEach((appnode: iTreenode) => {
|
||||
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
|
||||
let approle: iTreenode = appnode.children.find((child: iTreenode) => {
|
||||
return child.nodevalue['roleid'] === rolenode.nodevalue['roleid']
|
||||
})
|
||||
if (approle != undefined) {
|
||||
approle.isChecked = rolenode.isChecked
|
||||
}
|
||||
else if (rolenode.isChecked) {
|
||||
let newapprole = new Treenode(rolenode.nodevalue, false, [])
|
||||
newapprole.isChecked = rolenode.isChecked
|
||||
appnode.children.push(newapprole)
|
||||
}
|
||||
})
|
||||
})
|
||||
localDataModel.updateApplicationRoles()
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorMessage: m.Component = {
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Fehler",
|
||||
type: ModalType.ERROR,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p", "Es wurden noch keine Ziel-Anwendungen markiert!"),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export interface iApplyRolesWaring {
|
||||
applyroles: Function
|
||||
}
|
||||
const ApplyRolesWarning: m.Component<iApplyRolesWaring> = {
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Hinweis",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p", "Wenn Rollen nicht ausgewählt sind bedeutet dies, dass die Rollen in den ausgewählten Anwendungen gelöscht werden!"),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbruch"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null); vnode.attrs.applyroles()}}, "OK"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const WrongRoleEditSelectionMessage: m.Component = {
|
||||
view: () => {
|
||||
return m(WrongEditSelectionMessage, {elementname: "Rolle"} as iWrongEditSelectionMessage)
|
||||
}
|
||||
}
|
||||
const WrongApplicationEditSelectionMessage: m.Component = {
|
||||
view: () => {
|
||||
return m(WrongEditSelectionMessage, {elementname: "Anwendung"} as iWrongEditSelectionMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteRolesMessage: m.ClosureComponent = (vnode) => {
|
||||
let roles: iRole[]
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
roles = []
|
||||
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
|
||||
if (rolenode.isChecked) {
|
||||
roles.push(rolenode.nodevalue)
|
||||
}
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Rollen löschen",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p", "Sie wollen die folgenden Rollen löschen:"),
|
||||
m("ul",
|
||||
roles.map((role: iRole) => {
|
||||
return m("li", role.roleid)
|
||||
})
|
||||
),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right",
|
||||
{
|
||||
style: {'margin-left': '4px'},
|
||||
onclick: () => {
|
||||
localDataModel.deleteRoles(roles);
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}}, "Löschen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface iRoleForm {
|
||||
role: iRole
|
||||
}
|
||||
const RoleForm: m.ClosureComponent<iRoleForm> = () => {
|
||||
let form: HTMLFormElement = null;
|
||||
let currentrole: iRole
|
||||
function reset (e) {
|
||||
form.reset()
|
||||
}
|
||||
function unmount () {
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}
|
||||
function submit (e) {
|
||||
e.preventDefault()
|
||||
let formdata: FormData = new FormData(e.target as HTMLFormElement)
|
||||
|
||||
for (let element of formdata) {
|
||||
currentrole[element[0]] = element[1]
|
||||
}
|
||||
localDataModel.updateRole(currentrole)
|
||||
unmount()
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
currentrole = (vnode.attrs.role === undefined) ? {} as iRole : vnode.attrs.role
|
||||
return m(ModalView,
|
||||
{
|
||||
title: "Rolle",
|
||||
type: ModalType.MESSAGE,
|
||||
width: 'max-content'
|
||||
} as iModalView,
|
||||
|
||||
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
|
||||
m("label", "ID (intern)"),
|
||||
m("div.w3-padding-small", currentrole.id),
|
||||
m("label", "Rollen-ID"),
|
||||
m("input.w3-input.w3-border", {name: 'roleid', type: 'text', value: currentrole.roleid}),
|
||||
m("label", "Name"),
|
||||
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentrole.name}),
|
||||
m("label", "Level"),
|
||||
m("input.w3-input.w3-border", {name: 'level', type: 'number', value: currentrole.level}),
|
||||
m("label", "Beschreibung"),
|
||||
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentrole.beschreibung}),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
|
||||
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface iApplicationForm {
|
||||
application: iApplication
|
||||
}
|
||||
const ApplicationForm: m.ClosureComponent<iApplicationForm> = () => {
|
||||
let form: HTMLFormElement = null;
|
||||
let currentapp: iApplication
|
||||
let isDirty = false
|
||||
function reset (e) {
|
||||
form.reset()
|
||||
}
|
||||
function unmount () {
|
||||
if (isDirty) {
|
||||
alert("is Dirty")
|
||||
}
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}
|
||||
function submit (e) {
|
||||
e.preventDefault()
|
||||
let formdata: FormData = new FormData(e.target as HTMLFormElement)
|
||||
|
||||
for (let element of formdata) {
|
||||
currentapp[element[0]] = element[1]
|
||||
}
|
||||
localDataModel.updateApplication(currentapp)
|
||||
unmount()
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
currentapp = (vnode.attrs.application === undefined) ? {} as iApplication : vnode.attrs.application
|
||||
return m(ModalView,
|
||||
{
|
||||
title: "Anwendung",
|
||||
type: ModalType.MESSAGE,
|
||||
width: 'max-content'
|
||||
} as iModalView,
|
||||
|
||||
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
|
||||
m("label", "ID (intern)"),
|
||||
m("div.w3-padding-small", currentapp.id),
|
||||
m("label", "Application-ID"),
|
||||
m("input.w3-input.w3-border", {name: 'appid', type: 'text', value: currentapp.appid}),
|
||||
m("label", "Name"),
|
||||
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentapp.name}),
|
||||
m("label", "Minimum-Version"),
|
||||
m("input.w3-input.w3-border", {name: 'minversion', type: 'text', value: currentapp.minversion}),
|
||||
m("label", "Beschreibung"),
|
||||
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentapp.beschreibung}),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
|
||||
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteApplicatonsMessage: m.ClosureComponent = (vnode) => {
|
||||
let apps: iApplication[]
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
apps = []
|
||||
localDataModel.appnodes.forEach((appnode: iTreenode) => {
|
||||
if (appnode.isChecked) {
|
||||
apps.push(appnode.nodevalue)
|
||||
}
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Anwendungen löschen",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p.w3-panel.w3-red",
|
||||
[
|
||||
"Vor dem Löschen muss sichergestellt sein, dass es noch mindestens eine App gibt, in der die Rolle 'SysAdmin' ",
|
||||
"vergeben ist und in der ein Benutzer diese Rolle besitzt!"
|
||||
]
|
||||
),
|
||||
m("p", "Sie wollen die folgenden Anwendungen löschen:"),
|
||||
m("ul",
|
||||
apps.map((app: iApplication) => {
|
||||
return m("li", app.appid)
|
||||
})
|
||||
),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right",
|
||||
{
|
||||
style: {'margin-left': '4px'},
|
||||
onclick: () => {
|
||||
localDataModel.deleteApplications(apps);
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}}, "Löschen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import m from "mithril"
|
||||
import { get_menustack, menubar } from "../header"
|
||||
import { userlist } from "./userpage"
|
||||
import { applicationpage } from "./applicationpage"
|
||||
|
||||
let mountpoint = null
|
||||
|
||||
function close_page() {
|
||||
get_menustack().pop()
|
||||
m.route.set("/blankpage")
|
||||
}
|
||||
function show_userlist() {
|
||||
m.mount(mountpoint, userlist)
|
||||
}
|
||||
function show_applist() {
|
||||
m.mount(mountpoint, applicationpage)
|
||||
}
|
||||
|
||||
export const appmanagementpage: m.Component = {
|
||||
oncreate: (vnode) => {
|
||||
mountpoint = vnode.dom
|
||||
},
|
||||
oninit: (vnode) => {
|
||||
get_menustack().push(menu)
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div", m(userlist))
|
||||
}
|
||||
}
|
||||
|
||||
const menu: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m(menubar, [
|
||||
m("a.w3-button", {onclick: show_userlist}, "Benutzer"),
|
||||
sessionStorage.getItem("CurrentUserIsSysAdmin") != null ?
|
||||
m("div.w3-button", {onclick: show_applist}, "Anwendungen"):null,
|
||||
m("div.w3-button.w3-right", {onclick: close_page}, "Beenden"),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import m from "mithril"
|
||||
import { gridMountpoints } from "../mainlayout"
|
||||
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
|
||||
|
||||
|
||||
export interface iWrongEditSelectionMessage{
|
||||
elementname: string
|
||||
}
|
||||
export const WrongEditSelectionMessage: m.Component<iWrongEditSelectionMessage> = {
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Falsche Auswahl",
|
||||
type: ModalType.ERROR,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p", `Zum Editieren muss genau 1 ${vnode.attrs.elementname} ausgewählt sein!`),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export interface iServerErrorMessage {
|
||||
error: {
|
||||
messages: string[],
|
||||
status: number
|
||||
}
|
||||
}
|
||||
export const ServerErrorMessage: m.Component<iServerErrorMessage> = {
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Fehler",
|
||||
type: ModalType.ERROR,
|
||||
width: "33%"
|
||||
} as iModalView,
|
||||
m("div.w3-panel",
|
||||
vnode.attrs.error.messages.map((message) => {
|
||||
return m("div", message)
|
||||
}),
|
||||
m("div.w3-small", `(HTML-Status: ${vnode.attrs.error.status})`)
|
||||
),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function showServerError (e) {
|
||||
let servererror:iServerErrorMessage = {error: {messages:[], status: e.code}}
|
||||
console.log(JSON.stringify(e), e.message)
|
||||
if (e.response != undefined && e.response["messages"] != undefined) {
|
||||
if (!Array.isArray(e.response["messages"])) {
|
||||
e.response["messages"] = [e.response["messages"]]
|
||||
}
|
||||
servererror.error.messages = e.response.messages
|
||||
}
|
||||
else if (e.response != undefined) {
|
||||
servererror.error.messages = [JSON.stringify(e.response)]
|
||||
}
|
||||
else {
|
||||
servererror.error.messages = [JSON.stringify(e)]
|
||||
}
|
||||
m.mount(gridMountpoints.modalhook, {
|
||||
view: () => {return m(ServerErrorMessage, {error: servererror.error } as iServerErrorMessage)}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import m from "mithril";
|
||||
import {ModalView, iModalView} from "../widgets/Modal"
|
||||
import {iUser} from "./models/users"
|
||||
import {showServerError} from "./authmessages"
|
||||
import { headerdata } from "../header";
|
||||
|
||||
export interface iLoginscreen {
|
||||
setnextpage: string
|
||||
}
|
||||
export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
|
||||
let form: HTMLFormElement = null;
|
||||
function reset (e) {
|
||||
form.reset()
|
||||
}
|
||||
function checklogin(e: Event) {
|
||||
e.preventDefault()
|
||||
let formdata: FormData = new FormData(e.target as HTMLFormElement)
|
||||
let user: iUser
|
||||
m.request ({
|
||||
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
|
||||
url: "/auth/checklogin",
|
||||
body: Object.fromEntries(formdata.entries())
|
||||
})
|
||||
.then((result) => {
|
||||
user = result as iUser;
|
||||
// global.appglobals.currentuser = user.username
|
||||
// global.appglobals.currentuserid = user.userid
|
||||
sessionStorage.setItem("CurrentUser", user.username);
|
||||
headerdata.username = user.username
|
||||
headerdata.userid = user.userid
|
||||
// nur wenn es sich um einen SysAdmin handelt, wird der SessionStorage gesetzt. Will man später wissen,
|
||||
// ob der aktuelle User ein SysAdmin ist, braucht man nur auf das Vorhandensein des Schlüssels abzufragen
|
||||
// und nicht auch noch auf den Inhalt
|
||||
if (user.isSysAdmin) {
|
||||
sessionStorage.setItem("CurrentUserIsSysAdmin", "1");
|
||||
}
|
||||
})
|
||||
.then(() => {m.route.set(vnode.attrs.setnextpage)})
|
||||
.catch((error) => {
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
showServerError(error)
|
||||
})
|
||||
|
||||
}
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/logout"
|
||||
})
|
||||
.then (() => {
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
// global.appglobals.currentuser = "nicht angemeldet"
|
||||
// global.appglobals.currentuserid = ""
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m(ModalView, {title: "Login", width: '33%'} as iModalView,
|
||||
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: checklogin},
|
||||
m("label", "Benutzer-ID"),
|
||||
m("input.w3-input.w3-border", {name: 'userid', type: 'text'}),
|
||||
m("label", "Kennwort"),
|
||||
m("input.w3-input.w3-border", {name: 'password', type: 'password'}),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Login"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const changepasswordscreen: m.ClosureComponent = () => {
|
||||
let form: HTMLFormElement = null;
|
||||
function reset (e) {
|
||||
form.reset()
|
||||
}
|
||||
function changepassword(e) {
|
||||
e.preventDefault()
|
||||
let formdata: FormData = new FormData(e.target as HTMLFormElement)
|
||||
m.request ({
|
||||
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
|
||||
url: "/auth/changepassword",
|
||||
body: Object.fromEntries(formdata.entries())
|
||||
})
|
||||
.then(() => {m.route.set("/login")})
|
||||
.catch((e) => {showServerError(e)})
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m(ModalView, {title: "Kennwort ändern", width: '33%'} as iModalView,
|
||||
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: changepassword},
|
||||
m("label", "Benutzer-ID"),
|
||||
m("input.w3-input.w3-border", {name: 'Userid', type: 'text', value: headerdata.userid, readonly: true}),
|
||||
m("label", "Altes Kennwort"),
|
||||
m("input.w3-input.w3-border", {name: 'OldPassword', type: 'password'}),
|
||||
m("label", "Neues Kennwort"),
|
||||
m("input.w3-input.w3-border", {name: 'NewPassword1', type: 'password'}),
|
||||
m("label", "Neues Kennwort"),
|
||||
m("input.w3-input.w3-border", {name: 'NewPassword2', type: 'password'}),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Kennwort ändern"),
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: reset, type: "button"}, "Zurücksetzen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {history.back()}, type: "button"}, "Abbrechen"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {iRole} from "./roles"
|
||||
|
||||
export interface iApplication {
|
||||
id: number
|
||||
appid: string
|
||||
name: string
|
||||
minversion: string
|
||||
beschreibung: string
|
||||
roles: iRole[]
|
||||
}
|
||||
|
||||
export interface iUserApp {
|
||||
id: number
|
||||
userid: number
|
||||
appid: number
|
||||
userapp_roles : iRole[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface iRole {
|
||||
id: number
|
||||
roleid: string
|
||||
name: string
|
||||
level: number
|
||||
beschreibung: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { iUserApp } from "./applications"
|
||||
import { iRole } from "./roles"
|
||||
|
||||
export interface iUser {
|
||||
id: number
|
||||
userid: string
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
telefon: string
|
||||
isSysAdmin: boolean
|
||||
userapps: iUserApp[]
|
||||
}
|
||||
|
||||
export function hasAppRole(user: iUser, appid: number, roleid: number) {
|
||||
let userapp: iUserApp = user.userapps.find((userapp: iUserApp) => {return userapp.appid == appid})
|
||||
if (userapp != undefined) {
|
||||
let userapprole: iRole = userapp.userapp_roles.find((role: iRole) => {return role.id == roleid})
|
||||
if (userapprole != undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import m from "mithril"
|
||||
import {hasAppRole, iUser} from "./models/users"
|
||||
import {iApplication} from "./models/applications"
|
||||
import {iRole} from "./models/roles"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "../widgets/Treetable_V2"
|
||||
import {gridMountpoints, maingrid} from "../mainlayout"
|
||||
import {ModalView, ModalType, iModalView} from "../widgets/Modal"
|
||||
import { iWrongEditSelectionMessage, WrongEditSelectionMessage, showServerError} from "./authmessages"
|
||||
|
||||
|
||||
interface iTabelrow {
|
||||
rowtype: any
|
||||
col1: any
|
||||
cols: any[]
|
||||
info?: {
|
||||
parentuserid: number
|
||||
parentappid: number
|
||||
roleid: number
|
||||
ischecked: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const localDataModel = {
|
||||
userlist: [] as iUser[],
|
||||
applist: [] as iApplication[],
|
||||
usernodes: [] as iTreenode[],
|
||||
startexpanded: false,
|
||||
loadData: () => {
|
||||
Promise.all([
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/getusers"
|
||||
}).then((result) => localDataModel.userlist = result as iUser[]),
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/getapplications"
|
||||
}).then((result) => localDataModel.applist = result as iApplication[]),
|
||||
])
|
||||
.then(() => {
|
||||
localDataModel.usernodes = []
|
||||
localDataModel.userlist.map((u) => {
|
||||
let userrow: iTabelrow = {
|
||||
rowtype: 'user',
|
||||
col1: u.userid,
|
||||
cols: [u.username, u.email, u.telefon]
|
||||
}
|
||||
let usernode: iTreenode = new Treenode(userrow, localDataModel.startexpanded, [])
|
||||
localDataModel.applist.forEach((app: iApplication) => {
|
||||
let approw: iTabelrow = {
|
||||
rowtype: "app",
|
||||
col1: app.appid,
|
||||
cols: [app.name, app.minversion],
|
||||
info: {
|
||||
ischecked: false,
|
||||
parentuserid: u.id,
|
||||
parentappid: null,
|
||||
roleid: null
|
||||
}
|
||||
|
||||
}
|
||||
let appnode: Treenode = new Treenode(approw, localDataModel.startexpanded, [])
|
||||
app.roles.forEach((role: iRole) => {
|
||||
let ischecked = hasAppRole(u, app.id, role.id)
|
||||
let rolerow: iTabelrow = {
|
||||
rowtype: "role",
|
||||
col1: role.name,
|
||||
cols: [role.roleid],
|
||||
info: {
|
||||
parentuserid: u.id,
|
||||
parentappid: app.id,
|
||||
ischecked: ischecked,
|
||||
roleid: role.id
|
||||
}
|
||||
}
|
||||
let rolenode: iTreenode = new Treenode(rolerow, localDataModel.startexpanded, [])
|
||||
appnode.children.push(rolenode)
|
||||
})
|
||||
usernode.children.push(appnode)
|
||||
})
|
||||
localDataModel.usernodes.push(usernode)
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.code == 401) {
|
||||
m.route.set("/login")
|
||||
}
|
||||
})
|
||||
},
|
||||
updateUser: (user: iUser) => {
|
||||
m.request ({
|
||||
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
|
||||
url: "/auth/updateuser",
|
||||
body: user
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
deleteUsers: (users: iUser[]) => {
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/auth/deleteusers",
|
||||
body: users.map((u: iUser) => {return u.userid})
|
||||
})
|
||||
.catch((e) => {showServerError(e)})
|
||||
.finally(() => localDataModel.loadData())
|
||||
},
|
||||
applyUserAppRole: (tablerow: iTabelrow) => {
|
||||
m.request ({
|
||||
method: "POST",
|
||||
url: "/auth/applyuserapprole",
|
||||
body: tablerow.info
|
||||
})
|
||||
.catch((e) => {showServerError(e); localDataModel.loadData()})
|
||||
}
|
||||
}
|
||||
|
||||
export const userlist: m.ClosureComponent = (vnode) => {
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
localDataModel.loadData()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-content", {style: {'max-width': 'min-content', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}},
|
||||
m("h1", "Benutzerliste"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
startexpanded: localDataModel.startexpanded,
|
||||
treenodes: localDataModel.usernodes,
|
||||
columnmapper: (node: iTreenode) => {
|
||||
let row: iTabelrow = node.nodevalue
|
||||
let isChecked = hasCheckedChilds(node)
|
||||
if (row.rowtype == "user") {
|
||||
return [
|
||||
m("strong", row.col1),
|
||||
m("strong", row.cols[0]),
|
||||
m(isChecked?"div.w3-text-green":"div.w3-text-red", {style: {display: 'flex'}},
|
||||
// m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
|
||||
// m("i.material-icons", isChecked?null:"dangerous"),
|
||||
// ),
|
||||
m("div", isChecked?"aktiv":"inaktiv")
|
||||
),
|
||||
m("div.w3-small",
|
||||
m("div", row.cols[1]),
|
||||
m("div", row.cols[2])
|
||||
),
|
||||
m(EditUserButton, {userid: row.col1} as iEditUserButton),
|
||||
m(DeleteUserButton, {userid: row.col1} as iDeleteUserButton)
|
||||
]
|
||||
}
|
||||
else if (row.rowtype == "role") {
|
||||
return [
|
||||
m("span.w3-small", row.col1),
|
||||
m("span.w3-button", {
|
||||
style: {padding: '0px'},
|
||||
onclick: () => {
|
||||
row.info.ischecked = !row.info.ischecked
|
||||
localDataModel.applyUserAppRole(row)
|
||||
}
|
||||
},
|
||||
m("i.material-icons.w3-theme-light", row.info.ischecked?"check_box":"check_box_outline_blank")
|
||||
),
|
||||
m("span.w3-small", row.cols[0]),
|
||||
m("span.w3-small", row.cols[1]),
|
||||
m("span", ""),
|
||||
m("span", "")
|
||||
]
|
||||
}
|
||||
else {
|
||||
// rowtype "app"
|
||||
let isChecked = hasCheckedChilds(node)
|
||||
return [
|
||||
m(isChecked?"span":"strong.w3-text-red", row.col1),
|
||||
m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
|
||||
m("i.material-icons", isChecked?"check":null)
|
||||
),
|
||||
m("span", row.cols[0]),
|
||||
m("span", row.cols[1]),
|
||||
m("span", ""),
|
||||
m("span", "")
|
||||
]
|
||||
}
|
||||
}
|
||||
} as iTreetable,
|
||||
m(TopbarButton, {
|
||||
onclick: () => {
|
||||
m.mount(gridMountpoints.modalhook, {
|
||||
view: (vnode) => {
|
||||
return m(UserForm, {user: {id: -1} as iUser})
|
||||
}
|
||||
})
|
||||
}} as iTopbarButton,
|
||||
m("i.material-icons", "add")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasCheckedChilds(parenttreenode: iTreenode): boolean {
|
||||
return parenttreenode.children.some((treenode: iTreenode) => {
|
||||
if (treenode.hasChildren) {
|
||||
return hasCheckedChilds(treenode)
|
||||
}
|
||||
let tablerow = treenode.nodevalue as iTabelrow
|
||||
return tablerow.info.ischecked
|
||||
})
|
||||
}
|
||||
|
||||
interface iUserForm {
|
||||
user: iUser
|
||||
}
|
||||
const UserForm: m.ClosureComponent<iUserForm> = () => {
|
||||
let form: HTMLFormElement = null;
|
||||
let currentuser: iUser
|
||||
function reset (e) {
|
||||
form.reset()
|
||||
}
|
||||
function unmount () {
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}
|
||||
function submit (e) {
|
||||
e.preventDefault()
|
||||
let formdata: FormData = new FormData(e.target as HTMLFormElement)
|
||||
|
||||
for (let element of formdata) {
|
||||
currentuser[element[0]] = element[1]
|
||||
}
|
||||
localDataModel.updateUser(currentuser)
|
||||
unmount()
|
||||
}
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
currentuser = (vnode.attrs.user === undefined) ? {} as iUser : vnode.attrs.user
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m(ModalView,
|
||||
{
|
||||
title: "Benutzer",
|
||||
type: ModalType.MESSAGE,
|
||||
width: 'max-content'
|
||||
} as iModalView,
|
||||
|
||||
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
|
||||
m("label", "ID (intern)"),
|
||||
m("div.w3-padding-small", currentuser.id),
|
||||
m("label", "Benutzer-ID"),
|
||||
m("input.w3-input.w3-border", {name: 'userid', type: 'text', value: currentuser.userid}),
|
||||
m("label", "Name"),
|
||||
m("input.w3-input.w3-border", {name: 'username', type: 'text', value: currentuser.username}),
|
||||
m("label", "E-Mail"),
|
||||
m("input.w3-input.w3-border", {name: 'email', type: 'email', value: currentuser.email}),
|
||||
m("label", "Telefon"),
|
||||
m("input.w3-input.w3-border", {name: 'telefon', type: 'tel', value: currentuser.telefon}),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
|
||||
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
|
||||
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WrongUserEditSelectionMessage: m.Component = {
|
||||
view: () => {
|
||||
return m(WrongEditSelectionMessage, {elementname: "Benutzer"} as iWrongEditSelectionMessage)
|
||||
}
|
||||
}
|
||||
|
||||
interface iDeleteUsersMessage {
|
||||
userid: string
|
||||
}
|
||||
const DeleteUsersMessage: m.ClosureComponent<iDeleteUsersMessage> = (vnode) => {
|
||||
let users: iUser[]
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
users = localDataModel.userlist.filter((u: iUser) => {
|
||||
return u.userid == vnode.attrs.userid
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
return (
|
||||
m(ModalView,
|
||||
{
|
||||
title: "Benutzer löschen",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '33%'
|
||||
} as iModalView,
|
||||
m("p", "Sie wollen den folgenden Benutzer löschen:"),
|
||||
m("ul",
|
||||
users.map((user: iUser) => {
|
||||
return m("li", `${user.userid} (${user.username})`)
|
||||
})
|
||||
),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
[
|
||||
m("button.w3-button.w3-theme.w3-right",
|
||||
{
|
||||
style: {'margin-left': '4px'},
|
||||
onclick: () => {
|
||||
localDataModel.deleteUsers(users);
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}}, "Löschen"),
|
||||
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface iDeleteUserButton {
|
||||
userid: string
|
||||
}
|
||||
export const DeleteUserButton: m.ClosureComponent<iDeleteUserButton> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("button.w3-button.w3-bar-item", {
|
||||
style: {'min-width': '24px', height: '24px', padding: '0'},
|
||||
onclick: () => {
|
||||
m.mount(gridMountpoints.modalhook, {view: () => {
|
||||
return m(DeleteUsersMessage, {userid: vnode.attrs.userid} as iDeleteUsersMessage)
|
||||
}})
|
||||
},
|
||||
title: "Benutzer löschen"
|
||||
},
|
||||
m("i.material-icons", "delete_forever")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
interface iEditUserButton {
|
||||
userid: string
|
||||
}
|
||||
export const EditUserButton: m.ClosureComponent<iEditUserButton> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("button.w3-button.w3-bar-item", {
|
||||
style: {'min-width': '24px', height: '24px', padding: '0'},
|
||||
onclick: () => {
|
||||
let users: iUser[] = localDataModel.userlist.filter((u:iUser) => {
|
||||
return u.userid == vnode.attrs.userid
|
||||
})
|
||||
m.mount(gridMountpoints.modalhook, {view: () => {
|
||||
return m(UserForm, {user: users[0]} as iUserForm)
|
||||
}})
|
||||
},
|
||||
title: "Benutzer bearbeiten"
|
||||
},
|
||||
m("i.material-icons", "edit")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import m from "mithril"
|
||||
|
||||
export var headerdata = {
|
||||
date: new Date(),
|
||||
version: "",
|
||||
username: "Nicht angemeldet",
|
||||
userid: "Nicht angemeldet",
|
||||
appheader: "Der gläserne Kunde"
|
||||
}
|
||||
|
||||
export const header: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-row-padding.w3-theme",
|
||||
m("div.w3-twothird.w3-padding-16", {style: "display: flex; justify-content: space-between;"},
|
||||
// <img src="pfad/zum/bild.jpg" width="300" height="200" alt="Beschreibung des Bildes">
|
||||
m("img", {src: "static/KBplus-Logo.png", height: 80, alt: "KBPlus CRM Logo"}),
|
||||
m("h1",
|
||||
m("div.w3-cell", headerdata.appheader)
|
||||
)
|
||||
),
|
||||
m("div.w3-third.w3-display-container.w3-small",
|
||||
m("table.w3-right",
|
||||
m("tr.w3-hide-small",
|
||||
m("td", "Datum:"),
|
||||
m("td", headerdata.date.toLocaleDateString())
|
||||
),
|
||||
m("tr.w3-hide-small",
|
||||
m("td", "Version:"),
|
||||
m("td", headerdata.version)
|
||||
),
|
||||
m("tr",
|
||||
m("td", "Benutzer:"),
|
||||
m("td", headerdata.username)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const menubar: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-bar.w3-black",
|
||||
vnode.children
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let menustack: MenuStack = null
|
||||
|
||||
export function init_menustack(menubarmountpoint):void {
|
||||
if (menustack == null) {menustack = new MenuStack(menubarmountpoint)}
|
||||
}
|
||||
export function get_menustack(): MenuStack {
|
||||
return menustack
|
||||
}
|
||||
|
||||
class MenuStack {
|
||||
private menustack = []
|
||||
private menubarmountpoint = null
|
||||
constructor(menubarmountpoint) {
|
||||
this.menustack = []
|
||||
this.menubarmountpoint = menubarmountpoint
|
||||
}
|
||||
push(menubar) {
|
||||
this.menustack.push(menubar)
|
||||
m.mount(this.menubarmountpoint, menubar)
|
||||
}
|
||||
pop() {
|
||||
this.menustack.pop()
|
||||
let menubar = this.menustack[this.menustack.length - 1]
|
||||
m.mount(this.menubarmountpoint, menubar)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput} from "./widgets/inputs"
|
||||
|
||||
export const kunde: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-twothird",
|
||||
m("h1", "Kundenstammblatt"),
|
||||
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
|
||||
m("label", "Kundenname (ganz neu)", m(Input, {name: "kundenname", value: "Musterfirma GmbH", type: "text"} as iInput)),
|
||||
m("label", "Straße", m(Input, {name: "strasse", value: "Musterstraße 1", type: "text"} as iInput)) ,
|
||||
m("label", "PLZ", m(Input, {name: "plz", value: "12345", type: "text"} as iInput)),
|
||||
m("label", "Ort", m(Input, {name: "ort", value: "Musterstadt", type: "text"} as iInput)),
|
||||
m("label", "Land", m(Input, {name: "land", value: "Deutschland", type: "text"} as iInput))
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import m from "mithril";
|
||||
|
||||
export var gridMountpoints = {
|
||||
header: null,
|
||||
menubar: null,
|
||||
main: null,
|
||||
modalhook: null
|
||||
}
|
||||
export const maingrid: m.ClosureComponent = (vnode) => {
|
||||
let gridstyle = {
|
||||
'display': 'grid',
|
||||
'grid-template-rows': 'auto auto 1fr',
|
||||
'height': '100vh',
|
||||
'width': '100vw'
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return [
|
||||
m("div", {style: gridstyle},
|
||||
[
|
||||
m("div", {
|
||||
oncreate: (vnode) => { gridMountpoints.header = vnode.dom },
|
||||
style: {'grid-row': '1'}
|
||||
}),
|
||||
m("div", {
|
||||
oncreate: (vnode) => { gridMountpoints.menubar = vnode.dom },
|
||||
style: {'grid-row': '2'}
|
||||
}),
|
||||
m("div",
|
||||
{
|
||||
//oncreate: (vnode) => { gridMountpoints.main = vnode.dom },
|
||||
style: {'grid-row': "3", 'overflow-y': 'auto', 'height': '100%'}
|
||||
},
|
||||
m("div",
|
||||
{
|
||||
oncreate: (vnode) => { gridMountpoints.main = vnode.dom },
|
||||
style: {'overflow-y': 'auto', height: 'inherit'}
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
m("div#modalhook", {
|
||||
oncreate: (vnode) => { gridMountpoints.modalhook = vnode.dom}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput} from "./widgets/inputs"
|
||||
|
||||
export const Projekt: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-twothird",
|
||||
m("h1", "Projekt-Stammblatt"),
|
||||
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
|
||||
m("label", "Kundenname", m(Input, {name: "kundenname", value: "Musterfirma GmbH", type: "text"} as iInput)),
|
||||
m("label", "Projektname", m(Input, {name: "projektname", value: "Musterprojekt", type: "text"} as iInput)) ,
|
||||
m("label", "Zweck", m(Input, {name: "zweck", value: "Musterzweck", type: "text"} as iInput)),
|
||||
m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: "König Dickbauch", type: "text"} as iInput)),
|
||||
m("label", "Startdatum", m(Input, {name: "startdatum", value: "2023-01-01", type: "date"} as iInput))
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import m from "mithril"
|
||||
import {BaseView} from "./base"
|
||||
|
||||
export enum ModalType {
|
||||
'MESSAGE',
|
||||
'ERROR'
|
||||
}
|
||||
|
||||
export interface iModalView {
|
||||
title: string
|
||||
subtitle: string
|
||||
topposition: string
|
||||
width: string
|
||||
type: ModalType
|
||||
}
|
||||
export class ModalView extends BaseView implements iModalView{
|
||||
title:string
|
||||
subtitle:string
|
||||
topposition:string
|
||||
width:string
|
||||
type:ModalType
|
||||
|
||||
constructor (vnode) {
|
||||
super()
|
||||
this.title = super.getattr(vnode, "title", "Modal")
|
||||
this.subtitle = super.getattr(vnode, "subtitle", "")
|
||||
this.topposition = super.getattr(vnode, "topposition", "10%")
|
||||
this.width = super.getattr(vnode, "width", 'max-content')
|
||||
this.type = super.getattr(vnode, 'type', ModalType.MESSAGE)
|
||||
}
|
||||
view (vnode) {
|
||||
return m("div.w3-modal", {style: {display:'block'}},
|
||||
m("div.w3-modal-content.w3-card-4", {style: {width: this.width, top: this.topposition}},
|
||||
[
|
||||
m(modalHeader, {type: this.type},
|
||||
[
|
||||
m(title, this.title),
|
||||
(this.subtitle ? m(subtitle, this.subtitle) : null)
|
||||
]
|
||||
),
|
||||
m(modalContent, vnode.children)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const title = {
|
||||
view: (vnode) => {
|
||||
return m("h1", vnode.children)
|
||||
}
|
||||
}
|
||||
const subtitle = {
|
||||
view: (vnode) => {
|
||||
return m("h3", vnode.children)
|
||||
}
|
||||
}
|
||||
const modalHeader = {
|
||||
view: (vnode) => {
|
||||
let color = ".w3-theme"
|
||||
if (vnode.attrs["type"] == ModalType.ERROR) {
|
||||
color = ".w3-red"
|
||||
}
|
||||
return m("header.w3-container" + color, vnode.children)
|
||||
}
|
||||
}
|
||||
const modalContent = {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-theme-l1", vnode.children)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import m from "mithril"
|
||||
|
||||
export interface iTreenode {
|
||||
nodevalue: any
|
||||
children: iTreenode[]
|
||||
expand: boolean
|
||||
hasChildren: boolean
|
||||
isChecked?: boolean
|
||||
}
|
||||
|
||||
export class Treenode implements iTreenode {
|
||||
nodevalue:any = null
|
||||
children: iTreenode[] = []
|
||||
expand: boolean = true
|
||||
isChecked?: boolean = false
|
||||
constructor(nodevalue: any = null, expand: boolean = false, children: iTreenode[] = [], isChecked: boolean = false) {
|
||||
this.nodevalue = nodevalue
|
||||
this.children = children
|
||||
this.expand = expand
|
||||
this.isChecked = isChecked
|
||||
}
|
||||
get hasChildren() {
|
||||
return (this.children.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
export interface Columnmapper {
|
||||
(row:iTreenode) : any[]
|
||||
}
|
||||
|
||||
export interface iTreetable {
|
||||
treenodes: iTreenode[]
|
||||
columnmapper: Columnmapper
|
||||
onRowMarked?: (node: iTreenode) => void
|
||||
filter?: (row:iTreenode, searchvalue:string) => boolean
|
||||
}
|
||||
|
||||
export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
|
||||
let onRowMarked = vnode.attrs.onRowMarked === undefined ? (node: iTreenode) => {} : vnode.attrs.onRowMarked
|
||||
let expandtoggle: boolean = true
|
||||
let searchvalue:string = ""
|
||||
function setSearchvalue(v:string) {
|
||||
searchvalue = v
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("div.w3-border-theme", {style: {height: '99%', overflow: 'hidden', 'border-style': 'solid', 'border-width': '1px'}},
|
||||
m("div.w3-bar.w3-theme",
|
||||
m(TopbarButton, {
|
||||
onclick: () => {
|
||||
expandtoggle = true; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
|
||||
}},
|
||||
m("i.material-icons", "unfold_more")
|
||||
),
|
||||
m(TopbarButton, {
|
||||
onclick: () => {
|
||||
expandtoggle = false; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
|
||||
}},
|
||||
m("i.material-icons", "unfold_less")
|
||||
),
|
||||
vnode.children,
|
||||
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
|
||||
),
|
||||
m("div", {style: {height: '100%'}},
|
||||
m("div", {style: {height: 'calc(100% - 32px', overflow: 'auto', width: '100%'}},
|
||||
m("table.w3-table", {style: {'overflow-y': 'auto'}},
|
||||
vnode.attrs.treenodes.map((tn:iTreenode, z) => {
|
||||
if (vnode.attrs.filter == undefined || (vnode.attrs.filter != undefined && vnode.attrs.filter(tn, searchvalue) == true)) {
|
||||
return m(Treerow, {
|
||||
// ACHTUNG: Änderungen hier müssen unten beim rekursiven Aufruf der Treerow-Funktion ebenfalls eingebaut werden
|
||||
treenode: tn,
|
||||
level: 0,
|
||||
columnmapper1: vnode.attrs.columnmapper,
|
||||
onRowMarked: onRowMarked,
|
||||
isEven: ((z % 2) == 0)} as iTreerow)
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface iTreerow {
|
||||
treenode: iTreenode
|
||||
level: number
|
||||
columnmapper1: Columnmapper
|
||||
onRowMarked: (node: iTreenode) => void
|
||||
isEven?: boolean
|
||||
}
|
||||
let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
|
||||
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return [
|
||||
m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
|
||||
onkeyup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
|
||||
class: (vnode.attrs.isEven ? "w3-tablerow-even" : "w3-tablerow-odd"),
|
||||
style: {'border-top-style': 'solid', 'border-top-width': '1px'}
|
||||
},
|
||||
m("td", // Zunächst wird ein td-Element für die erste Spalte der Tabelle erzeugt, damit dort die
|
||||
// Einrückung in Abhängigkeit vom evtl. Vorhandensein von Kind-Elementen erledigt wird
|
||||
m("div", {style: {width: 'max-content', 'margin-left': `${(vnode.attrs.level * 25)}px`}} ,
|
||||
vnode.attrs.treenode.hasChildren
|
||||
// Wenn der Treenode Kinder hat, dann soll der "expand_more/expand_less"-Button
|
||||
// angezeigt werden
|
||||
? m("span.w3-button", {
|
||||
onclick: () => {changeTreenode(vnode.attrs.treenode, !vnode.attrs.treenode.expand)},
|
||||
style: {padding: '0px',
|
||||
'margin-right': '8px',
|
||||
width: '24px',
|
||||
height: '24px'}},
|
||||
m("i.material-icons", vnode.attrs.treenode.expand?"expand_more":"navigate_next")
|
||||
)
|
||||
// Wenn der Trennode keine Kinder hat, dann soll kein "expand_more/expand_less"-Button
|
||||
// angezeigt werden, allerdings soll durch 'margin-left' der entsprechende Platz des
|
||||
// Buttons eingerückt werden, damit die Einrückungs-Level auch bei "kinderlosen" Treenodes
|
||||
// denen der Trennodes mit Kindern entsprechen
|
||||
: vnode.attrs.level == 0
|
||||
? null // Wenn der kinderlos Treenode allerdings sirekt auf Level 0 ist, soll nocht
|
||||
// eingerückt werden, da es bei Level 0 keine Eltern-Nodes mit Kindern und
|
||||
// demzufolge mit "expand_more/expand_less"-Button gibt.
|
||||
: m(
|
||||
"span", {
|
||||
style: {'margin-left': '24px',
|
||||
'margin-right': '8px'}
|
||||
}
|
||||
),
|
||||
// Nachdem die einrückung auf Grund der "hasChildren"-Abfrage erledigt ist, wird der Inhalt
|
||||
// der ersten Spalte des Column-Mappers angezeigt
|
||||
vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(0,1)
|
||||
)
|
||||
),
|
||||
// Dann werden die übrigen Spalten ganz normal als td-Elemente eingefügt
|
||||
vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(1).map((column) => {
|
||||
return m("td", column)
|
||||
})
|
||||
),
|
||||
// Falls der Trennode Kinder hat und expandiert werden soll, wird rekursiv die Trennode-Komponente für
|
||||
// die Child-Nodes aufgerufen, ansonsten wir null zurückgegeben und die Einrückung hört auf
|
||||
vnode.attrs.treenode.hasChildren && vnode.attrs.treenode.expand
|
||||
? vnode.attrs.treenode.children.map((tn: iTreenode, z) => {
|
||||
return m(Treerow, {
|
||||
treenode: tn,
|
||||
level: vnode.attrs.level + 1,
|
||||
columnmapper1: vnode.attrs.columnmapper1,
|
||||
onRowMarked: vnode.attrs.onRowMarked,
|
||||
isEven: ((z % 2) == 0)
|
||||
} as iTreerow)
|
||||
})
|
||||
: null
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface iTopbarButton {
|
||||
onclick: Function
|
||||
tooltip?: string
|
||||
}
|
||||
export const TopbarButton: m.ClosureComponent<iTopbarButton> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("button.w3-button.w3-bar-item", {
|
||||
style: {'min-width': '32px', height: '32px', padding: '4px'},
|
||||
onclick: vnode.attrs.onclick,
|
||||
title: vnode.attrs.tooltip?vnode.attrs.tooltip:null
|
||||
},
|
||||
vnode.children
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Funktion kann aufgerufen werden, wenn eine Treenode-Struktur hinsichtlich
|
||||
* "Expand more/less" geändert werden soll.
|
||||
*/
|
||||
export function changeTreenodes(treenodes: Treenode[], expand: boolean) {
|
||||
treenodes.forEach((treenode: Treenode) => {
|
||||
changeTreenode(treenode, expand)
|
||||
if (treenode.hasChildren) {
|
||||
changeTreenodes(treenode.children, expand)
|
||||
}
|
||||
})
|
||||
}
|
||||
function changeTreenode(treenode: Treenode, expand: boolean) {
|
||||
treenode.expand = expand
|
||||
}
|
||||
|
||||
interface iSearchfield {
|
||||
setSearchvalue: (v:string) => void
|
||||
}
|
||||
const SearchField:m.ClosureComponent<iSearchfield> = (vnode) => {
|
||||
let inputfield: HTMLInputElement|null = null
|
||||
function oninput(e:Event) {
|
||||
if (inputfield) { // ✅ Null-Check
|
||||
let v:string = inputfield.value
|
||||
vnode.attrs.setSearchvalue(v)
|
||||
}
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("div", {style: {display: "flex", border: "1px solid black", margin: '1px'}},
|
||||
m("input.ggg-inputfocus", {
|
||||
oncreate: (vnode) => {
|
||||
inputfield = vnode.dom as HTMLInputElement
|
||||
inputfield.focus()
|
||||
},
|
||||
type:"text",
|
||||
style: {flex: '1', border: 'none'}, oninput: oninput
|
||||
}),
|
||||
m("button.w3-button", {
|
||||
style: {'min-width': '28px', height: '28px', padding: '4px', border: 'none'},
|
||||
onclick: () => {
|
||||
if (inputfield) { // ✅ Null-Check
|
||||
inputfield.value = ""
|
||||
vnode.attrs.setSearchvalue("")
|
||||
inputfield.focus()
|
||||
}
|
||||
}},
|
||||
m("i.material-icons", {style: {'font-size': 'medium'}}, "clear")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class BaseView {
|
||||
|
||||
getattr(vnode: any, attrname: string, defaultvalue: any) {
|
||||
return vnode.attrs[attrname] === undefined ? defaultvalue : vnode.attrs[attrname]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import m from "mithril"
|
||||
|
||||
export interface iForm {
|
||||
|
||||
onchanged?: (e:Event) => void
|
||||
onsubmit: (form:HTMLFormElement) => boolean
|
||||
oncancel: (e:Event) => void
|
||||
showbuttons?: boolean
|
||||
/** Die Funktion gibt das HTMLFormElement zurück, welches in der Form-Componente angelegt wird */
|
||||
onformcreate?: (form:HTMLFormElement) => void
|
||||
}
|
||||
|
||||
export const Form:m.ClosureComponent<iForm> = (vnode) => {
|
||||
let form: HTMLFormElement
|
||||
let isDirty: boolean = false
|
||||
let showButtons: boolean = vnode.attrs.showbuttons == undefined ? true : vnode.attrs.showbuttons
|
||||
function canCloseForm(typ: string = "") {
|
||||
if (!isDirty) {
|
||||
return true
|
||||
}
|
||||
let text = (typ == "reset") ? "zurücksetzen" : "beenden"
|
||||
return confirm (`Das Formular enthält noch ungespeicherte Eingaben. Wollen Sie das Formular trotzdem ${text}?`)
|
||||
}
|
||||
function onchanged (e:Event) {
|
||||
isDirty = true
|
||||
if (vnode.attrs.onchanged != undefined) {
|
||||
vnode.attrs.onchanged(e)
|
||||
}
|
||||
}
|
||||
function submit (e:Event) {
|
||||
e.preventDefault()
|
||||
if (vnode.attrs.onsubmit(form) == true) {
|
||||
isDirty = false
|
||||
}
|
||||
}
|
||||
function registerFormElement (f:HTMLFormElement) {
|
||||
if (vnode.attrs.onformcreate != undefined) {
|
||||
vnode.attrs.onformcreate(f)
|
||||
}
|
||||
}
|
||||
function cancel(e:Event) {
|
||||
e.preventDefault()
|
||||
if (canCloseForm() == true) {
|
||||
isDirty = false
|
||||
if (vnode.attrs.oncancel != undefined) {
|
||||
vnode.attrs.oncancel(e)
|
||||
}
|
||||
else {
|
||||
history.back()
|
||||
}
|
||||
}
|
||||
else {
|
||||
e['redraw'] = false
|
||||
}
|
||||
}
|
||||
function reset(e:Event) {
|
||||
if (canCloseForm("reset")) {
|
||||
isDirty = false
|
||||
// Hier passiert nichts.
|
||||
// Da mithril nach jedem User-Event (wie z.B. einem 'Click') ein redraw() ausführt, wird hier die Form
|
||||
// neu gezeichnet und zwar mit den ursprünglichen Daten aus den zu grundeliegenden Datenmodel, das ja
|
||||
// ohne submit noch nicht geändert wurde.
|
||||
return
|
||||
}
|
||||
e['redraw'] = false
|
||||
return
|
||||
}
|
||||
return {
|
||||
onremove: (vnode) => {
|
||||
let message = "remove"
|
||||
if (isDirty) {
|
||||
message += " isDirty"
|
||||
}
|
||||
console.log(message)
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("form", {
|
||||
onreset: () => {console.log("reset")},
|
||||
onsubmit: submit,
|
||||
oncreate: (vnode) => {
|
||||
form = vnode.dom as HTMLFormElement
|
||||
let elements = form.querySelectorAll("input,textarea")
|
||||
for (let index = 0; index < elements.length; index++) {
|
||||
const element = elements[index];
|
||||
element.addEventListener("change", onchanged)
|
||||
}
|
||||
registerFormElement(form)
|
||||
}
|
||||
},
|
||||
vnode.children,
|
||||
showButtons == true ?
|
||||
m("div.w3-bar.w3-padding-16",[
|
||||
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, type: "submit"}, "Speichern"),
|
||||
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, onclick: cancel, type: "button"}, "Abbrechen"),
|
||||
m("button.w3-button.w3-theme.w3-right.w3-mobile", {onclick: reset, type: "button"}, "Zurücksetzen")
|
||||
])
|
||||
: null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface iFormBorder {
|
||||
maxwidth: string
|
||||
}
|
||||
export const FormBorder: m.Component<iFormBorder> = {
|
||||
view: (vnode) => {
|
||||
let mw:string = vnode.attrs.maxwidth === undefined ? "600px" : vnode.attrs.maxwidth
|
||||
return m("div.w3-border.w3-border-theme.w3-container.w3-content.w3-margin-top",
|
||||
{style: {'max-width': mw, 'box-shadow': '3px 3px 3px darkgrey'}}, vnode.children)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import m from "mithril"
|
||||
|
||||
/**
|
||||
* Der Vnode der Komponente 'Input' akzeptiert die folgenden Parameter:
|
||||
* @property {string} name - Der Name des Feldes
|
||||
* @property {any} value - der darzustellende Wert
|
||||
* @property {string} type (optional) - der Type des Input Felds. Möglich sind alle Type des HTML-Elements 'Input'
|
||||
* @property {string|number} width (optional) - die Breite des Input Felds. Möglich sind alle Längeneinheiten, die bom Width-Attribut möglich sind
|
||||
* @property {number} min (optional) -
|
||||
* @property {number} max (optional) -
|
||||
* @property {number} step (optional) -
|
||||
* @property {boolean} autofocus (optional) -
|
||||
*/
|
||||
export interface iInput {
|
||||
name: string
|
||||
value: any
|
||||
type?: string
|
||||
width?: string
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
autofocus?: boolean
|
||||
readonly?: boolean
|
||||
required?:boolean
|
||||
onchanged?: Function
|
||||
}
|
||||
/**
|
||||
* Diese Komponente stellt ein Html-Input Element dar. Der darzustellende Wert wird einfach mit dem Attribut
|
||||
* 'value' übergeben. Der eingegebene Wert muss dann später explizit aus Value abgefragt werden. Es findet also kein
|
||||
* sutomatisches Binding statt.
|
||||
*/
|
||||
export const Input:m.Component<iInput> = {
|
||||
onupdate: (vnode) => {
|
||||
if (vnode.attrs.autofocus != undefined && vnode.attrs.autofocus == true) {
|
||||
let e: HTMLElement = vnode.dom as HTMLElement
|
||||
e.focus()
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
let attributes = {}
|
||||
let width:number|string = '100%'
|
||||
attributes['name'] = vnode.attrs.name
|
||||
attributes['type'] = vnode.attrs.type == undefined ? 'text' : vnode.attrs.type
|
||||
if (attributes['type'] == 'number') {
|
||||
if (vnode.attrs.min != undefined) {
|
||||
attributes['min'] = vnode.attrs.min
|
||||
}
|
||||
if (vnode.attrs.max != undefined) {
|
||||
attributes['max'] = vnode.attrs.max
|
||||
}
|
||||
if (vnode.attrs.step != undefined) {
|
||||
attributes['step'] = vnode.attrs.step
|
||||
}
|
||||
}
|
||||
if (attributes['type'] == 'date') {
|
||||
attributes['value'] = toDateInputValue(vnode.attrs.value)
|
||||
}
|
||||
else {
|
||||
attributes['value'] = vnode.attrs.value
|
||||
}
|
||||
if (vnode.attrs.required != undefined && vnode.attrs.required == true) {
|
||||
attributes['required'] = vnode.attrs.required
|
||||
}
|
||||
if (vnode.attrs.readonly != undefined && vnode.attrs.readonly == true) {
|
||||
attributes['readonly'] = "true"
|
||||
}
|
||||
if (vnode.attrs.width != undefined) {
|
||||
width = vnode.attrs.width
|
||||
}
|
||||
attributes['style'] = {'margin-bottom': '5px', 'width': width}
|
||||
if (vnode.attrs.onchanged != undefined) {
|
||||
attributes['onchange'] = vnode.attrs.onchanged
|
||||
}
|
||||
|
||||
|
||||
return m("input.w3-input.w3-border", attributes)
|
||||
}
|
||||
}
|
||||
|
||||
export interface iTextarea {
|
||||
name: string
|
||||
value: any
|
||||
cols?: number
|
||||
rows?: number
|
||||
autofocus?: boolean
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Diese Komponente stellt ein Html-Textarea Element dar. Der darzustellende Wert wird einfach mit dem Attribut
|
||||
* 'value' übergeben. Der eingegebene Wert muss dann später explizit aus Value abgefragt werden. Es findet also kein
|
||||
* automatisches Binding statt.
|
||||
*/
|
||||
export const Textarea:m.Component<iTextarea> = {
|
||||
onupdate: (vnode) => {
|
||||
if (vnode.attrs.autofocus != undefined && vnode.attrs.autofocus == true) {
|
||||
let e: HTMLElement = vnode.dom as HTMLElement
|
||||
e.focus()
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
let attributes = {}
|
||||
attributes['name'] = vnode.attrs.name
|
||||
attributes['value'] = vnode.attrs.value
|
||||
if (vnode.attrs.cols != undefined) {
|
||||
attributes['cols'] = vnode.attrs.cols
|
||||
}
|
||||
if (vnode.attrs.rows != undefined) {
|
||||
attributes['rows'] = vnode.attrs.rows
|
||||
}
|
||||
if (vnode.attrs.readonly != undefined && vnode.attrs.readonly == true) {
|
||||
attributes['readonly'] = vnode.attrs.readonly
|
||||
}
|
||||
attributes['style'] = {'margin-bottom': '5px', "width": '100%'}
|
||||
|
||||
return m("textarea.w3-input.w3-border.w3-mobile", attributes)
|
||||
}
|
||||
}
|
||||
|
||||
export function toDateInputValue(date: Date): string {
|
||||
console.log(date, typeof(date), (date instanceof Date))
|
||||
let result: string = null
|
||||
if (date instanceof Date && date != null) {
|
||||
result = [date.getFullYear(), (date.getMonth() + 1).toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0")].join("-")
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import m from "mithril"
|
||||
|
||||
|
||||
interface iTabDef {
|
||||
tabname: string
|
||||
tabview: m.ComponentTypes
|
||||
}
|
||||
|
||||
type TabDef = [string, m.ComponentTypes]
|
||||
|
||||
export interface iTabView {
|
||||
tabviews: TabDef[]
|
||||
}
|
||||
|
||||
export const TabView: m.ClosureComponent<iTabView> = (vnode) => {
|
||||
let tv = {}
|
||||
vnode.attrs.tabviews.forEach((item: TabDef) => {
|
||||
tv[item[0]] = item[1]
|
||||
})
|
||||
|
||||
let currTab = ""
|
||||
function changeview(e) {
|
||||
currTab = e.target.value
|
||||
}
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("div", {style: {display: 'grid', 'grid-template-rows': "auto 1fr", height: '100%'}}, [
|
||||
m("div.w3-bar.w3-theme.w3-border-bottom.w3-border-theme.w3-theme-action.w3-small",
|
||||
Object.keys(tv).map((key) => {
|
||||
if (currTab == "") {currTab = key}
|
||||
let c = (key == currTab) ? ".w3-theme-light" : ""
|
||||
return m("button.w3-bar-item.w3-button" + c, {value: key, onclick: changeview}, key)
|
||||
})
|
||||
),
|
||||
m("div", m(tv[currTab]))
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import m from "mithril"
|
||||
import {gridMountpoints, maingrid} from "./Components/mainlayout"
|
||||
import {header, menubar, init_menustack, get_menustack, headerdata} from "./Components/header"
|
||||
import { appmanagementpage } from "./Components/auth/appmanagementpage"
|
||||
import { changepasswordscreen, loginscreen } from "./Components/auth/login"
|
||||
import {kunde} from "./Components/kunde"
|
||||
import { Projekt } from "./Components/projekt"
|
||||
|
||||
|
||||
function healthcheck () {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/xhr/health-check"})
|
||||
.then((result) => {
|
||||
data.success = result['result']
|
||||
data.time = result['time']
|
||||
data.server = result['server']
|
||||
data.projectname = result["projectname"]
|
||||
data.clientappversion = result["clientappversion"]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
function getClientappversion(): void {
|
||||
//let version:string = document.querySelector('meta[name="clientappversion"]')['content']
|
||||
const meta = document.querySelector<HTMLMetaElement>('meta[name="clientappversion"]');
|
||||
const version = meta?.content ?? ''; // leerer String als Fallback
|
||||
console.log(version)
|
||||
headerdata.version = version
|
||||
}
|
||||
|
||||
function check_currentuser (): boolean {
|
||||
let result = true
|
||||
const cu = sessionStorage.getItem("CurrentUser")
|
||||
if (cu != null) {
|
||||
headerdata.username = cu
|
||||
}
|
||||
else {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
let data = {
|
||||
success: "n/a",
|
||||
time: "n/a",
|
||||
server: "n/a",
|
||||
projectname: "n/a",
|
||||
clientappversion: "n/a"
|
||||
}
|
||||
|
||||
function logout() {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/logout"
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result)
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
headerdata.username = "Nicht angemeldet"
|
||||
if (result['logout'] == true) {m.route.set("/blankpage")}
|
||||
})
|
||||
.finally(() => {
|
||||
location.href = "/index.html"
|
||||
})
|
||||
}
|
||||
|
||||
const hello: m.Component = {
|
||||
oninit: (vnode) => {
|
||||
healthcheck()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div",
|
||||
m("h1", "Health-Check Python App-Server"),
|
||||
m(hello2),
|
||||
m(knopf)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const hello2: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("ul",
|
||||
m("li", "Result: " + data.success),
|
||||
m("li", data.time),
|
||||
m("li", data.server),
|
||||
m("li", data.projectname),
|
||||
m("li", data.clientappversion),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const knopf: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m("button", {onclick: healthcheck}, "Aktualisieren")
|
||||
}
|
||||
}
|
||||
const menu: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m(menubar, [
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/kunde"}, "Kunden"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/projekt"}, "Projekte"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/routename"}, "Gesprächsnotizen"),
|
||||
// m("a.w3-button.w3-bar-item", {onclick: healthcheck}, "Aktualisieren"),
|
||||
m("div.w3-dropdown-hover",
|
||||
m("button.w3-button", "Optionen"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Tags"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenexport"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenimport"),
|
||||
)
|
||||
),
|
||||
m("div.w3-button.w3-right", {onclick: logout}, "Logout"),
|
||||
m("div.w3-dropdown-hover.w3-right",
|
||||
m("button.w3-button", "System"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/login"}, "Login"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/changepassword"}, "Kennwort ändern"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/usermanagement"}, "Verwalten"),
|
||||
)
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
let mountpoint = document.getElementById("body")
|
||||
|
||||
m.mount(mountpoint, maingrid)
|
||||
m.mount(gridMountpoints.header, header)
|
||||
m.mount(gridMountpoints.main, hello)
|
||||
|
||||
init_menustack(gridMountpoints.menubar)
|
||||
get_menustack().push(menu)
|
||||
|
||||
getClientappversion()
|
||||
|
||||
if (check_currentuser() == false) {
|
||||
m.route.set("/login")
|
||||
}
|
||||
|
||||
m.route(gridMountpoints.main, "/blankpage", {
|
||||
"/blankpage": hello,
|
||||
"/usermanagement": appmanagementpage,
|
||||
"/changepassword": changepasswordscreen,
|
||||
"/login": loginscreen,
|
||||
"/kunde": kunde,
|
||||
"/projekt": Projekt
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
var result = ""
|
||||
|
||||
// https://stackoverflow.com/questions/55409195/reading-ini-file-using-javascript
|
||||
function parseINIString(data){
|
||||
var regex = {
|
||||
section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
|
||||
param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/,
|
||||
comment: /^\s*;.*$/
|
||||
};
|
||||
var value = {};
|
||||
var lines = data.split(/[\r\n]+/);
|
||||
var section = null;
|
||||
lines.forEach(function(line){
|
||||
if(regex.comment.test(line)){
|
||||
return;
|
||||
}else if(regex.param.test(line)){
|
||||
var match = line.match(regex.param);
|
||||
if(section){
|
||||
value[section][match[1]] = match[2];
|
||||
}else{
|
||||
value[match[1]] = match[2];
|
||||
}
|
||||
}else if(regex.section.test(line)){
|
||||
var match = line.match(regex.section);
|
||||
value[match[1]] = {};
|
||||
section = match[1];
|
||||
}else if(line.length == 0 && section){
|
||||
section = null;
|
||||
};
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getIniValue(value){
|
||||
return result["APP"][value]
|
||||
}
|
||||
|
||||
var inifile = readFileSync("../App_Server/root/config.ini").toString()
|
||||
result = parseINIString(inifile)
|
||||
Generated
+1299
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mithril": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/mithril": "^2.2.7",
|
||||
"rollup": "^4.21.2",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Die Config-Datei muss ab Rollup Version 3 die Endung .mjs haben. (s. Rollup Dokumentation "Migration to Rollup 3")
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
// import json from '@rollup/plugin-json';
|
||||
import {getIniValue} from "./inireader.mjs"
|
||||
|
||||
var version = getIniValue("version")
|
||||
var projectname = getIniValue("projectname")
|
||||
|
||||
export default {
|
||||
input: './tsout/index.js',
|
||||
output: [
|
||||
{
|
||||
file: `/usr/opt/pythonapp/NGINX_Server/html/static/${projectname}_${version}.js`,
|
||||
format: 'iife',
|
||||
name: `${projectname}`
|
||||
},
|
||||
{
|
||||
file: `/usr/opt/pythonapp/NGINX_Server/html/static/${projectname}_${version}.min.js`,
|
||||
format: 'iife',
|
||||
name: `${projectname}`,
|
||||
plugins: [
|
||||
terser()
|
||||
]
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
// Der folgende Eintrag ist erforderlich, weil das tsout-Verzeichnis und somit das Input-Verzeichnis für Rollup,
|
||||
// welches die Typescript-Kompilierten Programme enthält, nicht als Unterverzeichnis des Projekt-Verzeichnisses
|
||||
// (...\UWSGI-Server\vassals\timesheet\client\src) angelegt ist, sondern parallel dazu.
|
||||
// Ohne den folgenden Eintrag findet Rollup dann nicht das "Mithril"-Modul und es kommt zu Fehlern ("global nicht gefunden" usw.)
|
||||
// Zusätzlich verwirrend war, dass der Fehler nicht auftrat, wenn das commonjs-Plugin mit der Version 21 eingesetzt war.
|
||||
// Erst nachdem ich herausgefunden hatte, dass der nachfolgende Eintrag im Node-Resolve-Plugin den Fehler behob, konnte
|
||||
// ich das commenjs-Plugin in der aktuellsten Version einsetzten.
|
||||
modulePaths: ['node_modules'],
|
||||
}),
|
||||
commonjs()
|
||||
// json()
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"outDir": "./tsout",
|
||||
"module": "ESNext",
|
||||
"target": "ES2019",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user