@operato/shell 2.0.0-alpha.0 → 2.0.0-alpha.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/demo/index.html +14 -3
  3. package/dist/src/actions/busy.js.map +1 -1
  4. package/dist/src/actions/route.d.ts +27 -4
  5. package/dist/src/actions/route.js +35 -6
  6. package/dist/src/actions/route.js.map +1 -1
  7. package/dist/src/app/app-style.js +12 -4
  8. package/dist/src/app/app-style.js.map +1 -1
  9. package/dist/src/app/app.d.ts +10 -5
  10. package/dist/src/app/app.js +27 -15
  11. package/dist/src/app/app.js.map +1 -1
  12. package/dist/src/app/pages/page-404.d.ts +1 -1
  13. package/dist/src/app/pages/page-404.js +5 -5
  14. package/dist/src/app/pages/page-404.js.map +1 -1
  15. package/dist/src/app/pages/page-view.d.ts +58 -0
  16. package/dist/src/app/pages/page-view.js +52 -1
  17. package/dist/src/app/pages/page-view.js.map +1 -1
  18. package/dist/src/custom-alert.d.ts +23 -0
  19. package/dist/src/custom-alert.js +26 -0
  20. package/dist/src/custom-alert.js.map +1 -0
  21. package/dist/src/entries/public/home.d.ts +2 -3
  22. package/dist/src/entries/public/home.js +17 -16
  23. package/dist/src/entries/public/home.js.map +1 -1
  24. package/dist/src/index.d.ts +1 -0
  25. package/dist/src/index.js +1 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/store.js +8 -0
  28. package/dist/src/store.js.map +1 -1
  29. package/dist/stories/app.stories.d.ts +22 -0
  30. package/dist/stories/app.stories.js +38 -0
  31. package/dist/stories/app.stories.js.map +1 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +19 -19
  34. package/src/actions/busy.ts +1 -1
  35. package/src/actions/route.ts +35 -6
  36. package/src/app/app-style.ts +12 -4
  37. package/src/app/app.ts +49 -16
  38. package/src/app/pages/page-404.ts +5 -5
  39. package/src/app/pages/page-view.ts +63 -1
  40. package/src/custom-alert.ts +43 -0
  41. package/src/entries/public/home.ts +17 -16
  42. package/src/index.ts +1 -0
  43. package/src/store.ts +9 -0
  44. package/stories/app.stories.ts +51 -0
  45. package/themes/app-theme.css +145 -0
  46. package/yarn-error.log +0 -17015
@@ -4,12 +4,17 @@ import { HOMEPAGE, UPDATE_PAGE } from '../actions/const'
4
4
  import { store } from '../store'
5
5
 
6
6
  /**
7
+ * Navigate to a page using one of two methods:
8
+ * 1. Using a page link: <a href='page'>goto page</a> (equivalent to route(page))
9
+ * 2. Using the navigate('page') function.
10
+ *
7
11
  * 페이지를 이동하는 방법으로는 다음 두가지가 있다.
8
12
  * 1. page link를 사용하는 방법 <a href='page'>goto page</a>
9
13
  * 이 방법은 route(page)와 동일하다.
10
14
  * 2. navigate('page')를 사용하는 방법
11
15
  *
12
- * @param string page
16
+ * @param location - The path of the page to navigate to.
17
+ * @param replace - Optional. If true, replaces the current page in the browser's history.
13
18
  */
14
19
  export const navigate = (location: string, replace?: boolean) => {
15
20
  if (replace) history.replaceState(history.state, '', location)
@@ -18,6 +23,12 @@ export const navigate = (location: string, replace?: boolean) => {
18
23
  window.dispatchEvent(new Event('popstate'))
19
24
  }
20
25
 
26
+ /**
27
+ * Navigate to a page with optional query parameters, and dispatch a Redux action to load the page.
28
+ *
29
+ * @param pathInfo - Object containing pathname, search, and optional params.
30
+ * @param dispatch - Redux dispatch function.
31
+ */
21
32
  export const navigateWithSilence =
22
33
  ({ pathname: path, search, params }: { pathname: string; search?: string; params?: { [key: string]: any } }) =>
23
34
  (dispatch: any) => {
@@ -42,7 +53,13 @@ export const navigateWithSilence =
42
53
  dispatch(loadPage(page, id, params))
43
54
  }
44
55
 
45
- const _preLoadPage = (page: any) => {
56
+ /**
57
+ * Preload a page by performing any necessary preprocessing before loading.
58
+ *
59
+ * @param page - The page to preload.
60
+ * @returns - The new page path or routing result after preprocessing.
61
+ */
62
+ const _preLoadPage = async (page: any) => {
46
63
  /*
47
64
  * _preLoadPage 에서는 page를 load하기 전처리를 수행한다.
48
65
  * 예를 들면, page dynamic import 또는 page re-routing
@@ -53,8 +70,8 @@ const _preLoadPage = (page: any) => {
53
70
  var modules = state.app.modules
54
71
  if (modules) {
55
72
  for (let i = modules.length - 1; i >= 0; i--) {
56
- let factoryModule = modules[i]
57
- let _page = factoryModule.route && factoryModule.route(page)
73
+ let module = modules[i]
74
+ let _page = module.route && (await module.route(page, module))
58
75
  if (_page) {
59
76
  return _page
60
77
  }
@@ -62,8 +79,15 @@ const _preLoadPage = (page: any) => {
62
79
  }
63
80
  }
64
81
 
65
- export const loadPage = (page: string, id: string, params: { [key: string]: any }) => (dispatch: any) => {
66
- var newPage = _preLoadPage(page)
82
+ /**
83
+ * Load a page by dispatching a Redux action, and handle page navigation if necessary.
84
+ *
85
+ * @param page - The page to load.
86
+ * @param id - The associated resource ID.
87
+ * @param params - Additional parameters to pass to the page.
88
+ */
89
+ export const loadPage = (page: string, id: string, params: { [key: string]: any }) => async (dispatch: any) => {
90
+ var newPage = await _preLoadPage(page)
67
91
 
68
92
  if (page !== newPage && newPage.indexOf('/') == 0) {
69
93
  dispatch(
@@ -83,6 +107,11 @@ export const loadPage = (page: string, id: string, params: { [key: string]: any
83
107
  })
84
108
  }
85
109
 
110
+ /**
111
+ * Route to a given URL by creating a link element and triggering a click event.
112
+ *
113
+ * @param url - The URL to route to.
114
+ */
86
115
  export const route = (url: string) => {
87
116
  const link = document.createElement('a')
88
117
 
@@ -63,16 +63,24 @@ export const AppStyle = css`
63
63
  z-index: 1000;
64
64
  }
65
65
 
66
- /* Wide layout */
67
- @media (min-width: 460px) {
68
- }
69
-
70
66
  @media print {
71
67
  :host {
72
68
  width: 100%;
73
69
  height: 100%;
74
70
  min-height: 100vh;
75
71
  min-height: 100dvh;
72
+
73
+ max-width: unset;
74
+ width: unset;
75
+ height: unset;
76
+ height: unset;
77
+ }
78
+
79
+ main {
80
+ /* important for printing!!! */
81
+ display: block;
82
+ flex: unset;
83
+ overflow: visible;
76
84
  }
77
85
 
78
86
  ox-page-header-bar {
package/src/app/app.ts CHANGED
@@ -23,7 +23,21 @@ export class ThingsApp extends connect(store)(LitElement) {
23
23
 
24
24
  static moduleInitialized: MODULES_STATE = MODULES_STATE.NOT_INITIALIZED
25
25
  static modules: Array<any> = []
26
- static pages: { [path: string]: string } = {}
26
+
27
+ /*
28
+ 모든 모듈의 routes 리스트가 수집될 때까지, routeToPage(..) 를 hold 시키기 위해서 ThingsApp.pages를 Promise로 정의한다.
29
+ */
30
+ static pagesResolver: (
31
+ value:
32
+ | {
33
+ [path: string]: string
34
+ }
35
+ | PromiseLike<{
36
+ [path: string]: string
37
+ }>
38
+ ) => void
39
+ static pages: Promise<{ [path: string]: string }>
40
+
27
41
  static callbacks: Array<any> = []
28
42
  static contextPath?: string
29
43
 
@@ -86,13 +100,13 @@ export class ThingsApp extends connect(store)(LitElement) {
86
100
  ).then(module => {
87
101
  var modules: {
88
102
  name: string
89
- bootstrap: any
103
+ bootstrap: (m?: any /* self */) => void
90
104
  }[] = module.modules
91
105
 
92
106
  /* lifecycle - bootstrapping */
93
107
  modules.forEach(async (m, idx) => {
94
108
  try {
95
- m.bootstrap && (await m.bootstrap())
109
+ m.bootstrap && (await m.bootstrap(m))
96
110
  } catch (e) {
97
111
  console.error(`[${idx} BOOTSTRAP ERROR -${m.name}]`, e)
98
112
  }
@@ -114,7 +128,13 @@ export class ThingsApp extends connect(store)(LitElement) {
114
128
  var { contextPath } = getPathInfo(pathname)
115
129
 
116
130
  /* 페이지를 나가기 전에 옮기지 않도록 개입할 기회를 준다 */
117
- if (lastPathName && lastPathName != pathname && this.activePage && !(await this.activePage.canDeactivate())) {
131
+ if (
132
+ lastPathName &&
133
+ lastPathName != pathname &&
134
+ this.activePage &&
135
+ this.activePage.canDeactivate &&
136
+ !(await this.activePage.canDeactivate())
137
+ ) {
118
138
  history.back()
119
139
  return
120
140
  }
@@ -146,7 +166,7 @@ export class ThingsApp extends connect(store)(LitElement) {
146
166
  super.disconnectedCallback()
147
167
  }
148
168
 
149
- routeToPage() {
169
+ async routeToPage() {
150
170
  const activePages = this.renderRoot.querySelectorAll('main > .page[active]')
151
171
  activePages.forEach(page => {
152
172
  page.removeAttribute('active')
@@ -156,7 +176,7 @@ export class ThingsApp extends connect(store)(LitElement) {
156
176
 
157
177
  if (!this.activePage) {
158
178
  /* 해당 route에 연결된 page가 없는 경우에 main 섹션에 해당 element를 추가해준다. */
159
- const tagname = ThingsApp.pages[this.page!]
179
+ const tagname = (await ThingsApp.pages)[this.page!]
160
180
  if (tagname) {
161
181
  const el = document.createElement(tagname) as PageView
162
182
  el.setAttribute('class', 'page')
@@ -219,19 +239,32 @@ export class ThingsApp extends connect(store)(LitElement) {
219
239
  ThingsApp.callbacks = state.route.callbacks
220
240
  }
221
241
 
222
- static registerPages() {
242
+ static async registerPages() {
243
+ ThingsApp.pages = new Promise<{ [path: string]: string }>(resolve => (ThingsApp.pagesResolver = resolve))
244
+
223
245
  var reversedModules = [...ThingsApp.modules].reverse()
224
- ThingsApp.pages = {}
246
+ const pages: { [path: string]: string } = {}
225
247
 
226
248
  /* 모듈 참조 순서 역순으로 page를 추가한다. (for overidable) */
227
- reversedModules.forEach(m => {
228
- m.routes &&
229
- m.routes.forEach((route: any) => {
230
- if (!ThingsApp.pages[route.page]) {
231
- ThingsApp.pages[route.page] = route.tagname
232
- }
233
- })
234
- })
249
+ for (const m of reversedModules) {
250
+ if (!m.routes) {
251
+ continue
252
+ }
253
+
254
+ /*
255
+ 각 모듈의 routes가 모두 완성될 때까지 ThingsApp.pages 구성을 지연한다.
256
+ 각 모듈의 routes를 동적으로도 구성할 수 있도록 하기 위해서이다.
257
+ */
258
+ const routes = await m.routes
259
+
260
+ routes.forEach((route: any) => {
261
+ if (!pages[route.page]) {
262
+ pages[route.page] = route.tagname
263
+ }
264
+ })
265
+ }
266
+
267
+ ThingsApp.pagesResolver(pages)
235
268
  }
236
269
 
237
270
  setBase() {
@@ -17,8 +17,8 @@ export class Page404 extends PageView {
17
17
  text-align: center;
18
18
  color: var(--secondary-color);
19
19
  }
20
- mwc-icon {
21
- --mdc-icon-size: 120px;
20
+ md-icon {
21
+ --md-icon-size: 120px;
22
22
  color: var(--status-danger-color);
23
23
  text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1);
24
24
  }
@@ -28,9 +28,9 @@ export class Page404 extends PageView {
28
28
  text-transform: capitalize;
29
29
  }
30
30
  @media only screen and (max-width: 460px) {
31
- mwc-icon {
31
+ md-icon {
32
32
  padding-top: 25%;
33
- --mdc-icon-size: 90px;
33
+ --md-icon-size: 90px;
34
34
  }
35
35
  h2 {
36
36
  font-size: 2em;
@@ -47,7 +47,7 @@ export class Page404 extends PageView {
47
47
  render() {
48
48
  return html`
49
49
  <section>
50
- <mwc-icon>error_outline</mwc-icon>
50
+ <md-icon>error_outline</md-icon>
51
51
  <h2>page not found!</h2>
52
52
  The page you requested cannot be found.
53
53
  </section>
@@ -22,12 +22,26 @@ function diff(after: any, before: any): any {
22
22
  return changed && changes
23
23
  }
24
24
 
25
+ /**
26
+ * PageView is a base class for creating page elements with lifecycle management.
27
+ * Subclasses can extend PageView to define custom behavior and handle page lifecycle events.
28
+ */
25
29
  export class PageView extends LitElement {
30
+ /**
31
+ * Determines whether the page can be deactivated. Subclasses can override this method
32
+ * to implement custom deactivation logic.
33
+ * @returns A Promise that resolves to true if the page can be deactivated, or false otherwise.
34
+ */
26
35
  async canDeactivate(): Promise<boolean> {
27
36
  return Promise.resolve(true)
28
37
  }
29
38
 
30
- // Only render this page if it's actually visible.
39
+ /**
40
+ * Determines whether the page should update. This method is called whenever there are
41
+ * changes to the page's properties.
42
+ * @param changes - A map of changed property values.
43
+ * @returns True if the page should update, or false otherwise.
44
+ */
31
45
  shouldUpdate(changes: PropertyValues<this>) {
32
46
  var active = String(this.active) == 'true'
33
47
  var { active: oldActive = false } = this._oldLifecycleInfo$ || {}
@@ -50,13 +64,31 @@ export class PageView extends LitElement {
50
64
  return active
51
65
  }
52
66
 
67
+ /**
68
+ * Indicates whether the page is currently active.
69
+ */
53
70
  @property({ type: Boolean }) active: boolean = false
71
+
72
+ /**
73
+ * Stores information about the page's lifecycle.
74
+ */
54
75
  @property({ type: Object }) lifecycle: any
76
+
77
+ /**
78
+ * The context path for the page.
79
+ */
55
80
  @property({ type: String, attribute: 'context-path' }) contextPath?: string
56
81
 
57
82
  _oldLifecycleInfo$: any
58
83
 
59
84
  /* lifecycle */
85
+
86
+ /**
87
+ * Handles page updates and lifecycle events. Subclasses can override this method
88
+ * to implement custom logic for initializing, updating, and disposing of the page.
89
+ * @param changes - A map of changed properties.
90
+ * @param force - If true, forces an update of the page.
91
+ */
60
92
  async pageUpdate(changes: any = {}, force: boolean = false) {
61
93
  var before = this._oldLifecycleInfo$ || {}
62
94
 
@@ -113,6 +145,9 @@ export class PageView extends LitElement {
113
145
  }
114
146
  }
115
147
 
148
+ /**
149
+ * Resets the page. Subclasses can override this method to perform custom reset logic.
150
+ */
116
151
  async pageReset() {
117
152
  var { initialized } = this._oldLifecycleInfo$ || {}
118
153
 
@@ -122,14 +157,35 @@ export class PageView extends LitElement {
122
157
  }
123
158
  }
124
159
 
160
+ /**
161
+ * Disposes of the page. Subclasses can override this method to perform custom disposal logic.
162
+ */
125
163
  async pageDispose() {
126
164
  await this.pageUpdate({
127
165
  initialized: false
128
166
  })
129
167
  }
130
168
 
169
+ /**
170
+ * Initializes the page. Subclasses can override this method to perform custom initialization logic.
171
+ * @param pageInfo - Information about the page's state.
172
+ */
131
173
  pageInitialized(pageInfo: any) {}
174
+
175
+ /**
176
+ * Handles page updates and changes in properties.
177
+ * Subclasses can override this method to implement custom update logic.
178
+ * @param changes - A map of changed properties.
179
+ * @param after - The current state of the page.
180
+ * @param before - The previous state of the page.
181
+ */
132
182
  pageUpdated(changes: any, after: any, before: any) {}
183
+
184
+ /**
185
+ * Handles the disposal of the page. Subclasses can override this method
186
+ * to implement custom disposal logic.
187
+ * @param pageInfo - Information about the page's state.
188
+ */
133
189
  pageDisposed(pageInfo: any) {}
134
190
 
135
191
  /* context */
@@ -140,6 +196,12 @@ export class PageView extends LitElement {
140
196
  })
141
197
  }
142
198
 
199
+ /**
200
+ * Updates the context of the page. Subclasses can override the `context` getter
201
+ * to provide specific context information for the page. The context will be updated
202
+ * using the `updateContext` method inherited from PageView.
203
+ * @param override - An optional object with context properties to override.
204
+ */
143
205
  get context() {
144
206
  return {}
145
207
  }
@@ -0,0 +1,43 @@
1
+ import { OxPrompt } from '@operato/popup'
2
+
3
+ const TYPES_ICON: { [type: string]: string } = {
4
+ success: 'verified',
5
+ error: 'error',
6
+ warning: 'warning',
7
+ info: 'info',
8
+ question: 'question_mark'
9
+ }
10
+
11
+ export async function CustomAlert({
12
+ type,
13
+ icon,
14
+ title,
15
+ text,
16
+ confirmButton,
17
+ cancelButton,
18
+ callback
19
+ }: {
20
+ type?: 'info' | 'success' | 'error' | 'warning' | 'question'
21
+ icon?: string
22
+ title?: string
23
+ text?: string
24
+ confirmButton?: { color?: string; text: string }
25
+ cancelButton?: { color?: string; text: string }
26
+ callback?: (val: { isConfirmed: boolean; isDismissed: boolean; value: boolean }) => any
27
+ }) {
28
+ const result = await OxPrompt.open({
29
+ type: type || 'info',
30
+ icon: (icon && TYPES_ICON[icon]) || icon,
31
+ title,
32
+ text,
33
+ confirmButton,
34
+ cancelButton
35
+ })
36
+
37
+ const val = { isConfirmed: result, isDismissed: !result, value: result }
38
+ if (callback && typeof callback === 'function') {
39
+ callback(val)
40
+ } else {
41
+ return val
42
+ }
43
+ }
@@ -1,5 +1,4 @@
1
- import '@material/mwc-icon-button'
2
- import '@material/mwc-button'
1
+ import '@material/web/icon/icon.js'
3
2
 
4
3
  import { css, html, LitElement } from 'lit'
5
4
  import { customElement } from 'lit/decorators.js'
@@ -13,7 +12,7 @@ export class HomePage extends LitElement {
13
12
  display: block;
14
13
  position: relative;
15
14
 
16
- --mdc-theme-primary: white;
15
+ --md-theme-primary: white;
17
16
  }
18
17
 
19
18
  [home] {
@@ -30,17 +29,19 @@ export class HomePage extends LitElement {
30
29
  color: #fff;
31
30
  text-align: center;
32
31
  font-size: 20px;
33
- }
34
- [message] strong {
35
- display: block;
36
- font-size: 2.5rem;
37
- }
38
- [message] img {
39
- width: 450px;
40
- max-width: 90%;
41
- display: block;
42
- margin: auto;
43
- margin-top: 20px;
32
+
33
+ strong {
34
+ display: block;
35
+ font-size: 2.5rem;
36
+ }
37
+
38
+ img {
39
+ width: 450px;
40
+ max-width: 90%;
41
+ display: block;
42
+ margin: auto;
43
+ margin-top: 20px;
44
+ }
44
45
  }
45
46
 
46
47
  @media screen and (max-width: 460px) {
@@ -64,10 +65,10 @@ export class HomePage extends LitElement {
64
65
  }
65
66
 
66
67
  render() {
67
- var { icon, title, description } = this.applicationMeta
68
+ var { title, description } = this.applicationMeta
68
69
 
69
70
  return html`
70
- <mwc-icon-button home icon="home" @click=${() => (window.location.href = '/')}></mwc-icon-button>
71
+ <md-icon home @click=${() => (window.location.href = '/')}>home</md-icon>
71
72
 
72
73
  <div message>
73
74
  <strong>${title}</strong>
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from './store'
3
3
  export * from './actions'
4
4
  export * from './app/pages/page-view'
5
5
  export * from './object-store'
6
+ export * from './custom-alert'
package/src/store.ts CHANGED
@@ -14,6 +14,15 @@ declare global {
14
14
  // See https://github.com/zalmoxisus/redux-devtools-extension for more information.
15
15
  const devCompose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
16
16
 
17
+ // Only for providing test environment - combineReducers 에서 process.env.NODE_ENV를 접근하기 때문에.
18
+ if (typeof window.process == 'undefined') {
19
+ window.process = {
20
+ env: {
21
+ NODE_ENV: JSON.stringify('development')
22
+ }
23
+ } as any
24
+ }
25
+
17
26
  export const store: Store<unknown, Action<any>> & LazyStore = createStore(
18
27
  state => state,
19
28
  devCompose(lazyReducerEnhancer(combineReducers), applyMiddleware(thunk))
@@ -0,0 +1,51 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import { css, html, render, TemplateResult } from 'lit'
4
+ import '../src/app/app.js'
5
+
6
+ export default {
7
+ title: 'things-app',
8
+ component: 'things-app',
9
+ argTypes: {
10
+ label: { control: 'string' }
11
+ }
12
+ }
13
+
14
+ interface Story<T> {
15
+ (args: T): TemplateResult
16
+ args?: Partial<T>
17
+ argTypes?: Record<string, unknown>
18
+ }
19
+
20
+ interface ArgTypes {
21
+ label?: string
22
+ }
23
+
24
+ const Template: Story<ArgTypes> = ({ label = '' }: ArgTypes) => html`
25
+ <link href="/themes/app-theme.css" rel="stylesheet" />
26
+ <link
27
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,100..700,0..1"
28
+ rel="stylesheet"
29
+ />
30
+ <link
31
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL@20..48,100..700,0..1"
32
+ rel="stylesheet"
33
+ />
34
+ <link
35
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL@20..48,100..700,0..1"
36
+ rel="stylesheet"
37
+ />
38
+
39
+ <style>
40
+ body {
41
+ background-color: white;
42
+ }
43
+ </style>
44
+
45
+ <things-app></things-app>
46
+ `
47
+
48
+ export const Regular = Template.bind({})
49
+ Regular.args = {
50
+ label: 'common header styles'
51
+ }