}
@@ -151,7 +156,7 @@ export default class SettingsMenu extends React.Component {
}
render() {
- const isDevMode = window.__evite.env.NODE_ENV !== "production"
+ const isDevMode = window.__evite?.env?.NODE_ENV !== "production"
return (
@@ -161,7 +166,7 @@ export default class SettingsMenu extends React.Component {
{config.app?.siteName}
- v{window.__evite.projectVersion}
+ v{window.__evite?.projectVersion}
diff --git a/packages/app/src/components/Settings/index.less b/packages/app/src/components/Settings/index.less
index d9de886b..62d6a402 100644
--- a/packages/app/src/components/Settings/index.less
+++ b/packages/app/src/components/Settings/index.less
@@ -28,7 +28,13 @@
display: flex;
align-items: center;
- h5{
+ h4{
+ margin: 0;
+ }
+
+ p {
+ font-size: 11px;
+ color: var(--background-color-contrast);
margin: 0;
}
diff --git a/packages/app/src/components/UserSelector/index.jsx b/packages/app/src/components/UserSelector/index.jsx
new file mode 100644
index 00000000..0fcdc655
--- /dev/null
+++ b/packages/app/src/components/UserSelector/index.jsx
@@ -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
+
+
+
{item.fullName ?? item.username}
+
+
+ }
+
+ 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
+ }
+
+ return
+ ]}
+ onDone={(keys) => this.props.handleDone(keys)}
+ />
+
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/UserSelector/index.less b/packages/app/src/components/UserSelector/index.less
new file mode 100644
index 00000000..fd95cd48
--- /dev/null
+++ b/packages/app/src/components/UserSelector/index.less
@@ -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;
+ }
+ }
+}
diff --git a/packages/app/src/components/index.js b/packages/app/src/components/index.js
index 478f1bcc..b8c551ca 100644
--- a/packages/app/src/components/index.js
+++ b/packages/app/src/components/index.js
@@ -8,10 +8,11 @@ export { default as Sessions } from "./Sessions"
export { default as ActionsBar } from "./ActionsBar"
export { default as SelectableList } from "./SelectableList"
export { default as ObjectInspector } from "./ObjectInspector"
-export { default as FabricCreator } from "./FabricCreator"
export { default as ServerStatus } from "./ServerStatus"
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 QRReader from "./QRReader"
export * as Window from "./RenderWindow"
\ No newline at end of file
diff --git a/packages/app/src/controllers/index.js b/packages/app/src/controllers/index.js
deleted file mode 100644
index a7b0a4a3..00000000
--- a/packages/app/src/controllers/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as SettingsController } from './settings'
-export { default as SidebarController } from './sidebar'
\ No newline at end of file
diff --git a/packages/app/src/extensions/render/index.jsx b/packages/app/src/extensions/render/index.jsx
index f9ec0571..6fdac022 100644
--- a/packages/app/src/extensions/render/index.jsx
+++ b/packages/app/src/extensions/render/index.jsx
@@ -1,82 +1,101 @@
import React from "react"
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) => {
return window.app.bindContexts(component)
}
export function GetRoutesMap() {
- const jsxFiles = import.meta.glob('/src/pages/**/**.jsx')
- const tsxFiles = import.meta.glob('/src/pages/**/**.tsx')
+ return routes.map((route) => {
+ 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) => {
const component = loadable(async () => {
- const location = window.location
- let path = props.path ?? location.pathname
+ // TODO: Support evite async component initializations
- if (path.startsWith("/")) {
- 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 RouteRender
})
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 = {
key: "customRender",
expose: [
@@ -125,25 +144,30 @@ export const extension = {
async (app, main) => {
const defaultTransitionDelay = 150
+ main.progressBar = progressBar.configure({ parent: "html", showSpinner: false })
+
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
if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) {
return false
}
- main.eventBus.emit("setLocation")
+ main.progressBar.start()
+ main.eventBus.emit("transitionStart", delay)
setTimeout(() => {
main.history.push({
pathname: to,
}, state)
main.history.lastLocation = main.history.location
- }, defaultTransitionDelay)
+ }, delay ?? defaultTransitionDelay)
}
main.setToWindowContext("setLocation", main.history.setLocation)
diff --git a/packages/app/src/extensions/sound/index.js b/packages/app/src/extensions/sound/index.js
index b87908de..3f7ced6a 100644
--- a/packages/app/src/extensions/sound/index.js
+++ b/packages/app/src/extensions/sound/index.js
@@ -1,4 +1,5 @@
import { Howl } from "howler"
+import config from "config"
export class SoundEngine {
constructor() {
@@ -10,10 +11,8 @@ export class SoundEngine {
}
getSounds = async () => {
- const origin = process.env.NODE_ENV === "development" ? `${window.location.origin}/src/assets/sounds/index.js` : `${window.location.origin}/assets/sounds/index.js`
-
- let soundPack = await import(origin)
- soundPack = soundPack.default || soundPack
+ // TODO: Load custom soundpacks manifests
+ let soundPack = config.defaultSoundPack ?? {}
Object.keys(soundPack).forEach((key) => {
const src = soundPack[key]
@@ -29,6 +28,8 @@ export class SoundEngine {
play = (name) => {
if (this.sounds[name]) {
this.sounds[name].play()
+ } else {
+ console.error(`Sound ${name} not found.`)
}
}
}
diff --git a/packages/app/src/i18n/index.js b/packages/app/src/i18n/index.js
new file mode 100644
index 00000000..c5fde276
--- /dev/null
+++ b/packages/app/src/i18n/index.js
@@ -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
\ No newline at end of file
diff --git a/packages/app/src/i18n/locales.js b/packages/app/src/i18n/locales.js
new file mode 100644
index 00000000..6b6a7e6c
--- /dev/null
+++ b/packages/app/src/i18n/locales.js
@@ -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
+}
\ No newline at end of file
diff --git a/packages/app/src/i18n/translations/en.json b/packages/app/src/i18n/translations/en.json
new file mode 100644
index 00000000..882f0b28
--- /dev/null
+++ b/packages/app/src/i18n/translations/en.json
@@ -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."
+ }
+ },
+}
\ No newline at end of file
diff --git a/packages/app/src/layout/bottombar/index.jsx b/packages/app/src/layout/bottombar/index.jsx
new file mode 100644
index 00000000..a1d947b9
--- /dev/null
+++ b/packages/app/src/layout/bottombar/index.jsx
@@ -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
+ {this.state.render}
+
+ }
+
+ return
+
+
window.app.openFabric()} key="fabric" id="fabric" className="item">
+
+ {createIconRender("PlusCircle")}
+
+
+
window.app.goMain()} key="main" id="main" className="item">
+
+ {createIconRender("Home")}
+
+
+
this.onClickItemId("nav")} key="nav" id="nav" className="item">
+
+ {createIconRender("Navigation")}
+
+
+
window.app.openSettings()} key="settings" id="settings" className="item">
+
+ {createIconRender("Settings")}
+
+
+ {this.props.user ?
window.app.goToAccount()} key="account" id="account" className="item">
+
+
:
this.onClickItemId("login")} className="item">
+
+ {createIconRender("LogIn")}
+
+
}
+
+
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/layout/bottombar/index.less b/packages/app/src/layout/bottombar/index.less
new file mode 100644
index 00000000..3663c033
--- /dev/null
+++ b/packages/app/src/layout/bottombar/index.less
@@ -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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/layout/drawer/index.jsx b/packages/app/src/layout/drawer/index.jsx
index bc5685f1..38030e67 100644
--- a/packages/app/src/layout/drawer/index.jsx
+++ b/packages/app/src/layout/drawer/index.jsx
@@ -1,9 +1,105 @@
import React from "react"
import * as antd from "antd"
+import classnames from "classnames"
import EventEmitter from "@foxify/events"
+import { Icons } from "components/Icons"
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(
)
+ 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]] =
+ 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 {
options = this.props.options ?? {}
events = new EventEmitter()
@@ -83,113 +179,25 @@ export class Drawer extends React.Component {
handleFail: this.handleFail,
}
+ if (window.isMobile) {
+ drawerProps.height = "100%"
+ drawerProps.placement = "bottom"
+ }
+
return (
-
-
+
+ {!this.props.headerDisabled &&
}
subTitle={this.props.subtitle}
/>
-
+ }
{React.createElement(this.props.children, componentProps)}
)
}
-}
-
-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(
)
- 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]] =
- 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
- }
-}
+}
\ No newline at end of file
diff --git a/packages/app/src/layout/drawer/index.less b/packages/app/src/layout/drawer/index.less
index 122831cf..0d2295fd 100644
--- a/packages/app/src/layout/drawer/index.less
+++ b/packages/app/src/layout/drawer/index.less
@@ -1,20 +1,52 @@
-.drawer {
- height: 100vh;
- max-height: 100vh;
+@import "theme/vars.less";
- .header {
- position: relative;
- top: 0;
- z-index: 100;
- }
- .body {
- padding: 10px 30px;
- height: fit-content;
- width: 100%;
- }
+.drawer {
+ height: 100vh;
+ max-height: 100vh;
+
+ .pageTitle {
+ position: sticky;
+ background-color: var(--background-color-primary);
+ top: 0;
+ 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{
- height: 100vh;
- max-height: 100vh;
-}
\ No newline at end of file
+.ant-drawer-content,
+.ant-drawer-wrapper-body,
+.ant-drawer-body {
+ height: 100vh;
+ max-height: 100vh;
+}
diff --git a/packages/app/src/layout/header/index.jsx b/packages/app/src/layout/header/index.jsx
index e0f4c628..a30d5b12 100644
--- a/packages/app/src/layout/header/index.jsx
+++ b/packages/app/src/layout/header/index.jsx
@@ -16,6 +16,10 @@ export default class Header extends React.Component {
this.HeaderController = {
toogleVisible: (to) => {
+ if (window.isMobile) {
+ to = true
+ }
+
this.setState({ visible: to ?? !this.state.visible })
},
isVisible: () => this.state.visible,
@@ -28,15 +32,33 @@ export default class Header extends React.Component {
window.app.openFabric()
}
+ onClickHome = () => {
+ window.app.goMain()
+ }
+
render() {
return (
-
+
+ {window.isMobile && }
-
-
} style={{ display: "flex", alignItems: "center", justifyContent: "center" }} />
+
}
+ />
+ {!window.isMobile &&
+ }
)
}
diff --git a/packages/app/src/layout/index.js b/packages/app/src/layout/index.js
deleted file mode 100644
index fa8ed05d..00000000
--- a/packages/app/src/layout/index.js
+++ /dev/null
@@ -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,
-}
\ No newline at end of file
diff --git a/packages/app/src/layout/index.jsx b/packages/app/src/layout/index.jsx
new file mode 100644
index 00000000..68f4b614
--- /dev/null
+++ b/packages/app/src/layout/index.jsx
@@ -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
+
+
+
+ {props.children}
+
+
+
+
+
+
+ },
+ default: (props) => {
+ return
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+ }
+}
+
+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)
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/layout/sidebar/components/editor/index.jsx b/packages/app/src/layout/sidebar/components/editor/index.jsx
index 70e5c49c..96492fef 100644
--- a/packages/app/src/layout/sidebar/components/editor/index.jsx
+++ b/packages/app/src/layout/sidebar/components/editor/index.jsx
@@ -6,8 +6,8 @@ import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
import Selector from "../selector"
+import sidebarItems from "schemas/routes.json"
import defaultSidebarKeys from "schemas/defaultSidebar.json"
-import sidebarItems from "schemas/sidebar.json"
import "./index.less"
diff --git a/packages/app/src/layout/sidebar/components/selector/index.jsx b/packages/app/src/layout/sidebar/components/selector/index.jsx
index 4b608a40..8207c091 100644
--- a/packages/app/src/layout/sidebar/components/selector/index.jsx
+++ b/packages/app/src/layout/sidebar/components/selector/index.jsx
@@ -3,7 +3,7 @@ import { Icons, createIconRender } from "components/Icons"
import { SelectableList } from "components"
import { List } from "antd"
-import sidebarItems from "schemas/sidebar.json"
+import sidebarItems from "schemas/routes.json"
import "./index.less"
diff --git a/packages/app/src/layout/sidebar/index.jsx b/packages/app/src/layout/sidebar/index.jsx
index 740e3f20..6eee53c7 100644
--- a/packages/app/src/layout/sidebar/index.jsx
+++ b/packages/app/src/layout/sidebar/index.jsx
@@ -1,11 +1,11 @@
import React from "react"
-import { Icons, createIconRender } from "components/icons"
+import { Icons, createIconRender } from "components/Icons"
import { Layout, Menu, Avatar } from "antd"
import { SidebarEditor } from "./components"
import config from "config"
-import sidebarItems from "schemas/sidebar.json"
+import sidebarItems from "schemas/routes.json"
import defaultSidebarItems from "schemas/defaultSidebar.json"
import classnames from "classnames"
diff --git a/packages/app/src/layout/sidebar/index.less b/packages/app/src/layout/sidebar/index.less
index aea78a89..9a829b27 100644
--- a/packages/app/src/layout/sidebar/index.less
+++ b/packages/app/src/layout/sidebar/index.less
@@ -1,4 +1,4 @@
-@import "@/theme/vars.less";
+@import "theme/vars.less";
// SIDEBAR
.ant-layout-sider {
@@ -19,8 +19,7 @@
}
}
-.ant-menu,
-ul {
+.ant-menu, .ant-menu ul {
background: transparent !important;
background-color: transparent !important;
diff --git a/packages/app/src/locales/en.js b/packages/app/src/locales/en.js
deleted file mode 100644
index e3894890..00000000
--- a/packages/app/src/locales/en.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- Profile: "Profile",
-}
\ No newline at end of file
diff --git a/packages/app/src/locales/es.js b/packages/app/src/locales/es.js
deleted file mode 100644
index e20bf555..00000000
--- a/packages/app/src/locales/es.js
+++ /dev/null
@@ -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}"
-}
\ No newline at end of file
diff --git a/packages/app/src/models/session/index.js b/packages/app/src/models/session/index.js
new file mode 100644
index 00000000..ddbb74b4
--- /dev/null
+++ b/packages/app/src/models/session/index.js
@@ -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
+}
\ No newline at end of file
diff --git a/packages/app/src/controllers/settings/index.js b/packages/app/src/models/settings/index.js
similarity index 100%
rename from packages/app/src/controllers/settings/index.js
rename to packages/app/src/models/settings/index.js
diff --git a/packages/app/src/controllers/sidebar/index.js b/packages/app/src/models/sidebar/index.js
similarity index 100%
rename from packages/app/src/controllers/sidebar/index.js
rename to packages/app/src/models/sidebar/index.js
diff --git a/packages/app/src/models/user/index.js b/packages/app/src/models/user/index.js
index e923f8b8..d77cb319 100644
--- a/packages/app/src/models/user/index.js
+++ b/packages/app/src/models/user/index.js
@@ -25,6 +25,16 @@ export default class User {
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) => {
const request = await User.bridge.get.user(undefined, { username: payload.username, _id: payload.user_id }, {
parseData: false
diff --git a/packages/app/src/pages/account/components/sessionsView/index.jsx b/packages/app/src/pages/account/components/sessionsView/index.jsx
index 8f3e6b20..841120ec 100644
--- a/packages/app/src/pages/account/components/sessionsView/index.jsx
+++ b/packages/app/src/pages/account/components/sessionsView/index.jsx
@@ -1,6 +1,5 @@
import React from "react"
import * as antd from "antd"
-import { Icons } from "components/Icons"
import { Sessions } from "components"
export default class SessionsView extends React.Component {
@@ -9,8 +8,12 @@ export default class SessionsView extends React.Component {
title: "Caution",
content: "This action will cause all sessions to be closed, you will have to log in again.",
onOk: () => {
- //this.setState({ sessions: null })
- window.app.eventBus.emit("destroyAllSessions")
+ if (typeof this.props.handleSignOutAll === "function") {
+ this.props.handleSignOutAll()
+ } else {
+ antd.message.error("Sign out all sessions failed")
+ console.error("handleSignOutAll is not a function")
+ }
},
okCancel: true,
})
diff --git a/packages/app/src/pages/account/index.jsx b/packages/app/src/pages/account/index.jsx
index 01052b84..71776ba6 100644
--- a/packages/app/src/pages/account/index.jsx
+++ b/packages/app/src/pages/account/index.jsx
@@ -8,7 +8,6 @@ import { Session } from "models"
import "./index.less"
-const api = window.app.apiBridge
const SelfViewComponents = {
sessionsView: SessionsView,
@@ -68,6 +67,8 @@ export default class Account extends React.Component {
sessions: null
}
+ api = window.app.request
+
componentDidMount = async () => {
const token = Session.decodedToken
const location = window.app.history.location
@@ -90,13 +91,14 @@ export default class Account extends React.Component {
handleUpdateUserData = async (changes, callback) => {
const update = {}
+
if (Array.isArray(changes)) {
changes.forEach((change) => {
update[change.id] = change.value
})
}
- await api.put
+ await this.api.put
.selfUser(update)
.then((data) => {
callback(false, data)
@@ -105,7 +107,11 @@ export default class Account extends React.Component {
callback(true, err)
})
- window.app.eventBus.emit("forceReloadUser")
+ window.app.eventBus.emit("reinitializeUser")
+ }
+
+ handleSignOutAll = () => {
+ return this.props.contexts.app.sessionController.destroyAllSessions()
}
openUserEdit = () => {
@@ -158,9 +164,6 @@ export default class Account extends React.Component {
#{user._id}
>
}
-