mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 19:14:16 +00:00
refactor App with evite scattfold
This commit is contained in:
parent
9becc55879
commit
a7f01730eb
@ -11,19 +11,16 @@ String.prototype.toTitleCase = function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { CreateEviteApp, BindPropsProvider } from "evite"
|
import { CreateEviteApp, BindPropsProvider } from "evite-react-lib"
|
||||||
import { Helmet } from "react-helmet"
|
import { Helmet } from "react-helmet"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import progressBar from "nprogress"
|
|
||||||
import classnames from "classnames"
|
|
||||||
|
|
||||||
import { SidebarController, SettingsController } from "controllers"
|
import { Session, User, SidebarController, SettingsController } from "models"
|
||||||
import { Session, User } from "models"
|
|
||||||
import { API, Render, Splash, Theme, Sound } from "extensions"
|
import { API, Render, Splash, Theme, Sound } from "extensions"
|
||||||
import config from "config"
|
import config from "config"
|
||||||
|
|
||||||
import { NotFound, RenderError, FabricCreator, Settings } from "components"
|
import { NotFound, RenderError, Settings } from "components"
|
||||||
import { Sidebar, Header, Drawer, Sidedrawer } from "./layout"
|
import Layout from "./layout"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
import "theme/index.less"
|
import "theme/index.less"
|
||||||
@ -56,13 +53,31 @@ class ThrowCrash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const __renderTest = () => {
|
||||||
|
const [position, setPosition] = React.useState(0)
|
||||||
|
|
||||||
|
// create a 300ms interval to move randomly inside window screen
|
||||||
|
React.useEffect(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
const x = Math.random() * window.innerWidth
|
||||||
|
const y = Math.random() * window.innerHeight
|
||||||
|
|
||||||
|
setPosition({ x, y })
|
||||||
|
}, 50)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// clear interval when component unmount
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearInterval()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div style={{ top: position.y, left: position.x }} className="__render_box_test" />
|
||||||
|
}
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
static initialize() {
|
static initialize() {
|
||||||
this.progressBar = progressBar.configure({ parent: "html", showSpinner: false })
|
|
||||||
|
|
||||||
this.sessionController = new Session()
|
|
||||||
this.userController = new User()
|
|
||||||
|
|
||||||
this.configuration = {
|
this.configuration = {
|
||||||
settings: new SettingsController(),
|
settings: new SettingsController(),
|
||||||
sidebar: new SidebarController(),
|
sidebar: new SidebarController(),
|
||||||
@ -70,45 +85,41 @@ class App {
|
|||||||
|
|
||||||
this.eventBus = this.contexts.main.eventBus
|
this.eventBus = this.contexts.main.eventBus
|
||||||
|
|
||||||
this.eventBus.on("app_ready", () => {
|
this.eventBus.on("app_loading", async () => {
|
||||||
this.setState({ initialized: true })
|
await this.setState({ initialized: false })
|
||||||
})
|
this.eventBus.emit("splash_show")
|
||||||
this.eventBus.on("top_loadBar_start", () => {
|
|
||||||
this.progressBar.start()
|
|
||||||
})
|
|
||||||
this.eventBus.on("top_loadBar_stop", () => {
|
|
||||||
this.progressBar.done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus.on("forceInitialize", async () => {
|
this.eventBus.on("app_ready", async () => {
|
||||||
await this.initialization()
|
await this.setState({ initialized: true })
|
||||||
|
this.eventBus.emit("splash_close")
|
||||||
})
|
})
|
||||||
this.eventBus.on("forceReloadUser", async () => {
|
|
||||||
await this.__init_user()
|
this.eventBus.on("reinitializeSession", async () => {
|
||||||
|
await this.__SessionInit()
|
||||||
})
|
})
|
||||||
this.eventBus.on("forceReloadSession", async () => {
|
this.eventBus.on("reinitializeUser", async () => {
|
||||||
await this.__init_session()
|
await this.__UserInit()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus.on("forceToLogin", () => {
|
this.eventBus.on("forceToLogin", () => {
|
||||||
if (window.location.pathname !== "/login") {
|
if (window.location.pathname !== "/login") {
|
||||||
this.beforeLoginLocation = window.location.pathname
|
this.beforeLoginLocation = window.location.pathname
|
||||||
}
|
}
|
||||||
|
|
||||||
window.app.setLocation("/login")
|
window.app.setLocation("/login")
|
||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus.on("destroyAllSessions", async () => {
|
this.eventBus.on("new_session", async () => {
|
||||||
await this.sessionController.destroyAllSessions()
|
await this.initialization()
|
||||||
})
|
|
||||||
this.eventBus.on("new_session", () => {
|
|
||||||
this.eventBus.emit("forceInitialize")
|
|
||||||
|
|
||||||
if (window.location.pathname == "/login") {
|
if (window.location.pathname == "/login") {
|
||||||
window.app.setLocation(this.beforeLoginLocation ?? "/main")
|
window.app.setLocation(this.beforeLoginLocation ?? "/main")
|
||||||
this.beforeLoginLocation = null
|
this.beforeLoginLocation = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.eventBus.on("destroyed_session", () => {
|
this.eventBus.on("destroyed_session", async () => {
|
||||||
this.flushState()
|
await this.flushState()
|
||||||
this.eventBus.emit("forceToLogin")
|
this.eventBus.emit("forceToLogin")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -126,19 +137,13 @@ class App {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus.on("setLocation", () => {
|
|
||||||
this.eventBus.emit("top_loadBar_start")
|
|
||||||
this.setState({ isOnTransition: true })
|
|
||||||
})
|
|
||||||
this.eventBus.on("setLocationDone", () => {
|
|
||||||
this.eventBus.emit("top_loadBar_stop")
|
|
||||||
this.setState({ isOnTransition: false })
|
|
||||||
})
|
|
||||||
this.eventBus.on("cleanAll", () => {
|
this.eventBus.on("cleanAll", () => {
|
||||||
window.app.DrawerController.closeAll()
|
window.app.DrawerController.closeAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus.on("crash", (message, error) => {
|
this.eventBus.on("crash", (message, error) => {
|
||||||
|
console.debug("[App] crash detecting, returning crash...")
|
||||||
|
|
||||||
this.setState({ crash: { message, error } })
|
this.setState({ crash: { message, error } })
|
||||||
this.contexts.app.SoundEngine.play("crash")
|
this.contexts.app.SoundEngine.play("crash")
|
||||||
})
|
})
|
||||||
@ -149,25 +154,20 @@ class App {
|
|||||||
openSettings: (goTo) => {
|
openSettings: (goTo) => {
|
||||||
window.app.DrawerController.open("settings", Settings, {
|
window.app.DrawerController.open("settings", Settings, {
|
||||||
props: {
|
props: {
|
||||||
width: "40%",
|
width: "fit-content",
|
||||||
},
|
},
|
||||||
componentProps: {
|
componentProps: {
|
||||||
goTo,
|
goTo,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openFabric: (defaultType) => {
|
goMain: () => {
|
||||||
window.app.DrawerController.open("FabricCreator", FabricCreator, {
|
return window.app.setLocation(config.app.mainPath)
|
||||||
props: {
|
},
|
||||||
width: "70%",
|
goToAccount: (username) => {
|
||||||
},
|
return window.app.setLocation(`/account`, { username })
|
||||||
componentProps: {
|
|
||||||
defaultType,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
configuration: this.configuration,
|
configuration: this.configuration,
|
||||||
isValidSession: this.isValidSession,
|
|
||||||
getSettings: (...args) => this.contexts.app.configuration?.settings?.get(...args),
|
getSettings: (...args) => this.contexts.app.configuration?.settings?.get(...args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +178,6 @@ class App {
|
|||||||
sessionController: this.sessionController,
|
sessionController: this.sessionController,
|
||||||
userController: this.userController,
|
userController: this.userController,
|
||||||
configuration: this.configuration,
|
configuration: this.configuration,
|
||||||
progressBar: this.progressBar,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,45 +193,43 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionController = new Session()
|
||||||
|
|
||||||
|
userController = new User()
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
// app
|
// app
|
||||||
initialized: false,
|
initialized: false,
|
||||||
isMobile: false,
|
|
||||||
crash: false,
|
crash: false,
|
||||||
isOnTransition: false,
|
|
||||||
|
|
||||||
// app session
|
// app session
|
||||||
session: null,
|
session: null,
|
||||||
data: null,
|
data: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
flushState = () => {
|
flushState = async () => {
|
||||||
this.setState({ session: null, data: null })
|
await this.setState({ session: null, data: null })
|
||||||
}
|
|
||||||
|
|
||||||
isValidSession = async () => {
|
|
||||||
return await this.sessionController.isCurrentTokenValid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
|
this.eventBus.emit("app_loading")
|
||||||
|
|
||||||
|
await this.contexts.app.initializeDefaultBridge()
|
||||||
await this.initialization()
|
await this.initialization()
|
||||||
|
|
||||||
|
this.eventBus.emit("app_ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
initialization = async () => {
|
initialization = async () => {
|
||||||
try {
|
try {
|
||||||
this.eventBus.emit("splash_show")
|
await this.__SessionInit()
|
||||||
await this.contexts.app.initializeDefaultBridge()
|
await this.__UserInit()
|
||||||
await this.__init_session()
|
|
||||||
await this.__init_user()
|
|
||||||
this.eventBus.emit("app_ready")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.eventBus.emit("splash_close")
|
|
||||||
throw new ThrowCrash(error.message, error.description)
|
throw new ThrowCrash(error.message, error.description)
|
||||||
}
|
}
|
||||||
this.eventBus.emit("splash_close")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__init_session = async () => {
|
__SessionInit = async () => {
|
||||||
if (typeof Session.token === "undefined") {
|
if (typeof Session.token === "undefined") {
|
||||||
window.app.eventBus.emit("forceToLogin")
|
window.app.eventBus.emit("forceToLogin")
|
||||||
} else {
|
} else {
|
||||||
@ -252,7 +249,7 @@ class App {
|
|||||||
this.setState({ session: this.session })
|
this.setState({ session: this.session })
|
||||||
}
|
}
|
||||||
|
|
||||||
__init_user = async () => {
|
__UserInit = async () => {
|
||||||
if (!this.session || !this.session.valid) {
|
if (!this.session || !this.session.valid) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -267,6 +264,10 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.state.initialized) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.crash) {
|
if (this.state.crash) {
|
||||||
return <div className="app_crash">
|
return <div className="app_crash">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
@ -275,37 +276,26 @@ class App {
|
|||||||
</div>
|
</div>
|
||||||
<h2>{this.state.crash.message}</h2>
|
<h2>{this.state.crash.message}</h2>
|
||||||
<pre>{this.state.crash.error}</pre>
|
<pre>{this.state.crash.error}</pre>
|
||||||
|
<div className="actions">
|
||||||
|
<antd.Button onClick={() => window.location.reload()}>Reload</antd.Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.initialized) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{config.app.siteName}</title>
|
<title>{config.app.siteName}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<antd.ConfigProvider>
|
<antd.ConfigProvider>
|
||||||
<antd.Layout className="app_layout" style={{ height: "100%" }}>
|
<Layout user={this.state.user} >
|
||||||
<Drawer />
|
<BindPropsProvider
|
||||||
<Sidebar user={this.state.user} />
|
user={this.state.user}
|
||||||
<antd.Layout className="content_layout">
|
session={this.state.session}
|
||||||
<Header />
|
>
|
||||||
<antd.Layout.Content className="layout_page">
|
<Render.RouteRender staticRenders={App.staticRenders} />
|
||||||
<div className={classnames("fade-transverse-active", { "fade-transverse-leave": this.state.isOnTransition })}>
|
</BindPropsProvider>
|
||||||
<BindPropsProvider
|
</Layout>
|
||||||
user={this.state.user}
|
|
||||||
session={this.state.session}
|
|
||||||
>
|
|
||||||
<Render.RenderRouter staticRenders={App.staticRenders} />
|
|
||||||
</BindPropsProvider>
|
|
||||||
</div>
|
|
||||||
</antd.Layout.Content>
|
|
||||||
</antd.Layout>
|
|
||||||
<Sidedrawer />
|
|
||||||
</antd.Layout>
|
|
||||||
</antd.ConfigProvider>
|
</antd.ConfigProvider>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
@ -21,9 +21,9 @@ export class AboutCard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const eviteNamespace = window.__evite
|
const eviteNamespace = window.__evite ?? {}
|
||||||
const appConfig = config.app ?? {}
|
const appConfig = config.app ?? {}
|
||||||
const isDevMode = eviteNamespace.env.NODE_ENV !== "production"
|
const isDevMode = eviteNamespace?.env?.NODE_ENV !== "production"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<antd.Modal
|
<antd.Modal
|
||||||
@ -43,13 +43,13 @@ export class AboutCard extends React.Component {
|
|||||||
<h1>{appConfig.siteName}</h1>
|
<h1>{appConfig.siteName}</h1>
|
||||||
<div>
|
<div>
|
||||||
<antd.Tag>
|
<antd.Tag>
|
||||||
<Icons.Tag />v{eviteNamespace.projectVersion}
|
<Icons.Tag />v{eviteNamespace?.projectVersion ?? " experimental"}
|
||||||
</antd.Tag>
|
</antd.Tag>
|
||||||
<antd.Tag color="geekblue">eVite v{eviteNamespace.eviteVersion}</antd.Tag>
|
{eviteNamespace.eviteVersion &&
|
||||||
<antd.Tag color="green">
|
<antd.Tag color="geekblue">eVite v{eviteNamespace?.eviteVersion}</antd.Tag>}
|
||||||
<Icons.Hexagon /> v{eviteNamespace.versions.node}
|
{eviteNamespace.version?.node && <antd.Tag color="green">
|
||||||
</antd.Tag>
|
<Icons.Hexagon /> v{eviteNamespace?.versions?.node}
|
||||||
|
</antd.Tag>}
|
||||||
<antd.Tag color={isDevMode ? "magenta" : "green"}>
|
<antd.Tag color={isDevMode ? "magenta" : "green"}>
|
||||||
{isDevMode ? <Icons.Triangle /> : <Icons.CheckCircle />}
|
{isDevMode ? <Icons.Triangle /> : <Icons.CheckCircle />}
|
||||||
{isDevMode ? "development" : "stable"}
|
{isDevMode ? "development" : "stable"}
|
||||||
|
@ -3,9 +3,9 @@ import classnames from "classnames"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const { children, float } = props
|
const { children } = props
|
||||||
|
|
||||||
return <div style={props.style} className={classnames("actionsBar_card", { ["float"]: float })}>
|
return <div style={props.style} className={classnames("actionsBar_card", [props.mode])}>
|
||||||
<div style={props.wrapperStyle} className="actionsBar_flexWrapper">
|
<div style={props.wrapperStyle} className="actionsBar_flexWrapper">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,28 +1,73 @@
|
|||||||
|
@actionsBar_height: fit-content;
|
||||||
|
|
||||||
.actionsBar_card {
|
.actionsBar_card {
|
||||||
border: 1px solid #e0e0e0;
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
overflow-x: overlay;
|
||||||
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: @actionsBar_height;
|
||||||
|
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 200ms ease-in-out;
|
|
||||||
background-color: #0c0c0c15;
|
background-color: #0c0c0c15;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.float {
|
&.float {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fixedBottom {
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fixedTop {
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionsBar_flexWrapper {
|
.actionsBar_flexWrapper {
|
||||||
transition: all 200ms ease-in-out;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
margin-right: 10px;
|
margin: 0 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
17
packages/app/src/components/Clock/index.jsx
Normal file
17
packages/app/src/components/Clock/index.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const [time, setTime] = React.useState(new Date())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTime(new Date())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div className="clock">{time.toLocaleTimeString()}</div>
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export { default as Operations } from './operations'
|
|
@ -1,37 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import * as antd from "antd"
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
const api = window.app.request
|
|
||||||
|
|
||||||
export default class Operations extends React.Component {
|
|
||||||
state = {
|
|
||||||
list: []
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
await this.loadOperations()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOperations = async () => {
|
|
||||||
const operations = await api.get.operations()
|
|
||||||
console.log(operations)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderItem = (item) => {
|
|
||||||
console.log(item)
|
|
||||||
|
|
||||||
return <antd.List.Item>
|
|
||||||
{item}
|
|
||||||
</antd.List.Item>
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="operations">
|
|
||||||
<antd.List
|
|
||||||
dataSource={this.state.list}
|
|
||||||
renderItem={this.renderItem}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,480 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import * as antd from 'antd'
|
|
||||||
import { Icons as FIcons, createIconRender } from "components/Icons"
|
|
||||||
import * as MDIcons from "react-icons/md"
|
|
||||||
import loadable from "@loadable/component"
|
|
||||||
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
const Icons = {
|
|
||||||
...FIcons,
|
|
||||||
...MDIcons
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormComponents = {
|
|
||||||
"input": antd.Input,
|
|
||||||
"textarea": antd.Input.TextArea,
|
|
||||||
"select": antd.Select,
|
|
||||||
"datepicker": antd.DatePicker,
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestModifyByType = {
|
|
||||||
"vaultItem": {
|
|
||||||
"additions": ["essc"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIELDS
|
|
||||||
const FieldsForms = {
|
|
||||||
description: {
|
|
||||||
label: "Description",
|
|
||||||
component: "input",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
onUpdate: (update) => {
|
|
||||||
return update.target.value
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
minWidth: "300px",
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
placeholder: "Describe something...",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
operations: {
|
|
||||||
label: "Operations",
|
|
||||||
component: "input",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
onUpdate: (update) => {
|
|
||||||
return update.target.value
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
label: "Location",
|
|
||||||
component: "select",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
children: async () => {
|
|
||||||
const api = window.app.request
|
|
||||||
const regions = await api.get.regions()
|
|
||||||
|
|
||||||
return regions.map(region => {
|
|
||||||
return <antd.Select.Option value={region.name}>{region.name}</antd.Select.Option>
|
|
||||||
})
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
placeholder: "Select a location",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vaultItemTypeSelector: {
|
|
||||||
label: "Type",
|
|
||||||
component: "select",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
children: async () => {
|
|
||||||
let types = await import("schemas/vaultItemsTypes.json")
|
|
||||||
|
|
||||||
types = types.default || types
|
|
||||||
|
|
||||||
return Object.keys(types).map((group) => {
|
|
||||||
return <antd.Select.OptGroup key={group} label={String(group).toTitleCase()}>
|
|
||||||
{types[group].map((type) => {
|
|
||||||
return <antd.Select.Option key={type} value={`${group}-${type}`}>{String(type).toTitleCase()}</antd.Select.Option>
|
|
||||||
})}
|
|
||||||
</antd.Select.OptGroup>
|
|
||||||
})
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
placeholder: "Select a type",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vaultItemSerial: {
|
|
||||||
label: "Serial number",
|
|
||||||
component: "input",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
onUpdate: (update) => {
|
|
||||||
return update.target.value
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
placeholder: "S/N 00000000X",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vaultItemManufacturer: {
|
|
||||||
label: "Manufacturer",
|
|
||||||
component: "input",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
onUpdate: (update) => {
|
|
||||||
return update.target.value
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
placeholder: "e.g. Hewlett Packard",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vaultItemManufacturedYear: {
|
|
||||||
label: "Manufactured Year",
|
|
||||||
component: "datepicker",
|
|
||||||
updateEvent: "onChange",
|
|
||||||
onUpdate: (update) => {
|
|
||||||
return update.year()
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
picker: "year"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
//FORMULAS
|
|
||||||
const ProductFormula = {
|
|
||||||
defaultFields: [
|
|
||||||
"description",
|
|
||||||
"operations",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const OperationFormula = {
|
|
||||||
defaultFields: [
|
|
||||||
"description",
|
|
||||||
"task",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const PhaseFormula = {
|
|
||||||
defaultFields: [
|
|
||||||
"description",
|
|
||||||
"task",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const TaskFormula = {
|
|
||||||
defaultFields: [
|
|
||||||
"description",
|
|
||||||
"tasks",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const VaultItemFormula = {
|
|
||||||
defaultFields: [
|
|
||||||
// TODO: include location
|
|
||||||
"vaultItemTypeSelector",
|
|
||||||
"vaultItemSerial",
|
|
||||||
"vaultItemManufacturer",
|
|
||||||
"vaultItemManufacturedYear",
|
|
||||||
"location",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const FORMULAS = {
|
|
||||||
product: ProductFormula,
|
|
||||||
operation: OperationFormula,
|
|
||||||
phase: PhaseFormula,
|
|
||||||
task: TaskFormula,
|
|
||||||
vaultItem: VaultItemFormula,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TYPES
|
|
||||||
const FabricItemTypesIcons = {
|
|
||||||
"product": "Box",
|
|
||||||
"operation": "Settings",
|
|
||||||
"phase": "GitCommit",
|
|
||||||
"task": "Tool",
|
|
||||||
"vaultItem": "Archive",
|
|
||||||
}
|
|
||||||
|
|
||||||
const FabricItemTypes = ["product", "operation", "phase", "task", "vaultItem"]
|
|
||||||
|
|
||||||
export default class FabricCreator extends React.Component {
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
submitting: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
name: null,
|
|
||||||
type: null,
|
|
||||||
fields: [],
|
|
||||||
values: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
await this.setItemType(this.props.defaultType ?? "product")
|
|
||||||
this.setState({ loading: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
toogleLoading = (to) => {
|
|
||||||
this.setState({ loading: to ?? !this.state.loading })
|
|
||||||
}
|
|
||||||
|
|
||||||
toogleSubmitting = (to) => {
|
|
||||||
this.setState({ submitting: to ?? !this.state.submitting })
|
|
||||||
}
|
|
||||||
|
|
||||||
clearError = () => {
|
|
||||||
if (this.state.error != null) {
|
|
||||||
this.setState({ error: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearValues = async () => {
|
|
||||||
await this.setState({ values: {} })
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFields = async () => {
|
|
||||||
await this.setState({ fields: [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
setItemType = async (type) => {
|
|
||||||
const formulaKeys = Object.keys(FORMULAS)
|
|
||||||
|
|
||||||
if (formulaKeys.includes(type)) {
|
|
||||||
const formula = FORMULAS[type]
|
|
||||||
|
|
||||||
await this.clearValues()
|
|
||||||
await this.clearFields()
|
|
||||||
|
|
||||||
formula.defaultFields.forEach(field => {
|
|
||||||
this.appendFieldByType(field)
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.setState({ type: type, name: "New item" })
|
|
||||||
} else {
|
|
||||||
console.error(`Cannot load default fields from formula with type ${type}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appendFieldByType = (fieldType) => {
|
|
||||||
const field = FieldsForms[fieldType]
|
|
||||||
|
|
||||||
if (typeof field === "undefined") {
|
|
||||||
console.error(`No form available for field [${fieldType}]`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = this.state.fields
|
|
||||||
|
|
||||||
if (this.fieldsHasTypeKey(fieldType) && !field.allowMultiple) {
|
|
||||||
console.error(`Field [${fieldType}] already exists, and only can exists 1`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fields.push(this.generateFieldRender({ type: fieldType, ...field }))
|
|
||||||
|
|
||||||
this.setState({ fields: fields })
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldsHasTypeKey = (key) => {
|
|
||||||
let isOnFields = false
|
|
||||||
|
|
||||||
const fields = this.state.fields
|
|
||||||
|
|
||||||
fields.forEach(field => {
|
|
||||||
field.props.type === key ? isOnFields = true : null
|
|
||||||
})
|
|
||||||
|
|
||||||
return isOnFields
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFieldSelectorMenu = () => {
|
|
||||||
return <antd.Menu
|
|
||||||
onClick={(e) => {
|
|
||||||
this.appendFieldByType(e.key)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.keys(FieldsForms).map((key) => {
|
|
||||||
const field = FieldsForms[key]
|
|
||||||
const icon = field.icon && createIconRender(field.icon)
|
|
||||||
const disabled = this.fieldsHasTypeKey(key) && !field.allowMultiple
|
|
||||||
|
|
||||||
return <antd.Menu.Item disabled={disabled} key={key}>
|
|
||||||
{icon ?? null}
|
|
||||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
|
||||||
</antd.Menu.Item>
|
|
||||||
})}
|
|
||||||
</antd.Menu>
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTypeMenuSelector = () => {
|
|
||||||
return <antd.Menu
|
|
||||||
onClick={(e) => {
|
|
||||||
this.setItemType(e.key)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{FabricItemTypes.map((type) => {
|
|
||||||
const TypeIcon = FabricItemTypesIcons[type] && createIconRender(FabricItemTypesIcons[type])
|
|
||||||
|
|
||||||
return <antd.Menu.Item key={type}>
|
|
||||||
{TypeIcon ?? null}
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
||||||
</antd.Menu.Item>
|
|
||||||
})}
|
|
||||||
</antd.Menu>
|
|
||||||
}
|
|
||||||
|
|
||||||
onDone = async () => {
|
|
||||||
this.clearError()
|
|
||||||
this.toogleSubmitting(true)
|
|
||||||
|
|
||||||
const api = window.app.request
|
|
||||||
let properties = {}
|
|
||||||
|
|
||||||
this.getProperties().forEach((property) => {
|
|
||||||
if (typeof properties[property.type] !== "undefined") {
|
|
||||||
return properties[property.id] = property.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties[property.type] = property.value
|
|
||||||
})
|
|
||||||
|
|
||||||
let payload = {
|
|
||||||
type: this.state.type,
|
|
||||||
name: this.state.name,
|
|
||||||
properties: properties,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof requestModifyByType[this.state.type] !== "undefined") {
|
|
||||||
payload = {
|
|
||||||
...payload,
|
|
||||||
...requestModifyByType[this.state.type],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.put.fabric(payload).catch((response) => {
|
|
||||||
console.error(response)
|
|
||||||
this.setState({ error: response })
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
this.toogleSubmitting(false)
|
|
||||||
|
|
||||||
if (!this.state.error && typeof this.props.close === "function") {
|
|
||||||
this.props.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeName = (event) => {
|
|
||||||
this.setState({ name: event.target.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdateValue = (event, value) => {
|
|
||||||
const { updateEvent, key } = event
|
|
||||||
|
|
||||||
let state = this.state
|
|
||||||
state.values[key] = value
|
|
||||||
|
|
||||||
this.setState(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeField = (key) => {
|
|
||||||
let values = this.state.values
|
|
||||||
let fields = this.state.fields.filter(field => field.key != key)
|
|
||||||
|
|
||||||
delete values[key]
|
|
||||||
|
|
||||||
this.setState({ fields: fields, values: values })
|
|
||||||
}
|
|
||||||
|
|
||||||
getProperties = () => {
|
|
||||||
return this.state.fields.map((field) => {
|
|
||||||
return {
|
|
||||||
type: field.props.type,
|
|
||||||
id: field.props.id,
|
|
||||||
value: this.state.values[field.key],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeyFromLatestFieldType = (type) => {
|
|
||||||
let latestByType = 0
|
|
||||||
|
|
||||||
this.state.fields.forEach((field) => {
|
|
||||||
field.props.type === type ? latestByType++ : null
|
|
||||||
})
|
|
||||||
|
|
||||||
return `${type}-${latestByType}`
|
|
||||||
}
|
|
||||||
|
|
||||||
generateFieldRender = (field) => {
|
|
||||||
if (!field.key) {
|
|
||||||
field.key = this.getKeyFromLatestFieldType(field.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof FormComponents[field.component] === "undefined") {
|
|
||||||
console.error(`No component type available for field [${field.key}]`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSubmittingState = () => {
|
|
||||||
return this.state.submitting
|
|
||||||
}
|
|
||||||
|
|
||||||
let fieldComponentProps = {
|
|
||||||
...field.props,
|
|
||||||
value: this.state.values[field.key],
|
|
||||||
disabled: getSubmittingState(),
|
|
||||||
[field.updateEvent]: (...args) => {
|
|
||||||
if (typeof field.onUpdate === "function") {
|
|
||||||
return this.onUpdateValue({ updateEvent: field.updateEvent, key: field.key }, field.onUpdate(...args))
|
|
||||||
}
|
|
||||||
return this.onUpdateValue({ updateEvent: field.updateEvent, key: field.key }, ...args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let RenderComponent = null
|
|
||||||
|
|
||||||
if (typeof field.children === "function") {
|
|
||||||
RenderComponent = loadable(async () => {
|
|
||||||
try {
|
|
||||||
const children = await field.children()
|
|
||||||
return () => React.createElement(FormComponents[field.component], fieldComponentProps, children)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return ()=> <div>
|
|
||||||
<Icons.XCircle /> Load Error
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
fallback: <div>Loading...</div>,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
RenderComponent = () => React.createElement(FormComponents[field.component], fieldComponentProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div key={field.key} id={`${field.type}-${field.key}`} type={field.type} className="field" style={field.style}>
|
|
||||||
<div className="close" onClick={() => { this.removeField(field.key) }}><Icons.X /></div>
|
|
||||||
<h4>{field.icon && createIconRender(field.icon)}{field.label}</h4>
|
|
||||||
<div className="fieldContent">
|
|
||||||
<RenderComponent />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.loading) {
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
const TypeIcon = FabricItemTypesIcons[this.state.type] && createIconRender(FabricItemTypesIcons[this.state.type])
|
|
||||||
|
|
||||||
return <div className="fabric_creator">
|
|
||||||
<div key="name" className="name">
|
|
||||||
<div className="type">
|
|
||||||
<antd.Dropdown trigger={['click']} overlay={this.renderTypeMenuSelector}>
|
|
||||||
{TypeIcon ?? <Icons.HelpCircle />}
|
|
||||||
</antd.Dropdown>
|
|
||||||
</div>
|
|
||||||
<antd.Input defaultValue={this.state.name} onChange={this.onChangeName} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="fields">
|
|
||||||
<div className="wrap">
|
|
||||||
{this.state.submitting ? <antd.Skeleton active /> : this.state.fields}
|
|
||||||
</div>
|
|
||||||
<div className="bottom_actions">
|
|
||||||
<antd.Dropdown trigger={['click']} placement="topCenter" overlay={this.renderFieldSelectorMenu}>
|
|
||||||
<Icons.Plus />
|
|
||||||
</antd.Dropdown>
|
|
||||||
|
|
||||||
<antd.Button loading={this.state.submitting} onClick={this.onDone}>Done</antd.Button>
|
|
||||||
</div>
|
|
||||||
{this.state.error && <div className="error">
|
|
||||||
{this.state.error}
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
.fabric_creator {
|
|
||||||
.name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 45px;
|
|
||||||
|
|
||||||
height: fit-content;
|
|
||||||
line-height: 0;
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: unset;
|
|
||||||
line-height: 0;
|
|
||||||
font-size: 45px;
|
|
||||||
height: fit-content;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
color: #5e5e5e;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin: 0!important;
|
|
||||||
padding-right: 0!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type {
|
|
||||||
border-radius: 10px;
|
|
||||||
height: fit-content;
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type:hover{
|
|
||||||
background-color: antiquewhite;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin: 10px;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px;
|
|
||||||
border: transparent dashed 1px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-out;
|
|
||||||
|
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
.fieldContent {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
color: #8d8d8dc4;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
transform: translate(-85%, -100%);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 150ms ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.field:hover {
|
|
||||||
border-color: #8d8d8dc4;
|
|
||||||
|
|
||||||
.close {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom_actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100px;
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
font-size: 25px;
|
|
||||||
color: #5e5e5e;
|
|
||||||
border: transparent solid 1px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin: 0!important;
|
|
||||||
padding-right: 0!important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom_actions:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
border-color: #5e5e5e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 5px;
|
|
||||||
transition: all 150ms ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import QrReader from "react-qr-reader"
|
|
||||||
import { Window } from "components"
|
|
||||||
|
|
||||||
export class Reader extends React.Component {
|
|
||||||
state = {
|
|
||||||
delay: 100,
|
|
||||||
result: "No result",
|
|
||||||
}
|
|
||||||
qrReaderRef = React.createRef()
|
|
||||||
|
|
||||||
handleScan = (data) => {
|
|
||||||
this.setState({
|
|
||||||
result: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleError = (err) => {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
openImageDialog = () => {
|
|
||||||
this.qrReaderRef.current.openImageDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const previewStyle = {
|
|
||||||
height: 240,
|
|
||||||
width: 320,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input type="button" value="Submit QR Code" onClick={this.openImageDialog} />
|
|
||||||
<p>{this.state.result}</p>
|
|
||||||
|
|
||||||
<QrReader
|
|
||||||
ref={this.qrReaderRef}
|
|
||||||
delay={this.state.delay}
|
|
||||||
style={previewStyle}
|
|
||||||
onError={this.handleError}
|
|
||||||
onScan={this.handleScan}
|
|
||||||
legacyMode
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openModal() {
|
|
||||||
new Window.DOMWindow({ id: "QRScanner", children: Reader }).create()
|
|
||||||
}
|
|
85
packages/app/src/components/ScheduledProgress/index.jsx
Normal file
85
packages/app/src/components/ScheduledProgress/index.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from "react"
|
||||||
|
import moment from "moment"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const defaultDateFormat = "DD-MM-YYYY hh:mm"
|
||||||
|
|
||||||
|
export default class ScheduledProgress extends React.Component {
|
||||||
|
isDateReached = (date) => {
|
||||||
|
const format = this.props.dateFormat ?? defaultDateFormat
|
||||||
|
const now = moment().format(format)
|
||||||
|
const result = moment(date, format).isSameOrBefore(moment(now, format))
|
||||||
|
|
||||||
|
console.debug(`[${date}] is before [${now}] => ${result}`)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiffBetweenDates = (start, end) => {
|
||||||
|
// THIS IS NOT COUNTING WITH THE YEAR
|
||||||
|
const format = "DD-MM-YYYY"
|
||||||
|
|
||||||
|
const startDate = moment(start, format)
|
||||||
|
const endDate = moment(end, format)
|
||||||
|
const now = moment().format(format)
|
||||||
|
|
||||||
|
// count days will took to complete
|
||||||
|
const days = endDate.diff(startDate, "days")
|
||||||
|
|
||||||
|
const daysLeft = endDate.diff(moment(now, format), "days")
|
||||||
|
const daysPassed = moment(now, format).diff(startDate, "days")
|
||||||
|
|
||||||
|
let percentage = 0
|
||||||
|
|
||||||
|
switch (daysLeft) {
|
||||||
|
case 0: {
|
||||||
|
percentage = 99
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
percentage = 95
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (daysPassed > 0 && daysPassed < days) {
|
||||||
|
percentage = (daysPassed / days) * 100
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysPassed > days) {
|
||||||
|
percentage = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return { daysLeft, daysPassed, percentage }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const startReached = this.isDateReached(this.props.start)
|
||||||
|
const finishReached = this.isDateReached(this.props.finish)
|
||||||
|
const datesDiff = this.getDiffBetweenDates(this.props.start, this.props.finish)
|
||||||
|
|
||||||
|
return <div className="scheduled_progress">
|
||||||
|
<div className={classnames("scheduled_progress point", "scheduled_progress point left", { ["reached"]: startReached })}>
|
||||||
|
{this.props.start}
|
||||||
|
</div>
|
||||||
|
<antd.Progress
|
||||||
|
size="small"
|
||||||
|
percent={datesDiff.percentage}
|
||||||
|
showInfo={false}
|
||||||
|
className={classnames("ant-progress", {
|
||||||
|
startReached: startReached,
|
||||||
|
finishReached: finishReached,
|
||||||
|
})}
|
||||||
|
type="line"
|
||||||
|
/>
|
||||||
|
<div className={classnames("point", "right", { reached: finishReached })}>
|
||||||
|
{this.props.finish}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
68
packages/app/src/components/ScheduledProgress/index.less
Normal file
68
packages/app/src/components/ScheduledProgress/index.less
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
.scheduled_progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.point {
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: end;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
width: 72px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
transform: translate(50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reached {
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-progress::before {
|
||||||
|
content: "";
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 24px;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
background-color: rgb(128, 128, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-progress::after {
|
||||||
|
content: "";
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 24px;
|
||||||
|
transform: translateX(10px);
|
||||||
|
background-color: rgb(128, 128, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-progress {
|
||||||
|
font-size: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
&.startReached {
|
||||||
|
&::before {
|
||||||
|
background-color: var(--primaryColor) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.finishReached {
|
||||||
|
&::after {
|
||||||
|
background-color: var(--primaryColor) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-progress-bg {
|
||||||
|
background-color: var(--primaryColor);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
import { ActionsBar } from "components"
|
|
||||||
import { List, Button } from "antd"
|
import { List, Button } from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
|
||||||
@ -27,6 +26,12 @@ export default class SelectableList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unselectAll = () => {
|
||||||
|
this.setState({
|
||||||
|
selectedKeys: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectKey = (key) => {
|
selectKey = (key) => {
|
||||||
let list = this.state.selectedKeys ?? []
|
let list = this.state.selectedKeys ?? []
|
||||||
list.push(key)
|
list.push(key)
|
||||||
@ -44,9 +49,7 @@ export default class SelectableList extends React.Component {
|
|||||||
this.props.onDone(this.state.selectedKeys)
|
this.props.onDone(this.state.selectedKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.unselectAll()
|
||||||
selectedKeys: [],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDiscard = () => {
|
onDiscard = () => {
|
||||||
@ -54,9 +57,7 @@ export default class SelectableList extends React.Component {
|
|||||||
this.props.onDiscard(this.state.selectedKeys)
|
this.props.onDiscard(this.state.selectedKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.unselectAll()
|
||||||
selectedKeys: [],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
@ -69,131 +70,131 @@ export default class SelectableList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderActions = () => {
|
renderProvidedActions = () => {
|
||||||
if (typeof this.props.renderActions !== "undefined" && !this.props.renderActions) {
|
return this.props.actions.map((action) => {
|
||||||
return false
|
return (
|
||||||
}
|
<div key={action.key}>
|
||||||
if (this.state.selectedKeys.length === 0) {
|
<Button
|
||||||
return false
|
style={{
|
||||||
}
|
...action.props.style,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (typeof action.onClick === "function") {
|
||||||
|
action.onClick(this.state.selectedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
const renderProvidedActions = () => {
|
if (typeof this.props[action.props.call] !== "undefined") {
|
||||||
if (Array.isArray(this.props.actions)) {
|
if (typeof this.props[action.props.call] === "function") {
|
||||||
return this.props.actions.map((action) => {
|
let data = this.state.selectedKeys // by default send selectedKeys
|
||||||
return (
|
|
||||||
<div key={action.key}>
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
...action.props.style,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (typeof action.onClick === "function") {
|
|
||||||
action.onClick(this.state.selectedKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.props[action.props.call] !== "undefined") {
|
if (typeof action.props.sendData === "string") {
|
||||||
if (typeof this.props[action.props.call] === "function") {
|
switch (action.props.sendData) {
|
||||||
let data = this.state.selectedKeys // by default send selectedKeys
|
case "keys": {
|
||||||
|
data = this.state.selectedKeys
|
||||||
if (typeof action.props.sendData === "string") {
|
}
|
||||||
switch (action.props.sendData) {
|
default: {
|
||||||
case "keys": {
|
data = this.state.selectedKeys
|
||||||
data = this.state.selectedKeys
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
data = this.state.selectedKeys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props[action.props.call](data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
>
|
this.props[action.props.call](data)
|
||||||
{action}
|
}
|
||||||
</Button>
|
}
|
||||||
</div>
|
}}
|
||||||
)
|
>
|
||||||
})
|
{action}
|
||||||
}
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions = () => {
|
||||||
|
if (this.props.actionsDisabled) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className={classnames("selectableList_bottomActions", { ["mobile"]: window.isMobile && !this.props.ignoreMobileActions })}>
|
||||||
<div className="bottomActions_wrapper">
|
<div key="discard">
|
||||||
<ActionsBar style={{ borderRadius: "8px 8px 0 0", width: "fit-content" }}>
|
<Button
|
||||||
<div key="discard">
|
shape="round"
|
||||||
<Button
|
onClick={this.onDiscard}
|
||||||
shape="round"
|
{...this.props.onDiscardProps}
|
||||||
onClick={this.onDiscard}
|
>
|
||||||
{...this.props.onDiscardProps}
|
{this.props.onDiscardRender ?? <Icons.X />}
|
||||||
>
|
Discard
|
||||||
{this.props.onDiscardRender ?? <Icons.X />}
|
</Button>
|
||||||
Discard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{renderProvidedActions()}
|
|
||||||
</ActionsBar>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
{Array.isArray(this.props.actions) && this.renderProvidedActions()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
isKeySelected = (key) => {
|
||||||
|
return this.state.selectedKeys.includes(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = (item) => {
|
||||||
|
if (item.children) {
|
||||||
|
return <div className="selectableList_group">
|
||||||
|
{item.label}
|
||||||
|
<div className="selectableList_subItems">
|
||||||
|
{item.children.map((subItem) => {
|
||||||
|
return this.renderItem(subItem)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderChildren = this.props.renderItem(item)
|
||||||
|
|
||||||
|
const _key = item.key ?? item.id ?? item._id ?? renderChildren.key
|
||||||
|
|
||||||
|
const selectionMethod = ["onClick", "onDoubleClick"].includes(this.props.selectionMethod) ? this.props.selectionMethod : "onClick"
|
||||||
|
const isSelected = this.isKeySelected(_key)
|
||||||
|
const isDisabled = renderChildren.props.disabled
|
||||||
|
const isNotSelectable = renderChildren.props.notSelectable
|
||||||
|
|
||||||
|
let renderProps = {
|
||||||
|
disabled: isDisabled,
|
||||||
|
children: renderChildren,
|
||||||
|
className: classnames("selectableList_item", {
|
||||||
|
["selected"]: isSelected && !isNotSelectable,
|
||||||
|
["disabled"]: isDisabled && !isNotSelectable,
|
||||||
|
}),
|
||||||
|
[selectionMethod]: () => {
|
||||||
|
if (isDisabled && isNotSelectable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof this.props.selectionEnabled !== "undefined") {
|
||||||
|
if (!Boolean(this.props.selectionEnabled)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
this.unselectKey(_key)
|
||||||
|
} else {
|
||||||
|
this.selectKey(_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionMethod == "onDoubleClick") {
|
||||||
|
renderProps.onClick = () => {
|
||||||
|
if (this.state.selectedKeys.length > 0) {
|
||||||
|
if (isSelected) {
|
||||||
|
this.unselectKey(_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={_key} {...renderProps} />
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const validSelectionMethods = ["onClick", "onDoubleClick"]
|
const { borderer, grid, header, loadMore, locale, pagination, rowKey, size, split, itemLayout, loading } = this.props
|
||||||
|
|
||||||
const renderMethod = (item) => {
|
|
||||||
const selectionMethod = validSelectionMethods.includes(this.props.selectionMethod) ? this.props.selectionMethod : "onClick"
|
|
||||||
|
|
||||||
if (typeof this.props.renderItem === "function") {
|
|
||||||
const _key = item.key ?? item.id ?? item._id
|
|
||||||
const list = this.state.selectedKeys
|
|
||||||
const isSelected = list.includes(_key)
|
|
||||||
|
|
||||||
let props = {
|
|
||||||
key: _key,
|
|
||||||
id: _key,
|
|
||||||
className: classnames("selectableList_item", this.props.itemClassName, {
|
|
||||||
selected: this.state.selectedKeys.includes(_key),
|
|
||||||
}),
|
|
||||||
[selectionMethod]: () => {
|
|
||||||
if (typeof this.props.selectionEnabled !== "undefined") {
|
|
||||||
if (!Boolean(this.props.selectionEnabled)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
this.unselectKey(_key)
|
|
||||||
} else {
|
|
||||||
this.selectKey(_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionMethod == "onDoubleClick") {
|
|
||||||
props.onClick = () => {
|
|
||||||
if (list.length > 0) {
|
|
||||||
if (isSelected) {
|
|
||||||
this.unselectKey(_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...props}>
|
|
||||||
{this.props.renderItem(item)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("renderItem method is not defined!")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { borderer, grid, header, loadMore, locale, pagination, rowKey, size, split, itemLayout, loading } =
|
|
||||||
this.props
|
|
||||||
const listProps = {
|
const listProps = {
|
||||||
borderer,
|
borderer,
|
||||||
grid,
|
grid,
|
||||||
@ -208,18 +209,25 @@ export default class SelectableList extends React.Component {
|
|||||||
loading,
|
loading,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (this.state.selectedKeys.length === 0) {
|
||||||
<div className={classnames("selectableList", { ["selectionEnabled"]: this.props.selectionEnabled })}>
|
if (window.isMobile && !this.props.ignoreMobileActions) {
|
||||||
<List
|
window.app.BottomBarController.clear()
|
||||||
{...listProps}
|
}
|
||||||
dataSource={[
|
} else {
|
||||||
...(Array.isArray(this.props.items) ? this.props.items : []),
|
if (window.isMobile && !this.props.ignoreMobileActions) {
|
||||||
...(Array.isArray(this.props.children) ? this.props.children : []),
|
window.app.BottomBarController.render(this.renderActions())
|
||||||
]}
|
}
|
||||||
renderItem={renderMethod}
|
}
|
||||||
/>
|
|
||||||
{this.renderActions()}
|
return <div className={classnames("selectableList", { ["selectionEnabled"]: this.props.selectionEnabled })}>
|
||||||
|
<List
|
||||||
|
{...listProps}
|
||||||
|
dataSource={this.props.items}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
/>
|
||||||
|
<div className="selectableList_bottomActions_wrapper">
|
||||||
|
{this.props.ignoreMobileActions && this.renderActions()}
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,9 +6,9 @@
|
|||||||
.selectableList_item {
|
.selectableList_item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border: rgba(51, 51, 51, 0.3) 1px solid;
|
border: rgba(51, 51, 51, 0.3) 1px solid;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -24,42 +24,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectableList_group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.selectableList_subItems {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.selectableList_item {
|
.selectableList_item {
|
||||||
|
display: inline-flex;
|
||||||
|
overflow-x: overlay;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
border: @selectableList_item_borderColor_normal 1px solid;
|
border: @selectableList_item_borderColor_normal 1px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
margin-bottom: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
> div {
|
margin-bottom: 6px;
|
||||||
margin: 7px;
|
padding: 7px;
|
||||||
}
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
transform: translate(10px, 0);
|
transform: translate(10px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottomActions_wrapper {
|
.selectableList_bottomActions_wrapper {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 1000;
|
z-index: 300;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectableList_bottomActions {
|
||||||
|
width: fit-content;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
background-color: #0c0c0c15;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -87,6 +87,10 @@ export default class SettingsMenu extends React.Component {
|
|||||||
item.props.onClick = (event) => this.handleEvent(event, item)
|
item.props.onClick = (event) => this.handleEvent(event, item)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "select": {
|
||||||
|
item.props.onChange = (event) => this.handleEvent(event, item)
|
||||||
|
break
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
if (!item.props.children) {
|
if (!item.props.children) {
|
||||||
item.props.children = item.title ?? item.id
|
item.props.children = item.title ?? item.id
|
||||||
@ -101,10 +105,11 @@ export default class SettingsMenu extends React.Component {
|
|||||||
<div key={item.id} className="settingItem">
|
<div key={item.id} className="settingItem">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div>
|
<div>
|
||||||
<h5>
|
<h4>
|
||||||
{item.icon ? React.createElement(Icons[item.icon]) : null}
|
{Icons[item.icon] ? React.createElement(Icons[item.icon]) : null}
|
||||||
{item.title ?? item.id}
|
{item.title ?? item.id}
|
||||||
</h5>
|
</h4>
|
||||||
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{item.experimental && <antd.Tag> Experimental </antd.Tag>}
|
{item.experimental && <antd.Tag> Experimental </antd.Tag>}
|
||||||
@ -151,7 +156,7 @@ export default class SettingsMenu extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isDevMode = window.__evite.env.NODE_ENV !== "production"
|
const isDevMode = window.__evite?.env?.NODE_ENV !== "production"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
@ -161,7 +166,7 @@ export default class SettingsMenu extends React.Component {
|
|||||||
<div>{config.app?.siteName}</div>
|
<div>{config.app?.siteName}</div>
|
||||||
<div>
|
<div>
|
||||||
<antd.Tag>
|
<antd.Tag>
|
||||||
<Icons.Tag />v{window.__evite.projectVersion}
|
<Icons.Tag />v{window.__evite?.projectVersion}
|
||||||
</antd.Tag>
|
</antd.Tag>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -28,7 +28,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
h5{
|
h4{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--background-color-contrast);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
120
packages/app/src/components/UserSelector/index.jsx
Normal file
120
packages/app/src/components/UserSelector/index.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
import { SelectableList } from "components"
|
||||||
|
import { debounce } from "lodash"
|
||||||
|
import fuse from "fuse.js"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class UserSelector extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: [],
|
||||||
|
searchValue: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
api = window.app.request
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
this.toogleLoading(true)
|
||||||
|
await this.fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleLoading = (to) => {
|
||||||
|
this.setState({ loading: to ?? !this.state.loading })
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUsers = async () => {
|
||||||
|
const data = await this.api.get.users(undefined, { select: this.props.select }).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
antd.message.error("Error fetching operators")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({ data: data, loading: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
isExcludedId = (id) => {
|
||||||
|
if (this.props.excludedIds) {
|
||||||
|
return this.props.excludedIds.includes(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = (item) => {
|
||||||
|
return <div disabled={this.isExcludedId(item._id)} className="users_selector item">
|
||||||
|
<div>
|
||||||
|
<antd.Avatar shape="square" src={item.avatar} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>{item.fullName ?? item.username}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
search = (value) => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
if (typeof value.target?.value === "string") {
|
||||||
|
value = value.target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
return this.setState({ searchValue: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
const searcher = new fuse(this.state.data, {
|
||||||
|
includeScore: true,
|
||||||
|
keys: ["username", "fullName"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = searcher.search(value)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
searchValue: result.map((entry) => {
|
||||||
|
return entry.item
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedSearch = debounce((value) => this.search(value), 500)
|
||||||
|
|
||||||
|
onSearch = (event) => {
|
||||||
|
if (event === "" && this.state.searchValue) {
|
||||||
|
return this.setState({ searchValue: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debouncedSearch(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="users_selector">
|
||||||
|
<div className="users_selector header">
|
||||||
|
<div>
|
||||||
|
<antd.Input.Search
|
||||||
|
placeholder="Search"
|
||||||
|
allowClear
|
||||||
|
onSearch={this.onSearch}
|
||||||
|
onChange={this.onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SelectableList
|
||||||
|
ignoreMobileActions
|
||||||
|
items={this.state.searchValue ?? this.state.data}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
actions={[
|
||||||
|
<div call="onDone" key="done">
|
||||||
|
Done
|
||||||
|
</div>
|
||||||
|
]}
|
||||||
|
onDone={(keys) => this.props.handleDone(keys)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
19
packages/app/src/components/UserSelector/index.less
Normal file
19
packages/app/src/components/UserSelector/index.less
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.users_selector {
|
||||||
|
.header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,10 +8,11 @@ export { default as Sessions } from "./Sessions"
|
|||||||
export { default as ActionsBar } from "./ActionsBar"
|
export { default as ActionsBar } from "./ActionsBar"
|
||||||
export { default as SelectableList } from "./SelectableList"
|
export { default as SelectableList } from "./SelectableList"
|
||||||
export { default as ObjectInspector } from "./ObjectInspector"
|
export { default as ObjectInspector } from "./ObjectInspector"
|
||||||
export { default as FabricCreator } from "./FabricCreator"
|
|
||||||
export { default as ServerStatus } from "./ServerStatus"
|
export { default as ServerStatus } from "./ServerStatus"
|
||||||
export { default as ModifierTag } from "./ModifierTag"
|
export { default as ModifierTag } from "./ModifierTag"
|
||||||
|
export { default as UserSelector } from "./UserSelector"
|
||||||
|
export { default as Clock } from "./Clock"
|
||||||
|
export { default as ScheduledProgress } from "./ScheduledProgress"
|
||||||
|
|
||||||
export * as AboutApp from "./AboutApp"
|
export * as AboutApp from "./AboutApp"
|
||||||
export * as QRReader from "./QRReader"
|
|
||||||
export * as Window from "./RenderWindow"
|
export * as Window from "./RenderWindow"
|
@ -1,2 +0,0 @@
|
|||||||
export { default as SettingsController } from './settings'
|
|
||||||
export { default as SidebarController } from './sidebar'
|
|
@ -1,82 +1,101 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import loadable from "@loadable/component"
|
import loadable from "@loadable/component"
|
||||||
import resolve from "pages"
|
import routes from "virtual:generated-pages"
|
||||||
|
import progressBar from "nprogress"
|
||||||
|
|
||||||
|
import NotFound from "./statics/404"
|
||||||
|
|
||||||
export const ConnectWithApp = (component) => {
|
export const ConnectWithApp = (component) => {
|
||||||
return window.app.bindContexts(component)
|
return window.app.bindContexts(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetRoutesMap() {
|
export function GetRoutesMap() {
|
||||||
const jsxFiles = import.meta.glob('/src/pages/**/**.jsx')
|
return routes.map((route) => {
|
||||||
const tsxFiles = import.meta.glob('/src/pages/**/**.tsx')
|
const { path } = route
|
||||||
|
route.name =
|
||||||
|
path
|
||||||
|
.replace(/^\//, "")
|
||||||
|
.replace(/:/, "")
|
||||||
|
.replace(/\//, "-")
|
||||||
|
.replace("all(.*)", "not-found") || "home"
|
||||||
|
|
||||||
return { ...jsxFiles, ...tsxFiles }
|
route.path = route.path.includes("*") ? "*" : route.path
|
||||||
|
|
||||||
|
return route
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetRoutesComponentMap() {
|
||||||
|
return routes.reduce((acc, route) => {
|
||||||
|
const { path, component } = route
|
||||||
|
|
||||||
|
acc[path] = component
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RouteRender extends React.Component {
|
||||||
|
state = {
|
||||||
|
routes: GetRoutesComponentMap() ?? {},
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLocation = null
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.app.eventBus.on("locationChange", (event) => {
|
||||||
|
console.debug("[App] LocationChange, forcing update render...")
|
||||||
|
|
||||||
|
// render controller needs an better method for update render, this is a temporary solution
|
||||||
|
// FIXME: this event is called multiple times. we need to debug them methods
|
||||||
|
if (typeof this.forceUpdate === "function") {
|
||||||
|
this.forceUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(info, stack) {
|
||||||
|
this.setState({ error: { info, stack } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
// if (this.lastLocation.pathname !== window.location.pathname) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.lastLocation = window.location
|
||||||
|
|
||||||
|
let path = this.props.path ?? window.location.pathname
|
||||||
|
let componentModule = this.state.routes[path] ?? this.props.staticRenders.NotFound ?? NotFound
|
||||||
|
|
||||||
|
console.debug(`[RouteRender] Rendering ${path}`)
|
||||||
|
|
||||||
|
if (this.state.error) {
|
||||||
|
if (this.props.staticRenders?.RenderError) {
|
||||||
|
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.error })
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(this.state.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement(ConnectWithApp(componentModule), this.props)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LazyRouteRender = (props) => {
|
export const LazyRouteRender = (props) => {
|
||||||
const component = loadable(async () => {
|
const component = loadable(async () => {
|
||||||
const location = window.location
|
// TODO: Support evite async component initializations
|
||||||
let path = props.path ?? location.pathname
|
|
||||||
|
|
||||||
if (path.startsWith("/")) {
|
return RouteRender
|
||||||
path = path.substring(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = resolve(path)
|
|
||||||
console.log(src)
|
|
||||||
|
|
||||||
let module = await import(src).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
return props.staticRenders?.NotFound ?? import("./statics/404")
|
|
||||||
})
|
|
||||||
module = module.default || module
|
|
||||||
|
|
||||||
return class extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(info, stack) {
|
|
||||||
this.setState({ error: { info, stack } })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
if (props.staticRenders?.RenderError) {
|
|
||||||
return React.createElement(props.staticRenders?.RenderError, { error: this.state.error })
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(this.state.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.createElement(ConnectWithApp(module), props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return React.createElement(component)
|
return React.createElement(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RenderRouter extends React.Component {
|
|
||||||
lastPathname = null
|
|
||||||
lastHistoryState = null
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
if (this.lastPathname !== window.location.pathname || this.lastHistoryState !== window.app.history.location.state) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.lastPathname = window.location.pathname
|
|
||||||
this.lastHistoryState = window.app.history.location.state
|
|
||||||
|
|
||||||
return LazyRouteRender({ ...this.props, path: this.lastPathname })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extension = {
|
export const extension = {
|
||||||
key: "customRender",
|
key: "customRender",
|
||||||
expose: [
|
expose: [
|
||||||
@ -125,25 +144,30 @@ export const extension = {
|
|||||||
async (app, main) => {
|
async (app, main) => {
|
||||||
const defaultTransitionDelay = 150
|
const defaultTransitionDelay = 150
|
||||||
|
|
||||||
|
main.progressBar = progressBar.configure({ parent: "html", showSpinner: false })
|
||||||
|
|
||||||
main.history.listen((event) => {
|
main.history.listen((event) => {
|
||||||
main.eventBus.emit("setLocationDone")
|
main.eventBus.emit("transitionDone", event)
|
||||||
|
main.eventBus.emit("locationChange", event)
|
||||||
|
main.progressBar.done()
|
||||||
})
|
})
|
||||||
|
|
||||||
main.history.setLocation = (to, state) => {
|
main.history.setLocation = (to, state, delay) => {
|
||||||
const lastLocation = main.history.lastLocation
|
const lastLocation = main.history.lastLocation
|
||||||
|
|
||||||
if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) {
|
if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
main.eventBus.emit("setLocation")
|
main.progressBar.start()
|
||||||
|
main.eventBus.emit("transitionStart", delay)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
main.history.push({
|
main.history.push({
|
||||||
pathname: to,
|
pathname: to,
|
||||||
}, state)
|
}, state)
|
||||||
main.history.lastLocation = main.history.location
|
main.history.lastLocation = main.history.location
|
||||||
}, defaultTransitionDelay)
|
}, delay ?? defaultTransitionDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
main.setToWindowContext("setLocation", main.history.setLocation)
|
main.setToWindowContext("setLocation", main.history.setLocation)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Howl } from "howler"
|
import { Howl } from "howler"
|
||||||
|
import config from "config"
|
||||||
|
|
||||||
export class SoundEngine {
|
export class SoundEngine {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -10,10 +11,8 @@ export class SoundEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSounds = async () => {
|
getSounds = async () => {
|
||||||
const origin = process.env.NODE_ENV === "development" ? `${window.location.origin}/src/assets/sounds/index.js` : `${window.location.origin}/assets/sounds/index.js`
|
// TODO: Load custom soundpacks manifests
|
||||||
|
let soundPack = config.defaultSoundPack ?? {}
|
||||||
let soundPack = await import(origin)
|
|
||||||
soundPack = soundPack.default || soundPack
|
|
||||||
|
|
||||||
Object.keys(soundPack).forEach((key) => {
|
Object.keys(soundPack).forEach((key) => {
|
||||||
const src = soundPack[key]
|
const src = soundPack[key]
|
||||||
@ -29,6 +28,8 @@ export class SoundEngine {
|
|||||||
play = (name) => {
|
play = (name) => {
|
||||||
if (this.sounds[name]) {
|
if (this.sounds[name]) {
|
||||||
this.sounds[name].play()
|
this.sounds[name].play()
|
||||||
|
} else {
|
||||||
|
console.error(`Sound ${name} not found.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
packages/app/src/i18n/index.js
Normal file
57
packages/app/src/i18n/index.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from './locales'
|
||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
|
export {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
|
SUPPORTED_LOCALES,
|
||||||
|
SUPPORTED_LANGUAGES,
|
||||||
|
extractLocaleFromPath,
|
||||||
|
} from './locales'
|
||||||
|
|
||||||
|
// This is a dynamic import so not all languages are bundled in frontend.
|
||||||
|
// For YAML format, install `@rollup/plugin-yaml`.
|
||||||
|
const messageImports = import.meta.glob('./translations/*.json')
|
||||||
|
|
||||||
|
function importLocale(locale) {
|
||||||
|
const [, importLocale] =
|
||||||
|
Object.entries(messageImports).find(([key]) =>
|
||||||
|
key.includes(`/${locale}.`)
|
||||||
|
) || []
|
||||||
|
|
||||||
|
return importLocale && importLocale()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAsyncLanguage(i18n, locale = DEFAULT_LOCALE) {
|
||||||
|
try {
|
||||||
|
const result = await importLocale(locale)
|
||||||
|
if (result) {
|
||||||
|
i18n.addResourceBundle(locale, 'translation', result.default || result)
|
||||||
|
i18n.changeLanguage(locale)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function installI18n(locale = '') {
|
||||||
|
locale = SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE
|
||||||
|
const messages = await importLocale(locale)
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
|
.init({
|
||||||
|
// debug: true,
|
||||||
|
resources: {
|
||||||
|
// @ts-ignore
|
||||||
|
[locale]: { translation: messages.default || messages },
|
||||||
|
},
|
||||||
|
lng: locale,
|
||||||
|
fallbackLng: DEFAULT_LOCALE,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // react already safes from xss
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default i18n
|
18
packages/app/src/i18n/locales.js
Normal file
18
packages/app/src/i18n/locales.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const SUPPORTED_LANGUAGES = [
|
||||||
|
{
|
||||||
|
locale: 'en',
|
||||||
|
name: 'English',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SUPPORTED_LOCALES = SUPPORTED_LANGUAGES.map((l) => l.locale)
|
||||||
|
|
||||||
|
export const DEFAULT_LANGUAGE = SUPPORTED_LANGUAGES.find((l) => l.default)
|
||||||
|
|
||||||
|
export const DEFAULT_LOCALE = DEFAULT_LANGUAGE?.locale
|
||||||
|
|
||||||
|
export function extractLocaleFromPath(path = '') {
|
||||||
|
const [_, maybeLocale] = path.split('/')
|
||||||
|
return SUPPORTED_LOCALES.includes(maybeLocale) ? maybeLocale : DEFAULT_LOCALE
|
||||||
|
}
|
88
packages/app/src/i18n/translations/en.json
Normal file
88
packages/app/src/i18n/translations/en.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"welcome": "Welcome back, {{username}}",
|
||||||
|
"assigned_for_you": "Assigned for you",
|
||||||
|
},
|
||||||
|
"generics": {
|
||||||
|
"new": "New",
|
||||||
|
"close": "Close",
|
||||||
|
"done": "Done",
|
||||||
|
"edit": "Edit",
|
||||||
|
"select": "Select",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"import": "Import",
|
||||||
|
"export": "Export",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"reload": "Reload",
|
||||||
|
"search": "Search",
|
||||||
|
"status": "Status",
|
||||||
|
"type": "Type",
|
||||||
|
"about": "About",
|
||||||
|
"current": "Current",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"name": "Name",
|
||||||
|
"username": "Username",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"sessions": "Sessions",
|
||||||
|
"compact_view": "Compact view",
|
||||||
|
"about_us": "About us",
|
||||||
|
"sign_in": "Sign in",
|
||||||
|
"sign_out": "Sign out",
|
||||||
|
"sign_up": "Sign up",
|
||||||
|
"all_sessions": "All sessions",
|
||||||
|
"destroy_all_sessions": "Destroy all sessions",
|
||||||
|
"account_info": "Account information",
|
||||||
|
"password_confirmation": "Password confirmation",
|
||||||
|
},
|
||||||
|
"fabric": {
|
||||||
|
"new_product": "New product",
|
||||||
|
"description": "Description",
|
||||||
|
"describe_something": "Describe something...",
|
||||||
|
"operations": "Operations",
|
||||||
|
"select_operation": "Select operation",
|
||||||
|
"select_operations": "Select operations...",
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"select_task": "Select task",
|
||||||
|
"select_tasks": "Select tasks...",
|
||||||
|
"add_task": "Add task",
|
||||||
|
"add_tasks": "Add tasks",
|
||||||
|
"location": "Location",
|
||||||
|
"select_location": "Select location",
|
||||||
|
"select_locations": "Select locations...",
|
||||||
|
"add_location": "Add location",
|
||||||
|
"add_locations": "Add locations",
|
||||||
|
"variants": "Variants",
|
||||||
|
"select_variant": "Select variant",
|
||||||
|
"select_variants": "Select variants..."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"groupLabel": "General",
|
||||||
|
"language": "Language",
|
||||||
|
"persistentSession:": "Save session",
|
||||||
|
"persistentSession_description": "The application will save the session and no expire it."
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"groupLabel": "Sidebar",
|
||||||
|
"edit": "Edit sidebar",
|
||||||
|
"edit_description": "Edit the sidebar to add or remove items.",
|
||||||
|
"edit_button": "Edit",
|
||||||
|
"autoCollapse": "Auto collapse",
|
||||||
|
"autoCollapse_description": "Collapse the sidebar when loose focus"
|
||||||
|
},
|
||||||
|
"aspect": {
|
||||||
|
"groupLabel": "Aspect",
|
||||||
|
"reduceAnimation": "Reduce animation",
|
||||||
|
"reduceAnimation_description": "Reduce the animation of the application.",
|
||||||
|
"darkMode": "Dark mode",
|
||||||
|
"darkMode_description": "Enable dark mode for the application.",
|
||||||
|
"primaryColor": "Primary color",
|
||||||
|
"primaryColor_description": "Change the primary color of the application.",
|
||||||
|
"resetTheme": "Reset theme",
|
||||||
|
"resetTheme_description": "Reset the theme to the default."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
68
packages/app/src/layout/bottombar/index.jsx
Normal file
68
packages/app/src/layout/bottombar/index.jsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class BottomBar extends React.Component {
|
||||||
|
state = {
|
||||||
|
render: null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
window.app.BottomBarController = {
|
||||||
|
render: (fragment) => {
|
||||||
|
this.setState({ render: fragment })
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
this.setState({ render: null })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickItemId = (id) => {
|
||||||
|
window.app.setLocation(`/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.render) {
|
||||||
|
return <div className="bottomBar">
|
||||||
|
{this.state.render}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="bottomBar">
|
||||||
|
<div className="items">
|
||||||
|
<div onClick={() => window.app.openFabric()} key="fabric" id="fabric" className="item">
|
||||||
|
<div className="icon">
|
||||||
|
{createIconRender("PlusCircle")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => window.app.goMain()} key="main" id="main" className="item">
|
||||||
|
<div className="icon">
|
||||||
|
{createIconRender("Home")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => this.onClickItemId("nav")} key="nav" id="nav" className="item">
|
||||||
|
<div className="icon">
|
||||||
|
{createIconRender("Navigation")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => window.app.openSettings()} key="settings" id="settings" className="item">
|
||||||
|
<div className="icon">
|
||||||
|
{createIconRender("Settings")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.props.user ? <div onClick={() => window.app.goToAccount()} key="account" id="account" className="item">
|
||||||
|
<div className="icon">
|
||||||
|
<antd.Avatar src={this.props.user?.avatar} />
|
||||||
|
</div>
|
||||||
|
</div> : <div onClick={() => this.onClickItemId("login")} className="item">
|
||||||
|
<div key="login" id="login" className="icon">
|
||||||
|
{createIconRender("LogIn")}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
53
packages/app/src/layout/bottombar/index.less
Normal file
53
packages/app/src/layout/bottombar/index.less
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
@bottomBar_height: 80px;
|
||||||
|
|
||||||
|
.bottomBar {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
|
||||||
|
height: @bottomBar_height;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: overlay;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
.icon {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
color: var(--background-color-contrast);
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,105 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
import EventEmitter from "@foxify/events"
|
import EventEmitter from "@foxify/events"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class DrawerController extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
addresses: {},
|
||||||
|
refs: {},
|
||||||
|
drawers: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DrawerController = {
|
||||||
|
open: this.open,
|
||||||
|
close: this.close,
|
||||||
|
closeAll: this.closeAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
window.app["DrawerController"] = this.DrawerController
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent = (id, ...context) => {
|
||||||
|
const ref = this.state.refs[id]?.current
|
||||||
|
return ref.events.emit(...context)
|
||||||
|
}
|
||||||
|
|
||||||
|
open = (id, component, options) => {
|
||||||
|
const refs = this.state.refs ?? {}
|
||||||
|
const drawers = this.state.drawers ?? []
|
||||||
|
const addresses = this.state.addresses ?? {}
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
id,
|
||||||
|
key: id,
|
||||||
|
ref: React.createRef(),
|
||||||
|
children: component,
|
||||||
|
options,
|
||||||
|
controller: this,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof addresses[id] === "undefined") {
|
||||||
|
drawers.push(<Drawer {...instance} />)
|
||||||
|
addresses[id] = drawers.length - 1
|
||||||
|
refs[id] = instance.ref
|
||||||
|
} else {
|
||||||
|
const ref = refs[id].current
|
||||||
|
const isLocked = ref.state.locked
|
||||||
|
|
||||||
|
if (!isLocked) {
|
||||||
|
drawers[addresses[id]] = <Drawer {...instance} />
|
||||||
|
refs[id] = instance.ref
|
||||||
|
} else {
|
||||||
|
console.warn("Cannot update an locked drawer.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ refs, addresses, drawers })
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy = (id) => {
|
||||||
|
let { addresses, drawers, refs } = this.state
|
||||||
|
const index = addresses[id]
|
||||||
|
|
||||||
|
if (typeof drawers[index] !== "undefined") {
|
||||||
|
drawers = drawers.filter((value, i) => i !== index)
|
||||||
|
}
|
||||||
|
delete addresses[id]
|
||||||
|
delete refs[id]
|
||||||
|
|
||||||
|
this.setState({ addresses, drawers })
|
||||||
|
}
|
||||||
|
|
||||||
|
close = (id) => {
|
||||||
|
const ref = this.state.refs[id]?.current
|
||||||
|
|
||||||
|
if (typeof ref !== "undefined") {
|
||||||
|
if (ref.state.locked && ref.state.visible) {
|
||||||
|
return console.warn("This drawer is locked and cannot be closed")
|
||||||
|
} else {
|
||||||
|
return ref.close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return console.warn("This drawer not exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll = () => {
|
||||||
|
this.state.drawers.forEach((drawer) => {
|
||||||
|
drawer.ref.current.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.drawers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Drawer extends React.Component {
|
export class Drawer extends React.Component {
|
||||||
options = this.props.options ?? {}
|
options = this.props.options ?? {}
|
||||||
events = new EventEmitter()
|
events = new EventEmitter()
|
||||||
@ -83,113 +179,25 @@ export class Drawer extends React.Component {
|
|||||||
handleFail: this.handleFail,
|
handleFail: this.handleFail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.isMobile) {
|
||||||
|
drawerProps.height = "100%"
|
||||||
|
drawerProps.placement = "bottom"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<antd.Drawer className="drawer" {...drawerProps}>
|
<antd.Drawer className={classnames("drawer", { ["mobile"]: window.isMobile })} {...drawerProps}>
|
||||||
<div className="header">
|
{!this.props.headerDisabled && <div className="pageTitle">
|
||||||
<antd.PageHeader
|
<antd.PageHeader
|
||||||
onBack={this.onClose}
|
onBack={this.onClose}
|
||||||
title={this.props.title ?? "Close"}
|
title={this.props.title ?? "Close"}
|
||||||
backIcon={this.props.backIcon}
|
backIcon={this.props.backIcon ?? <Icons.X />}
|
||||||
subTitle={this.props.subtitle}
|
subTitle={this.props.subtitle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>}
|
||||||
<div className="body">
|
<div className="body">
|
||||||
{React.createElement(this.props.children, componentProps)}
|
{React.createElement(this.props.children, componentProps)}
|
||||||
</div>
|
</div>
|
||||||
</antd.Drawer>
|
</antd.Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DrawerController extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
addresses: {},
|
|
||||||
refs: {},
|
|
||||||
drawers: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
this.DrawerController = {
|
|
||||||
open: this.open,
|
|
||||||
close: this.close,
|
|
||||||
closeAll: this.closeAll,
|
|
||||||
}
|
|
||||||
|
|
||||||
window.app["DrawerController"] = this.DrawerController
|
|
||||||
}
|
|
||||||
|
|
||||||
sendEvent = (id, ...context) => {
|
|
||||||
const ref = this.state.refs[id]?.current
|
|
||||||
return ref.events.emit(...context)
|
|
||||||
}
|
|
||||||
|
|
||||||
open = (id, component, options) => {
|
|
||||||
const refs = this.state.refs ?? {}
|
|
||||||
const drawers = this.state.drawers ?? []
|
|
||||||
const addresses = this.state.addresses ?? {}
|
|
||||||
|
|
||||||
const instance = {
|
|
||||||
id,
|
|
||||||
ref: React.createRef(),
|
|
||||||
children: component,
|
|
||||||
options,
|
|
||||||
controller: this,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof addresses[id] === "undefined") {
|
|
||||||
drawers.push(<Drawer {...instance} />)
|
|
||||||
addresses[id] = drawers.length - 1
|
|
||||||
refs[id] = instance.ref
|
|
||||||
} else {
|
|
||||||
const ref = refs[id].current
|
|
||||||
const isLocked = ref.state.locked
|
|
||||||
|
|
||||||
if (!isLocked) {
|
|
||||||
drawers[addresses[id]] = <Drawer {...instance} />
|
|
||||||
refs[id] = instance.ref
|
|
||||||
} else {
|
|
||||||
console.warn("Cannot update an locked drawer.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ refs, addresses, drawers })
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy = (id) => {
|
|
||||||
let { addresses, drawers, refs } = this.state
|
|
||||||
const index = addresses[id]
|
|
||||||
|
|
||||||
if (typeof drawers[index] !== "undefined") {
|
|
||||||
drawers = drawers.filter((value, i) => i !== index)
|
|
||||||
}
|
|
||||||
delete addresses[id]
|
|
||||||
delete refs[id]
|
|
||||||
|
|
||||||
this.setState({ addresses, drawers })
|
|
||||||
}
|
|
||||||
|
|
||||||
close = (id) => {
|
|
||||||
const ref = this.state.refs[id]?.current
|
|
||||||
|
|
||||||
if (typeof ref !== "undefined") {
|
|
||||||
if (ref.state.locked && ref.state.visible) {
|
|
||||||
return console.warn("This drawer is locked and cannot be closed")
|
|
||||||
} else {
|
|
||||||
return ref.close()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return console.warn("This drawer not exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAll = () => {
|
|
||||||
this.state.drawers.forEach((drawer) => {
|
|
||||||
drawer.ref.current.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.state.drawers
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,52 @@
|
|||||||
.drawer {
|
@import "theme/vars.less";
|
||||||
height: 100vh;
|
|
||||||
max-height: 100vh;
|
|
||||||
|
|
||||||
.header {
|
.drawer {
|
||||||
position: relative;
|
height: 100vh;
|
||||||
top: 0;
|
max-height: 100vh;
|
||||||
z-index: 100;
|
|
||||||
}
|
.pageTitle {
|
||||||
.body {
|
position: sticky;
|
||||||
padding: 10px 30px;
|
background-color: var(--background-color-primary);
|
||||||
height: fit-content;
|
top: 0;
|
||||||
width: 100%;
|
z-index: 100;
|
||||||
}
|
height: @app_header_height;
|
||||||
|
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
.ant-page-header{
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.ant-page-header-heading{
|
||||||
|
height: 100%;
|
||||||
|
margin: 0!important;
|
||||||
|
}
|
||||||
|
.ant-page-header-heading-left {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
padding: 20px 30px;
|
||||||
|
height: 100vh - @app_header_height;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
.body {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-drawer-content, .ant-drawer-wrapper-body, .ant-drawer-body{
|
.ant-drawer-content,
|
||||||
height: 100vh;
|
.ant-drawer-wrapper-body,
|
||||||
max-height: 100vh;
|
.ant-drawer-body {
|
||||||
}
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
@ -16,6 +16,10 @@ export default class Header extends React.Component {
|
|||||||
|
|
||||||
this.HeaderController = {
|
this.HeaderController = {
|
||||||
toogleVisible: (to) => {
|
toogleVisible: (to) => {
|
||||||
|
if (window.isMobile) {
|
||||||
|
to = true
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ visible: to ?? !this.state.visible })
|
this.setState({ visible: to ?? !this.state.visible })
|
||||||
},
|
},
|
||||||
isVisible: () => this.state.visible,
|
isVisible: () => this.state.visible,
|
||||||
@ -28,15 +32,33 @@ export default class Header extends React.Component {
|
|||||||
window.app.openFabric()
|
window.app.openFabric()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickHome = () => {
|
||||||
|
window.app.goMain()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<antd.Layout.Header className={classnames(`app_header`, { ["hidden"]: !this.state.visible })}>
|
<antd.Layout.Header className={classnames(`app_header`, { ["hidden"]: !window.isMobile && !this.state.visible })}>
|
||||||
|
{window.isMobile && <div>
|
||||||
|
<antd.Button
|
||||||
|
onClick={this.onClickHome}
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon={<Icons.Home style={{ margin: 0 }} />}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
<div>
|
<div>
|
||||||
<AppSearcher />
|
<antd.Button
|
||||||
</div>
|
onClick={this.onClickCreate}
|
||||||
<div>
|
type="primary"
|
||||||
<antd.Button onClick={this.onClickCreate} type="primary" shape="circle" icon={<Icons.Plus style={{ margin: 0 }} />} style={{ display: "flex", alignItems: "center", justifyContent: "center" }} />
|
shape="circle"
|
||||||
|
icon={<Icons.Plus style={{ margin: 0 }} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{!window.isMobile &&
|
||||||
|
<div>
|
||||||
|
<AppSearcher />
|
||||||
|
</div>}
|
||||||
</antd.Layout.Header>
|
</antd.Layout.Header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import Sidebar from './sidebar'
|
|
||||||
import Header from './header'
|
|
||||||
import Drawer from './drawer'
|
|
||||||
import Sidedrawer from './sidedrawer'
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
Sidebar,
|
|
||||||
Header,
|
|
||||||
Sidedrawer,
|
|
||||||
}
|
|
104
packages/app/src/layout/index.jsx
Normal file
104
packages/app/src/layout/index.jsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from "react"
|
||||||
|
import classnames from "classnames"
|
||||||
|
import * as antd from 'antd'
|
||||||
|
import { enquireScreen, unenquireScreen } from 'enquire-js'
|
||||||
|
|
||||||
|
import Sidebar from './sidebar'
|
||||||
|
import Header from './header'
|
||||||
|
import Drawer from './drawer'
|
||||||
|
import Sidedrawer from './sidedrawer'
|
||||||
|
import BottomBar from "./bottombar"
|
||||||
|
|
||||||
|
const LayoutRenders = {
|
||||||
|
mobile: (props) => {
|
||||||
|
return <antd.Layout className={classnames("app_layout", ["mobile"])} style={{ height: "100%" }}>
|
||||||
|
<antd.Layout className="content_layout">
|
||||||
|
<antd.Layout.Content className="layout_page">
|
||||||
|
<div className={classnames("fade-transverse-active", { "fade-transverse-leave": props.isOnTransition })}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</antd.Layout.Content>
|
||||||
|
</antd.Layout>
|
||||||
|
<BottomBar user={props.user} />
|
||||||
|
<Drawer />
|
||||||
|
</antd.Layout>
|
||||||
|
},
|
||||||
|
default: (props) => {
|
||||||
|
return <antd.Layout className="app_layout" style={{ height: "100%" }}>
|
||||||
|
<Drawer />
|
||||||
|
<Sidebar user={props.user} />
|
||||||
|
<antd.Layout className="content_layout">
|
||||||
|
<Header />
|
||||||
|
<antd.Layout.Content className="layout_page">
|
||||||
|
<div className={classnames("fade-transverse-active", { "fade-transverse-leave": props.isOnTransition })}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</antd.Layout.Content>
|
||||||
|
</antd.Layout>
|
||||||
|
<Sidedrawer />
|
||||||
|
</antd.Layout>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Layout extends React.Component {
|
||||||
|
state = {
|
||||||
|
layoutType: "default",
|
||||||
|
isMobile: false,
|
||||||
|
isOnTransition: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout = (layout) => {
|
||||||
|
if (typeof LayoutRenders[layout] === "function") {
|
||||||
|
return this.setState({
|
||||||
|
layoutType: layout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return console.error("Layout type not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.enquireHandler = enquireScreen(mobile => {
|
||||||
|
const { isMobile } = this.state
|
||||||
|
|
||||||
|
if (isMobile !== mobile) {
|
||||||
|
window.isMobile = mobile
|
||||||
|
this.setState({
|
||||||
|
isMobile: mobile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobile) {
|
||||||
|
window.app.eventBus.emit("mobile_mode")
|
||||||
|
this.setLayout("mobile")
|
||||||
|
} else {
|
||||||
|
window.app.eventBus.emit("desktop_mode")
|
||||||
|
this.setLayout("default")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.app.eventBus.on("transitionStart", () => {
|
||||||
|
this.setState({ isOnTransition: true })
|
||||||
|
})
|
||||||
|
window.app.eventBus.on("transitionDone", () => {
|
||||||
|
this.setState({ isOnTransition: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unenquireScreen(this.enquireHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const layoutComponentProps = {
|
||||||
|
...this.props,
|
||||||
|
...this.state,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LayoutRenders[this.state.layoutType]) {
|
||||||
|
return LayoutRenders[this.state.layoutType](layoutComponentProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutRenders.default(layoutComponentProps)
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@ import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
|
|||||||
|
|
||||||
import Selector from "../selector"
|
import Selector from "../selector"
|
||||||
|
|
||||||
|
import sidebarItems from "schemas/routes.json"
|
||||||
import defaultSidebarKeys from "schemas/defaultSidebar.json"
|
import defaultSidebarKeys from "schemas/defaultSidebar.json"
|
||||||
import sidebarItems from "schemas/sidebar.json"
|
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { Icons, createIconRender } from "components/Icons"
|
|||||||
import { SelectableList } from "components"
|
import { SelectableList } from "components"
|
||||||
import { List } from "antd"
|
import { List } from "antd"
|
||||||
|
|
||||||
import sidebarItems from "schemas/sidebar.json"
|
import sidebarItems from "schemas/routes.json"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Icons, createIconRender } from "components/icons"
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
import { Layout, Menu, Avatar } from "antd"
|
import { Layout, Menu, Avatar } from "antd"
|
||||||
|
|
||||||
import { SidebarEditor } from "./components"
|
import { SidebarEditor } from "./components"
|
||||||
|
|
||||||
import config from "config"
|
import config from "config"
|
||||||
import sidebarItems from "schemas/sidebar.json"
|
import sidebarItems from "schemas/routes.json"
|
||||||
import defaultSidebarItems from "schemas/defaultSidebar.json"
|
import defaultSidebarItems from "schemas/defaultSidebar.json"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "@/theme/vars.less";
|
@import "theme/vars.less";
|
||||||
|
|
||||||
// SIDEBAR
|
// SIDEBAR
|
||||||
.ant-layout-sider {
|
.ant-layout-sider {
|
||||||
@ -19,8 +19,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu,
|
.ant-menu, .ant-menu ul {
|
||||||
ul {
|
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export default {
|
|
||||||
Profile: "Profile",
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
export default {
|
|
||||||
"Login": "Iniciar sesión",
|
|
||||||
"Back": "Volver",
|
|
||||||
"Profile": "Perfil",
|
|
||||||
"Settings": "Ajustes",
|
|
||||||
"Help & Assitence": "Ayuda y Asistencia",
|
|
||||||
"My Data": "Mis Datos",
|
|
||||||
"Statistics": "Estadistica",
|
|
||||||
"Workload": "Carga de Trabajo",
|
|
||||||
"Language": "Idioma",
|
|
||||||
welcome_index: "Bienvenido! {user}"
|
|
||||||
}
|
|
135
packages/app/src/models/session/index.js
Normal file
135
packages/app/src/models/session/index.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import cookies from 'js-cookie'
|
||||||
|
import jwt_decode from "jwt-decode"
|
||||||
|
import config from 'config'
|
||||||
|
|
||||||
|
export default class Session {
|
||||||
|
static get bridge() {
|
||||||
|
return window.app?.request
|
||||||
|
}
|
||||||
|
|
||||||
|
static tokenKey = config.app?.storage?.token ?? "token"
|
||||||
|
|
||||||
|
static get token() {
|
||||||
|
if (navigator.userAgent === "capacitor") {
|
||||||
|
// FIXME: sorry about that
|
||||||
|
return sessionStorage.getItem(this.tokenKey)
|
||||||
|
}
|
||||||
|
return cookies.get(this.tokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static set token(token) {
|
||||||
|
if (navigator.userAgent === "capacitor") {
|
||||||
|
// FIXME: sorry about that
|
||||||
|
return sessionStorage.setItem(this.tokenKey, token)
|
||||||
|
}
|
||||||
|
return cookies.set(this.tokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get decodedToken() {
|
||||||
|
return this.token && jwt_decode(this.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
//* BASIC HANDLERS
|
||||||
|
login = (payload, callback) => {
|
||||||
|
const body = {
|
||||||
|
username: window.btoa(payload.username),
|
||||||
|
password: window.btoa(payload.password),
|
||||||
|
allowRegenerate: payload.allowRegenerate
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.generateNewToken(body, (err, res) => {
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(err, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!err || res.status === 200) {
|
||||||
|
let token = res.data
|
||||||
|
|
||||||
|
if (typeof token === 'object') {
|
||||||
|
token = token.token
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.token = token
|
||||||
|
window.app.eventBus.emit("new_session")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logout = async () => {
|
||||||
|
await this.destroyCurrentSession()
|
||||||
|
this.forgetLocalSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
//* GENERATORS
|
||||||
|
generateNewToken = async (payload, callback) => {
|
||||||
|
const request = await Session.bridge.post.login(payload, undefined, {
|
||||||
|
parseData: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(request.error, request.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerateToken = async () => {
|
||||||
|
return await Session.bridge.post.regenerate()
|
||||||
|
}
|
||||||
|
|
||||||
|
//* GETTERS
|
||||||
|
getAllSessions = async () => {
|
||||||
|
return await Session.bridge.get.sessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenInfo = async () => {
|
||||||
|
const session = Session.token
|
||||||
|
|
||||||
|
return await Session.bridge.post.validateSession({ session })
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentTokenValid = async () => {
|
||||||
|
const health = await this.getTokenInfo()
|
||||||
|
|
||||||
|
return health.valid
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetLocalSession = () => {
|
||||||
|
if (navigator.userAgent === "capacitor") {
|
||||||
|
// FIXME: sorry about that
|
||||||
|
return sessionStorage.removeItem(Session.tokenKey)
|
||||||
|
}
|
||||||
|
return cookies.remove(Session.tokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyAllSessions = async () => {
|
||||||
|
const session = Session.decodedToken
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Session.bridge.delete.sessions({ user_id: session.user_id })
|
||||||
|
this.forgetLocalSession()
|
||||||
|
window.app.eventBus.emit("destroyed_session")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyCurrentSession = async () => {
|
||||||
|
const token = Session.token
|
||||||
|
const session = Session.decodedToken
|
||||||
|
|
||||||
|
if (!session || !token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Session.bridge.delete.session({ user_id: session.user_id, token: token })
|
||||||
|
this.forgetLocalSession()
|
||||||
|
window.app.eventBus.emit("destroyed_session")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
logout = this.destroyCurrentSession
|
||||||
|
}
|
@ -25,6 +25,16 @@ export default class User {
|
|||||||
return User.bridge.get.roles({ username: token.username })
|
return User.bridge.get.roles({ username: token.username })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAssignedWorkloads = async () => {
|
||||||
|
const token = Session.decodedToken
|
||||||
|
|
||||||
|
if (!token || !User.bridge) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return User.bridge.get.workloads({ username: token.username })
|
||||||
|
}
|
||||||
|
|
||||||
getData = async (payload, callback) => {
|
getData = async (payload, callback) => {
|
||||||
const request = await User.bridge.get.user(undefined, { username: payload.username, _id: payload.user_id }, {
|
const request = await User.bridge.get.user(undefined, { username: payload.username, _id: payload.user_id }, {
|
||||||
parseData: false
|
parseData: false
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import { Icons } from "components/Icons"
|
|
||||||
import { Sessions } from "components"
|
import { Sessions } from "components"
|
||||||
|
|
||||||
export default class SessionsView extends React.Component {
|
export default class SessionsView extends React.Component {
|
||||||
@ -9,8 +8,12 @@ export default class SessionsView extends React.Component {
|
|||||||
title: "Caution",
|
title: "Caution",
|
||||||
content: "This action will cause all sessions to be closed, you will have to log in again.",
|
content: "This action will cause all sessions to be closed, you will have to log in again.",
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
//this.setState({ sessions: null })
|
if (typeof this.props.handleSignOutAll === "function") {
|
||||||
window.app.eventBus.emit("destroyAllSessions")
|
this.props.handleSignOutAll()
|
||||||
|
} else {
|
||||||
|
antd.message.error("Sign out all sessions failed")
|
||||||
|
console.error("handleSignOutAll is not a function")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
okCancel: true,
|
okCancel: true,
|
||||||
})
|
})
|
||||||
|
@ -8,7 +8,6 @@ import { Session } from "models"
|
|||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const api = window.app.apiBridge
|
|
||||||
|
|
||||||
const SelfViewComponents = {
|
const SelfViewComponents = {
|
||||||
sessionsView: SessionsView,
|
sessionsView: SessionsView,
|
||||||
@ -68,6 +67,8 @@ export default class Account extends React.Component {
|
|||||||
sessions: null
|
sessions: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api = window.app.request
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
const token = Session.decodedToken
|
const token = Session.decodedToken
|
||||||
const location = window.app.history.location
|
const location = window.app.history.location
|
||||||
@ -90,13 +91,14 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
handleUpdateUserData = async (changes, callback) => {
|
handleUpdateUserData = async (changes, callback) => {
|
||||||
const update = {}
|
const update = {}
|
||||||
|
|
||||||
if (Array.isArray(changes)) {
|
if (Array.isArray(changes)) {
|
||||||
changes.forEach((change) => {
|
changes.forEach((change) => {
|
||||||
update[change.id] = change.value
|
update[change.id] = change.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.put
|
await this.api.put
|
||||||
.selfUser(update)
|
.selfUser(update)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
callback(false, data)
|
callback(false, data)
|
||||||
@ -105,7 +107,11 @@ export default class Account extends React.Component {
|
|||||||
callback(true, err)
|
callback(true, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.app.eventBus.emit("forceReloadUser")
|
window.app.eventBus.emit("reinitializeUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSignOutAll = () => {
|
||||||
|
return this.props.contexts.app.sessionController.destroyAllSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
openUserEdit = () => {
|
openUserEdit = () => {
|
||||||
@ -158,9 +164,6 @@ export default class Account extends React.Component {
|
|||||||
<span>#{user._id}</span>
|
<span>#{user._id}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
<div key="roles">
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{this.state.isSelf && this.renderSelfActions()}
|
{this.state.isSelf && this.renderSelfActions()}
|
||||||
</div>
|
</div>
|
||||||
@ -173,10 +176,11 @@ export default class Account extends React.Component {
|
|||||||
sessions: this.state.sessions,
|
sessions: this.state.sessions,
|
||||||
user: this.state.user,
|
user: this.state.user,
|
||||||
decodedToken: Session.decodedToken,
|
decodedToken: Session.decodedToken,
|
||||||
|
handleSignOutAll: this.handleSignOutAll,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { PostsFeed } from 'components'
|
|
||||||
|
|
||||||
export default class Explore extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <PostsFeed />
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
.exploreWrapper{
|
|
||||||
|
|
||||||
}
|
|
@ -1,18 +1,22 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { AppSearcher } from "components"
|
import * as antd from "antd"
|
||||||
|
import { AppSearcher, ServerStatus, Clock } from "components"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
|
// TODO: Customizable main menu
|
||||||
export default class Main extends React.Component {
|
export default class Main extends React.Component {
|
||||||
componentWillUnmount() {
|
api = window.app.request
|
||||||
if (!window.app?.HeaderController?.isVisible()) {
|
|
||||||
window.app.HeaderController.toogleVisible(true)
|
componentDidMount() {
|
||||||
|
if (!window.isMobile && window.app?.HeaderController?.isVisible()) {
|
||||||
|
window.app.HeaderController.toogleVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentWillUnmount() {
|
||||||
if (window.app?.HeaderController?.isVisible()) {
|
if (!window.isMobile && !window.app?.HeaderController?.isVisible()) {
|
||||||
window.app.HeaderController.toogleVisible(false)
|
window.app.HeaderController.toogleVisible(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,16 +26,27 @@ export default class Main extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
<div className="top">
|
<div className="top">
|
||||||
<div>
|
<div className="header_title">
|
||||||
<h1>Welcome back, {user.fullName ?? user.username ?? "Guest"}</h1>
|
<div>
|
||||||
|
<antd.Avatar shape="square" src={user.avatar} size={window.isMobile ? undefined : 120} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>Welcome back, {user.fullName ?? user.username ?? "Guest"}</h1>
|
||||||
|
</div>
|
||||||
|
{!window.isMobile && <div>
|
||||||
|
<ServerStatus />
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!window.isMobile && <div>
|
||||||
<AppSearcher />
|
<AppSearcher />
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
|
||||||
<div className="content">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,30 +1,52 @@
|
|||||||
.dashboard {
|
.dashboard {
|
||||||
padding-top: 20px;
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
h1{
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
margin-bottom: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
> div {
|
||||||
> div {
|
margin-bottom: 20px;
|
||||||
margin-left: 20px;
|
}
|
||||||
}
|
|
||||||
}
|
.content {
|
||||||
}
|
> div {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned {
|
||||||
|
> div {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header_title {
|
||||||
|
display: flex;
|
||||||
|
> div {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick_actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
54
packages/app/src/pages/streams/index.jsx
Normal file
54
packages/app/src/pages/streams/index.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import axios from "axios"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { SelectableList } from "components"
|
||||||
|
|
||||||
|
// http://192.168.1.144:8080/live/srgooglo_ff.m3u8
|
||||||
|
const streamsApi = "http://media.ragestudio.net/api"
|
||||||
|
const bridge = axios.create({
|
||||||
|
baseURL: streamsApi,
|
||||||
|
auth: {
|
||||||
|
username: "admin",
|
||||||
|
password: "sharedpass1414"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class Streams extends React.Component {
|
||||||
|
state = {
|
||||||
|
list: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStreamsList = async () => {
|
||||||
|
const streams = ((await bridge.get("/streams")).data).live
|
||||||
|
this.setState({ list: streams })
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
this.updateStreamsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickItem = (item) => {
|
||||||
|
window.app.setLocation(`/streams/viewer?key=${item}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderListItem = (key) => {
|
||||||
|
const streaming = this.state.list[key]
|
||||||
|
console.log(streaming)
|
||||||
|
return <div key={streaming.publisher.clientId} onClick={() => this.onClickItem(key)}>
|
||||||
|
<h1>@{streaming.publisher.stream} #{streaming.publisher.clientId}</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div>
|
||||||
|
<h1>Streams</h1>
|
||||||
|
<div>
|
||||||
|
<SelectableList
|
||||||
|
selectionEnabled={false}
|
||||||
|
renderItem={this.renderListItem}
|
||||||
|
items={Object.keys(this.state.list)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
99
packages/app/src/pages/streams/viewer/index.jsx
Normal file
99
packages/app/src/pages/streams/viewer/index.jsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import * as antd from "antd"
|
||||||
|
import Plyr from 'plyr'
|
||||||
|
import Hls from 'hls.js'
|
||||||
|
import mpegts from 'mpegts.js'
|
||||||
|
|
||||||
|
import "plyr/dist/plyr.css"
|
||||||
|
|
||||||
|
const streamsSource = "http://media.ragestudio.net/live"
|
||||||
|
|
||||||
|
export default class StreamViewer extends React.Component {
|
||||||
|
state = {
|
||||||
|
player: null,
|
||||||
|
streamKey: null,
|
||||||
|
streamSource: null,
|
||||||
|
loadedProtocol: "flv",
|
||||||
|
protocolInstance: null,
|
||||||
|
defaultOptions: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPlayerRef = React.createRef()
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
const requested = query.get("key")
|
||||||
|
|
||||||
|
const source = `${streamsSource}/${requested}`
|
||||||
|
const player = new Plyr('#player')
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
player,
|
||||||
|
streamKey: requested,
|
||||||
|
streamSource: source,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.loadWithProtocol[this.state.loadedProtocol]()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuality = (newQuality) => {
|
||||||
|
if (loadedProtocol === "hls") {
|
||||||
|
this.state.protocolInstance.levels.forEach((level, levelIndex) => {
|
||||||
|
if (level.height === newQuality) {
|
||||||
|
console.log("Found quality match with " + newQuality);
|
||||||
|
this.state.protocolInstance.currentLevel = levelIndex;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error("Unsupported protocol")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchProtocol = (protocol) => {
|
||||||
|
if (typeof this.state.protocolInstance.destroy === "function") {
|
||||||
|
this.state.protocolInstance.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ protocolInstance: null })
|
||||||
|
|
||||||
|
console.log("Switching to " + protocol)
|
||||||
|
this.loadWithProtocol[protocol]()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWithProtocol = {
|
||||||
|
hls: () => {
|
||||||
|
const source = `${this.state.streamSource}.m3u8`
|
||||||
|
const hls = new Hls()
|
||||||
|
|
||||||
|
hls.loadSource(source)
|
||||||
|
hls.attachMedia(this.videoPlayerRef.current)
|
||||||
|
|
||||||
|
this.setState({ protocolInstance: hls, loadedProtocol: "hls" })
|
||||||
|
},
|
||||||
|
flv: () => {
|
||||||
|
const source = `${this.state.streamSource}.flv`
|
||||||
|
|
||||||
|
const instance = mpegts.createPlayer({ type: 'flv', url: source, isLive: true })
|
||||||
|
|
||||||
|
instance.attachMediaElement(this.videoPlayerRef.current)
|
||||||
|
instance.load()
|
||||||
|
instance.play()
|
||||||
|
|
||||||
|
this.setState({ protocolInstance: instance, loadedProtocol: "flv" })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div>
|
||||||
|
<antd.Select
|
||||||
|
onChange={(value) => this.switchProtocol(value)}
|
||||||
|
value={this.state.loadedProtocol}
|
||||||
|
>
|
||||||
|
<antd.Select.Option value="hls">HLS</antd.Select.Option>
|
||||||
|
<antd.Select.Option value="flv">FLV</antd.Select.Option>
|
||||||
|
</antd.Select>
|
||||||
|
<video ref={this.videoPlayerRef} id="player" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
93
packages/app/src/pages/users/index.jsx
Normal file
93
packages/app/src/pages/users/index.jsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { Icons } from "components/icons"
|
||||||
|
import { ActionsBar, SelectableList } from "components"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class Users extends React.Component {
|
||||||
|
state = {
|
||||||
|
data: null,
|
||||||
|
selectionEnabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
api = window.app.request
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
await this.loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData = async () => {
|
||||||
|
this.setState({ data: null })
|
||||||
|
const data = await this.api.get.users()
|
||||||
|
this.setState({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleSelection = (to) => {
|
||||||
|
this.setState({ selectionEnabled: to ?? !this.state.selectionEnabled })
|
||||||
|
}
|
||||||
|
|
||||||
|
openUser(username) {
|
||||||
|
if (this.state.selectionEnabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
window.app.setLocation(`/account`, { username })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRoles(roles) {
|
||||||
|
return roles.map((role) => {
|
||||||
|
return <antd.Tag key={role}> {role} </antd.Tag>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = (item) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item._id}
|
||||||
|
onDoubleClick={() => this.openUser(item.username)}
|
||||||
|
className="user_item"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<antd.Avatar shape="square" src={item.avatar} />
|
||||||
|
</div>
|
||||||
|
<div className="title">
|
||||||
|
<div className="line">
|
||||||
|
<div>
|
||||||
|
<h1>{item.fullName ?? item.username}</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>#{item._id}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>{this.renderRoles(item.roles)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="users_list">
|
||||||
|
<ActionsBar mode="float">
|
||||||
|
<div>
|
||||||
|
<antd.Button shape="round" icon={this.state.selectionEnabled ? <Icons.Check /> : <Icons.MousePointer />} type={this.state.selectionEnabled ? "default" : "primary"} onClick={() => this.toogleSelection()}>
|
||||||
|
{this.state.selectionEnabled ? "Done" : "Select"}
|
||||||
|
</antd.Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<antd.Button type="primary" icon={<Icons.Plus />}>New User</antd.Button>
|
||||||
|
</div>
|
||||||
|
</ActionsBar>
|
||||||
|
{!this.state.data ? <antd.Skeleton active /> :
|
||||||
|
<SelectableList
|
||||||
|
selectionEnabled={this.state.selectionEnabled}
|
||||||
|
items={this.state.data}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
52
packages/app/src/pages/users/index.less
Normal file
52
packages/app/src/pages/users/index.less
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.users_list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectableList_item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_item {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
cursor: text;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-family: "Roboto Mono", monospace !important;
|
||||||
|
cursor: text;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
> div {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,11 @@
|
|||||||
@import "theme/fonts.css";
|
@import "theme/fonts.css";
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
position: absolute;
|
display: none;
|
||||||
|
|
||||||
width: 14px;
|
width: 0;
|
||||||
height: 18px;
|
height: 0;
|
||||||
z-index: 200;
|
z-index: 0;
|
||||||
transition: all 200ms ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@ -111,6 +110,27 @@ body {
|
|||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
width: 14px;
|
||||||
|
height: 18px;
|
||||||
|
z-index: 200;
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none !important;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout_page {
|
.layout_page {
|
||||||
@ -125,25 +145,11 @@ body {
|
|||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media (min-width: @screen-xs) {
|
@media (max-width: 768px) {
|
||||||
|
.layout_page {
|
||||||
// }
|
margin: 10px;
|
||||||
|
}
|
||||||
// @media (min-width: @screen-md) {
|
}
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @media (min-width: @screen-lg) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @media (min-width: @screen-xl) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @media (min-width: @screen-xxl) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
.fade-transverse-active {
|
.fade-transverse-active {
|
||||||
transition: all 250ms;
|
transition: all 250ms;
|
||||||
@ -208,8 +214,25 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-result-extra {
|
.ant-result-extra {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-modal-confirm-btns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.__render_box_test {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
width: 40vw;
|
||||||
|
height: 40vw;
|
||||||
|
|
||||||
|
background: red;
|
||||||
|
transition: all 50ms ease-out;
|
||||||
|
|
||||||
|
filter: drop-shadow(20px 20px 3px rgba(0, 0, 0, 1));
|
||||||
|
}
|
@ -4,7 +4,8 @@
|
|||||||
@app_sidebar_borderRadius: 18px;
|
@app_sidebar_borderRadius: 18px;
|
||||||
|
|
||||||
// SIZES
|
// SIZES
|
||||||
@app_header_height: 55px;
|
@app_header_height: 5vh;
|
||||||
|
@fixedHeader100VH: @app_header_height - 100vh;
|
||||||
|
|
||||||
@app_menuItemSize: 100px;
|
@app_menuItemSize: 100px;
|
||||||
@app_menuItemIconSize: 30px;
|
@app_menuItemIconSize: 30px;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user