@operato/menu 10.7.0 → 10.8.0
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.
- package/CHANGELOG.md +47 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/menu-active.d.ts +89 -0
- package/dist/src/menu-active.js +115 -0
- package/dist/src/menu-active.js.map +1 -0
- package/dist/src/menu-mode.d.ts +98 -0
- package/dist/src/menu-mode.js +100 -0
- package/dist/src/menu-mode.js.map +1 -0
- package/dist/src/menu-portrait-styles.d.ts +27 -0
- package/dist/src/menu-portrait-styles.js +516 -92
- package/dist/src/menu-portrait-styles.js.map +1 -1
- package/dist/src/menu-privilege.d.ts +34 -0
- package/dist/src/menu-privilege.js +22 -0
- package/dist/src/menu-privilege.js.map +1 -0
- package/dist/src/ox-menu-part.d.ts +100 -3
- package/dist/src/ox-menu-part.js +220 -82
- package/dist/src/ox-menu-part.js.map +1 -1
- package/dist/src/ox-menu-portrait.d.ts +158 -3
- package/dist/src/ox-menu-portrait.js +511 -13
- package/dist/src/ox-menu-portrait.js.map +1 -1
- package/dist/src/types.d.ts +38 -1
- package/dist/src/types.js.map +1 -1
- package/dist/test/menu-active.test.d.ts +1 -0
- package/dist/test/menu-active.test.js +131 -0
- package/dist/test/menu-active.test.js.map +1 -0
- package/dist/test/menu-mode.test.d.ts +1 -0
- package/dist/test/menu-mode.test.js +108 -0
- package/dist/test/menu-mode.test.js.map +1 -0
- package/dist/test/menu-privilege.test.d.ts +1 -0
- package/dist/test/menu-privilege.test.js +94 -0
- package/dist/test/menu-privilege.test.js.map +1 -0
- package/dist/test/ox-menu-portrait.test.d.ts +1 -0
- package/dist/test/ox-menu-portrait.test.js +339 -0
- package/dist/test/ox-menu-portrait.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/menu-active.test.ts +162 -0
- package/test/menu-mode.test.ts +135 -0
- package/test/menu-privilege.test.ts +138 -0
- package/test/ox-menu-portrait.test.ts +431 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ox-menu-portrait.js","sourceRoot":"","sources":["../../src/ox-menu-portrait.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { html, LitElement, nothing, TemplateResult } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { navigate } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { Menu } from './types.js'\nimport { MenuPortraitStyles } from './menu-portrait-styles.js'\n\n@customElement('ox-menu-portrait')\nexport class OxMenuPortrait extends LitElement {\n static styles = [ScrollbarStyles, MenuPortraitStyles]\n\n @property({ type: Array }) menus?: Menu[]\n @property({ type: Object }) activeTopLevel?: Menu\n @property({ type: Object }) activeMenu?: Menu\n @property({ type: String }) path!: string\n\n renderMenus(menus: Menu[], activeTopLevel?: Menu, activeMenu?: Menu): TemplateResult | typeof nothing {\n if (menus.length == 0) {\n return nothing\n }\n\n return html`\n <ul>\n ${menus.map(menu => {\n var { type, name, active, path, label = name, badge, icon, menus = [] } = menu\n active = active && typeof active === 'function' ? active.call(menu, { path: this.path }) : false\n badge = typeof badge === 'function' ? badge.call(menu) : (badge ?? false)\n\n return type == 'group'\n ? html`<li group-label>${label}</li>`\n : html`\n <li ?active=${activeTopLevel ? menu === activeTopLevel : active} .menu=${menu} menu>\n <a href=${path || '#'}>\n <md-icon filled>${icon || 'fiber_manual_record'}</md-icon>\n <span>${label}</span>${badge !== false ? html`<div badge>${badge}</div>` : html``}\n ${menus.length > 0 ? html` <md-icon filled submenu-button></md-icon> ` : html``}\n </a>\n ${menus && this.renderMenus(menus || [], activeMenu)}\n </li>\n `\n })}\n </ul>\n `\n }\n\n render() {\n const { menus, activeTopLevel, activeMenu } = this\n return this.renderMenus(menus || [], activeTopLevel, activeMenu)\n }\n\n firstUpdated() {\n this.renderRoot.addEventListener('click', (e: Event) => {\n const menuElement = (e.target as Element)!.closest('[menu]')\n\n //@ts-ignore\n if (menuElement?.menu) {\n //@ts-ignore\n let menu = menuElement.menu\n\n if (!menu.path) {\n /* protect to act move to href. */\n e.stopPropagation()\n e.preventDefault()\n }\n\n this.dispatchEvent(\n new CustomEvent('active-toplevel', {\n bubbles: true,\n detail: this.activeTopLevel === menu ? undefined : menu\n })\n )\n }\n\n /* to respond even if current acting menu is selected */\n let href = (e.target as HTMLAnchorElement)!.href\n href && location.href === href && navigate(href + '#force', true)\n })\n\n /* to hide scrollbar during transition */\n this.renderRoot.addEventListener('transitionstart', e => {\n ;(e.target as Element).removeAttribute('settled')\n })\n this.renderRoot.addEventListener('transitionend', e => {\n ;(e.target as Element).setAttribute('settled', '')\n })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ox-menu-portrait.js","sourceRoot":"","sources":["../../src/ox-menu-portrait.ts"],"names":[],"mappings":";;AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,KAAK,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAExD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAIjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEpF;;;;;GAKG;AACH,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,gBAAgB,EAAU,CAAA;AAG1C,IAAM,cAAc,sBAApB,MAAM,cAAe,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA1D;;QAQL;;;;;;WAMG;QACyC,SAAI,GAAG,KAAK,CAAA;QAExD;;;;;;WAMG;QACyC,cAAS,GAAG,KAAK,CAAA;QAE7D;;;;;WAKG;QACyC,WAAM,GAAG,KAAK,CAAA;QAe1D;;;;;WAKG;QACH;;;WAGG;QACM,aAAQ,GAAU,EAAE,CAAA;QAI7B,wDAAwD;QAChD,aAAQ,GAAG,CAAC,CAAA;QACZ,YAAO,GAAG,CAAC,CAAA;QACnB,oCAAoC;QAC5B,eAAU,GAAG,CAAC,CAAA;QAKtB;;;;;;;;;;;;;WAaG;QACK,gBAAW,GAAG,CAAC,CAAa,EAAE,EAAE;;YACtC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,OAAM;YACR,CAAC;YAED,MAAM,GAAG,GAAG,MAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,aAAa,mDAAG,UAAU,CAAC,CAAA;YAExD,IAAI,GAAG,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1C,OAAM;YACR,CAAC;YAED,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACzB,CAAC,CAAA;IAggBH,CAAC;IA9jBC;;;;;OAKG;IACH,IAAY,IAAI;QACd,OAAO,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IACtF,CAAC;IAwDD,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QAEpD;;;WAGG;QACH,MAAM,IAAI,GAAG,GAAG,EAAE,eAAC,OAAA,CAAC,CAAA,MAAA,MAAC,KAAK,CAAC,QAAQ,EAAU,0CAAE,IAAI,0CAAE,QAAQ,KAAI,EAAE,CAAU,CAAA,EAAA,CAAA;QAE7E,IAAI,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAA;QACtB,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;YAC5C,MAAM,IAAI,GAAG,IAAI,EAAE,CAAA;YACnB,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACzC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;YACtB,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,oBAAoB;;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,QAAQ,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QACvD,MAAA,IAAI,CAAC,iBAAiB,oDAAI,CAAA;IAC5B,CAAC;IAED;;;;;;;;;;;OAWG;IACK,SAAS,CAAC,KAAe,EAAE,WAAoB;QACrD,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;QAE1D,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,WAAW,IAAI,SAAS,CAAA;QACjC,CAAC;QAED,OAAO,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,IAAI,CAAA;IAC9E,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,KAAa,EAAE,cAAqB,EAAE,UAAiB,EAAE,KAAK,GAAG,CAAC;QAC5E,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACtB,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,OAAO,IAAI,CAAA;;UAEL,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;;YACjB,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;YACpF,MAAM,GAAG,MAAM,IAAI,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;YAChG;;;eAGG;YACH,MAAM,KAAK,GAAY,OAAO,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAA,IAAI,CAAC,KAAK,mCAAI,KAAK,CAAC,CAAA;YAEvG,OAAO,IAAI,IAAI,OAAO;gBACpB,CAAC,CAAC,IAAI,CAAA,mBAAmB,KAAK,OAAO;gBACrC,CAAC,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;4BAiBU,cAAc,CAAC,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,MAAM;6BAChD,CAAC,CAAC,UAAU,IAAI,IAAI,KAAK,UAAU;4BACpC,IAAI,CAAC,MAAM,KAAK,IAAI;yBACvB,KAAK,KAAK,CAAC;0BACV,IAAI;;;4BAGF,IAAI,IAAI,GAAG,UAAU,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;sCACxD,IAAI,IAAI,qBAAqB;4BACvC,KAAK,UAAU,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;sBAC/E,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,6CAA6C,CAAC,CAAC,CAAC,IAAI,CAAA,EAAE;;oBAE/E,KAAK,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,GAAG,CAAC,CAAC;;eAE9E,CAAA;QACP,CAAC,CAAC;;KAEL,CAAA;IACH,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,GAAG,IAAI,CAAA;QAElD;;;;;;;;WAQG;QACH,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACf,OAAO,IAAI,CAAA,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,EAAE,cAAc,EAAE,UAAU,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE;QAC5F,IAAI,CAAC,YAAY,EAAE,EAAE,CAAA;QACzB,CAAC;QAED,OAAO,IAAI,CAAA;;;;;;gBAMC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,oBAAoB,CAAC;iBACpE,CAAC,CAAa,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;;mBAExC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW;gBACxC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;;QAEvC,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE,EAAE,cAAc,EAAE,UAAU,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE,IAAI,IAAI,CAAC,YAAY,EAAE;KAC1G,CAAA;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACK,YAAY;QAClB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC1B,OAAO,OAAO,CAAA;QAChB,CAAC;QAED;;;;;;;;;WASG;QACH,mDAAmD;QACnD,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,KAAK,MAAM,CAAA;QAE5G,OAAO,IAAI,CAAA;;yBAEU,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;2BAEtB,IAAI,YAAY,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO,IAAI,IAAI,SAAS,IAAI;8BACvE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAa,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;;oBAElF,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;;;;;;;;;;;;cAa9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CACjB,OAAO,CAAC,EAAE,CAAC,IAAI,CAAA;qCACQ,CAAC,CAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC;oBAC1D,OAAO,CAAC,IAAI,SAAS,OAAO,CAAC,IAAI;;oBAEjC,CACP;;;;KAIR,CAAA;IACH,CAAC;IAED,uCAAuC;IAC/B,UAAU,CAAC,CAAQ,EAAE,OAAY;;QACvC,CAAC,CAAC,cAAc,EAAE,CAAA;QAClB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,MAAA,OAAO,CAAC,MAAM,uDAAI,CAAA;IACpB,CAAC;IAED;;;;;;;;OAQG;IACK,UAAU,CAAC,CAAa;QAC9B,CAAC,CAAC,cAAc,EAAE,CAAA;QAClB,uCAAuC;QACvC,CAAC,CAAC,eAAe,EAAE,CAAA;QAEnB,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAI,CAAC,CAAC,aAA6B,CAAC,qBAAqB,EAAE,CAAA;YAErE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC,KAAK,GAAG,CAAC,CAAA;YACtD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAA;YAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAA;YACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;YACzD,OAAM;QACR,CAAC;QAED,qCAAqC;QACrC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,iBAAiB,EAAE;YACjC,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,IAAI,CAAC,cAAc,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;SAC5D,CAAC,CACH,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACK,YAAY;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAA;QAExB,IAAI,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;YACnD,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,KAAK,MAAM,CAAA;QAE9B,OAAO,IAAI,CAAA;;;;;;;;;yCAS0B,IAAI,CAAC,QAAQ,WAAW,IAAI,CAAC,OAAO;6BAChD,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI;;;;;;;;;;UAU9E,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;;KAE/G,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACK,gBAAgB;QACtB,OAAO,IAAI,CAAA;;UAEL,IAAI,CAAC,QAAQ,CAAC,GAAG,CACjB,OAAO,CAAC,EAAE,CAAC,IAAI,CAAA;iCACQ,CAAC,CAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,SAAS,OAAO,CAAC,IAAI;gBAC/F,CACP;;KAEJ,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,QAAQ;;QACd,MAAM,GAAG,GAAG,MAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,aAAa,mDAAG,UAAU,CAAuB,CAAA;QAE9E,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,gBAAc,CAAC,UAAU,CAAA;QACxC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAA;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,GAAG,MAAM,GAAG,CAAC,CAAA;QAE5C,2CAA2C;QAC3C,MAAM,GAAG,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;QAEvH,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,OAAO,GAAG,GAAG,CAAA;QACpB,CAAC;QAED,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACK,OAAO,CAAC,GAAgB,EAAE,MAAc;;QAC9C,MAAM,IAAI,GAAG,MAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,aAAa,mDAAG,WAAW,CAAuB,CAAA;QAChF,0CAA0C;QAC1C,MAAM,IAAI,GAAG,MAAA,MAAA,IAAI,CAAC,UAAU,0CAAE,aAAa,mDAAG,UAAU,CAAuB,CAAA;QAE/E,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAM;QACR,CAAC;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAA;QAClD,uCAAuC;QACvC,MAAM,KAAK,GAAG,EAAE,CAAA;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC,CAAA;QAC1E,mCAAmC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,CAAA;QAEvC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,8CAA8C;QAC9C,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,IAAI,CAAA;QAC7C,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,IAAI,CAAA;IAChD,CAAC;IAES,OAAO;QACf,IAAI,CAAC,QAAQ,EAAE,CAAA;IACjB,CAAC;IAED,gEAAgE;IACxD,eAAe,CAAC,CAAa;QACnC,CAAC,CAAC,eAAe,EAAE,CAAA;QACnB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC3F,CAAC;IAED,YAAY;QACV,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAQ,EAAE,EAAE;;YACrD,MAAM,WAAW,GAAI,CAAC,CAAC,MAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAE5D;;;;;eAKG;YACH,IAAK,CAAC,CAAC,MAAmB,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;YACzB,CAAC;YAED;;;;;;eAMG;YACH,YAAY;YACZ,IAAI,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,IAAI,KAAI,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzD,YAAY;gBACZ,IAAI,IAAI,GAAG,WAAW,CAAC,IAAI,CAAA;gBAE3B;;;;;;mBAMG;gBACH;;;;;;mBAMG;gBACH,IAAI,CAAA,MAAA,IAAI,CAAC,KAAK,0CAAE,MAAM,KAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;oBAC7F,CAAC,CAAC,eAAe,EAAE,CAAA;oBACnB,CAAC,CAAC,cAAc,EAAE,CAAA;oBAElB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,iBAAiB,EAAE;wBACjC,OAAO,EAAE,IAAI;wBACb,MAAM,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;qBACxD,CAAC,CACH,CAAA;oBAED,OAAM;gBACR,CAAC;gBAED,IAAI,CAAA,MAAA,IAAI,CAAC,KAAK,0CAAE,MAAM,KAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;oBAChE,CAAC,CAAC,eAAe,EAAE,CAAA;oBACnB,CAAC,CAAC,cAAc,EAAE,CAAA;oBAElB,MAAM,IAAI,GAAI,WAA2B,CAAC,qBAAqB,EAAE,CAAA;oBAEjE;;;;;;;;uBAQG;oBACH,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC,KAAK,GAAG,CAAC,CAAA;oBACtD;;;;uBAIG;oBACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAA;oBAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAA;oBACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;oBAErD,OAAM;gBACR,CAAC;gBAED;;;;;;;;mBAQG;gBACH,MAAM,OAAO,GAAG,CAAC,CAAC,CAAA,MAAA,MAAC,CAAC,CAAC,MAAkB,0CAAE,OAAO,mDAAG,kBAAkB,CAAC,CAAA,CAAA;gBAEtE,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;oBAC1B,kCAAkC;oBAClC,CAAC,CAAC,eAAe,EAAE,CAAA;oBACnB,CAAC,CAAC,cAAc,EAAE,CAAA;gBACpB,CAAC;gBAED,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,iBAAiB,EAAE;oBACjC,OAAO,EAAE,IAAI;oBACb,MAAM,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;iBACxD,CAAC,CACH,CAAA;YACH,CAAC;YAED,wDAAwD;YACxD,IAAI,IAAI,GAAI,CAAC,CAAC,MAA6B,CAAC,IAAI,CAAA;YAChD,IAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAA;QACnE,CAAC,CAAC,CAAA;QAEF,yCAAyC;QACzC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,CAAC,CAAC,EAAE;YACtD,CAAC;YAAC,CAAC,CAAC,MAAkB,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE;YACpD,CAAC;YAAC,CAAC,CAAC,MAAkB,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;QACpD,CAAC,CAAC,CAAA;IACJ,CAAC;;AA9lBM,qBAAM,GAAG,CAAC,eAAe,EAAE,kBAAkB,CAAC,AAAxC,CAAwC;AAkErD,0CAA0C;AAClB,yBAAU,GAAG,CAAC,AAAJ,CAAI;AAjEX;IAA1B,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;6CAAe;AACb;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;sDAAsB;AACrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;kDAAkB;AACjB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAAc;AASG;IAA3C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;4CAAa;AASZ;IAA3C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;iDAAkB;AAQjB;IAA3C,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;8CAAe;AAazC;IAAhB,KAAK,EAAE;8CAAsB;AAYrB;IAAR,KAAK,EAAE;gDAAqB;AAzDlB,cAAc;IAD1B,aAAa,CAAC,kBAAkB,CAAC;GACrB,cAAc,CAgmB1B","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { html, LitElement, nothing, TemplateResult } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, store } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { Menu } from './types.js'\n\nimport { MenuPortraitStyles } from './menu-portrait-styles.js'\nimport { childrenOpenAs, menuMode, showsLabels, topNavigates } from './menu-mode.js'\n\n/**\n * 시스템 항목을 담는 가짜 최상위.\n *\n * 팝업이 「어느 최상위에서 열렸나」를 하나의 값(`popped`)으로 들고 있어서, 시스템도 최상위인\n * 것처럼 다룬다. 그래야 노브와 자리 계산이 한 벌로 남는다.\n */\nconst SYSTEM = { name: 'ox-menu-system' } as Menu\n\n@customElement('ox-menu-portrait')\nexport class OxMenuPortrait extends localize(i18next)(LitElement) {\n static styles = [ScrollbarStyles, MenuPortraitStyles]\n\n @property({ type: Array }) menus?: Menu[]\n @property({ type: Object }) activeTopLevel?: Menu\n @property({ type: Object }) activeMenu?: Menu\n @property({ type: String }) path!: string\n\n /**\n * 레일 모드 — 접을 수 있고, 접히면 하위가 팝업으로 뜬다.\n *\n * **옵트인이다.** 켜지 않은 앱은 지금과 한 글자도 다르지 않다. 모양이 함께 바뀌는 것은 취향이\n * 아니라 요건이다 — 기존 모양은 줄마다 폭 전체를 쓰는 밑줄이 있고 왼쪽 여백이 16px 이라 56px\n * 에서 무너진다(§`menu-rail-styles.ts`).\n */\n @property({ type: Boolean, reflect: true }) rail = false\n\n /**\n * 접힘 — 아이콘만.\n *\n * **주인은 이 컴포넌트가 아니다.** 폭을 정하는 요소는 `ox-menu-part` 이고(그쪽에 `min-width`\n * 가 있다), 셸의 리사이즈 손잡이도 그 요소에 인라인 폭을 쓴다. 그래서 상태와 저장은 그쪽이\n * 갖고 여기는 받아서 그린다.\n */\n @property({ type: Boolean, reflect: true }) collapsed = false\n\n /**\n * 서랍인가 — 화면 위에 겹쳐 떴다가 닫히는 메뉴.\n *\n * 그때 최상위는 **펼치기만** 한다. 누르는 순간 서랍이 닫히면 그 묶음의 나머지 하위를 볼\n * 기회가 없다(§`ox-menu-part` 의 `drawer`).\n */\n @property({ type: Boolean, reflect: true }) drawer = false\n\n /**\n * 지금 어떤 모양으로 서 있나 — 세 플래그에서 하나로 모은다.\n *\n * 이 값과 그에 딸린 두 답(`하위를 어디에` · `최상위가 이동하나`)은 `menu-mode.ts` 에 있고\n * 시험으로 못박혀 있다. 여기서 그때그때 판단하지 않는다 — 그렇게 하다가 네 번 깨졌다.\n */\n private get mode() {\n return menuMode({ rail: this.rail, collapsed: this.collapsed, drawer: this.drawer })\n }\n\n /** 접힌 상태에서 팝업으로 열린 최상위 메뉴. 시스템 팝업이면 SYSTEM 이다. */\n @state() private popped?: Menu\n\n /**\n * 시스템 항목 — 프로필 · 로그아웃 · 설정처럼 **앱이 아니라 프레임워크가 붙이는 것**들.\n *\n * 하드코딩하지 않는다. 각 패키지가 부팅할 때 `ADD_MORENDA` 로 등록하고(권한이 없으면 등록\n * 자체를 안 한다), 우리는 그 목록을 그대로 그린다. 그래서 목록이 비면 이 자리도 없다.\n */\n /*\n * private 을 두지 않는다 — 시험이 이 목록을 넣어 보고 「항목이 없으면 자리도 없다」를 확인한다.\n * 앱에서는 store 가 채운다(아래 connectedCallback).\n */\n @state() morendas: any[] = []\n\n private _unsubscribeStore?: () => void\n\n /** 팝업 자리 — 누른 항목의 오른쪽. 세로는 그린 뒤 재서 맞춘다(§`fitPopup`). */\n private _popLeft = 0\n private _popTop = 0\n /** 누른 항목의 윗선. 화면에 들어가면 이 자리를 쓴다. */\n private _popPrefer = 0\n\n /** 팝업이 화면 안에 들어가게 세로 자리를 맞춘다. 팝업 밖 여백. */\n private static readonly POP_MARGIN = 8\n\n /**\n * 팝업 밖을 누르면 닫는다.\n *\n * ── 팝업 안의 클릭을 막지 않는다 ────────────────────────────────────────\n * 한때 팝업 컨테이너에 stopPropagation 을 걸어 두었다. 팝업 안을 눌러도 이 처리기가 받아\n * 곧바로 닫히던 것을 막으려던 것인데, 그것이 **셸의 라우터까지 막았다.**\n *\n * 라우터는 document.body 에서 클릭을 받아 composedPath 로 A 를 찾고 preventDefault 한 뒤\n * 화면만 바꾼다(@operato/shell 의 installRouter). 클릭이 거기까지 못 가면 브라우저의 기본\n * 이동이 그대로 일어나 **페이지 전체가 다시 읽혔다.**\n *\n * 그래서 막지 않고, 여기서 「팝업 안이면 닫지 않는다」로 가른다. 줄을 누를 때 닫는 일은 그\n * 줄의 처리기가 한다.\n */\n private _onDocClick = (e: MouseEvent) => {\n if (!this.popped) {\n return\n }\n\n const pop = this.renderRoot?.querySelector?.('.railpop')\n\n if (pop && e.composedPath().includes(pop)) {\n return\n }\n\n this.popped = undefined\n }\n\n connectedCallback() {\n super.connectedCallback()\n document.addEventListener('click', this._onDocClick)\n\n /*\n * morenda 는 각 패키지의 부팅에서 **비동기로** 등록된다. 한 번 읽고 끝내면 늦게 붙는 것이\n * 빠진다. 길이가 바뀔 때만 다시 그린다 — 이 목록은 늘어나기만 한다.\n */\n const read = () => ((store.getState() as any)?.more?.morendas || []) as any[]\n\n this.morendas = read()\n this._unsubscribeStore = store.subscribe(() => {\n const next = read()\n if (next.length !== this.morendas.length) {\n this.morendas = next\n }\n })\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n document.removeEventListener('click', this._onDocClick)\n this._unsubscribeStore?.()\n }\n\n /**\n * 마우스를 올렸을 때 뜨는 글.\n *\n * ── 접혔을 때는 이름이 먼저다 ───────────────────────────────────────────\n * 접히면 아이콘만 남으므로 **이름을 모른다.** 그때는 이름을 윗줄에 얹고 설명을 아랫줄에 둔다.\n * 펼쳐져 있으면 이름은 이미 옆에 있으니 설명만 낸다.\n *\n * 줄바꿈으로 잇는다 — 대시로 이으면 설명 안의 대시와 겹쳐 「A — B — C」가 된다.\n *\n * 설명도 없고 이름도 안 보이는 경우가 아니면 아무것도 내지 않는다. 이름을 그대로 되풀이하는\n * 툴팁은 소음이다.\n */\n private hoverText(label?: unknown, description?: string) {\n const name = typeof label === 'string' ? label : undefined\n\n if (showsLabels(this.mode)) {\n return description || undefined\n }\n\n return description && name ? `${name}\\n${description}` : description || name\n }\n\n /**\n * @param depth 0 이면 최상위다. **하위도 `active` 를 달기 때문에** 깊이를 값으로 넘긴다 —\n * CSS 로 「최상위인 li」를 가리려면 `:not(li li)` 같은 것이 필요하고, 그건 읽는 사람이\n * 한 번 더 생각해야 하는 선택자다.\n */\n renderMenus(menus: Menu[], activeTopLevel?: Menu, activeMenu?: Menu, depth = 0): TemplateResult | typeof nothing {\n if (menus.length == 0) {\n return nothing\n }\n\n return html`\n <ul>\n ${menus.map(menu => {\n var { type, name, active, path, label = name, icon, menus = [], description } = menu\n active = active && typeof active === 'function' ? active.call(menu, { path: this.path }) : false\n /*\n * 건수는 값이거나 값을 내는 함수다. 함수의 반환은 화면에 그릴 수 있는 무엇이든 온다 —\n * 숫자이거나, 갱신될 때마다 바뀌는 Lit 디렉티브다. 그래서 따로 담는다.\n */\n const badge: unknown = typeof menu.badge === 'function' ? menu.badge.call(menu) : (menu.badge ?? false)\n\n return type == 'group'\n ? html`<li group-label>${label}</li>`\n : html`\n <!--\n popped — **지금 팝업을 연 항목**이다. active(지금 페이지)와 다른 뜻이라\n 다른 표시를 준다. 접힌 레일에서 강조된 아이콘은 「지금 페이지」만 뜻하므로,\n 이 표시가 없으면 어느 항목의 팝업인지 알 수 없다.\n -->\n <!--\n active 와 current 는 뜻이 다르다.\n\n 최상위의 active 하위가 펼쳐져 있다 (여닫힘)\n 하위의 active 이것이 지금 페이지다\n current 이것이 지금 페이지다 — 어느 층이든\n\n active 하나로 두면 「펼쳐져 있다」와 「지금 페이지다」에 같은 강조가 걸려,\n 최상위와 그 안의 한 줄이 나란히 강조된다.\n -->\n <li\n ?active=${activeTopLevel ? menu === activeTopLevel : active}\n ?current=${!!activeMenu && menu === activeMenu}\n ?popped=${this.popped === menu}\n ?top=${depth === 0}\n .menu=${menu}\n menu\n >\n <a href=${path || '#'} title=${ifDefined(this.hoverText(label, description))}>\n <md-icon filled>${icon || 'fiber_manual_record'}</md-icon>\n <span>${label}</span>${badge !== false ? html`<div badge>${badge}</div>` : html``}\n ${menus.length > 0 ? html` <md-icon filled submenu-button></md-icon> ` : html``}\n </a>\n ${menus && this.renderMenus(menus || [], activeMenu, activeMenu, depth + 1)}\n </li>\n `\n })}\n </ul>\n `\n }\n\n render() {\n const { menus, activeTopLevel, activeMenu } = this\n\n /*\n * 레일이 아니어도 시스템 자리는 그린다.\n *\n * 한때 이것을 레일 가지 안에만 두었다. 그래서 **메뉴 감추기를 켜면 「관리」 한 줄이\n * 사라졌다** — 그 설정이 레일을 끄기 때문이다.\n *\n * 레일을 감추기와 배타로 둔 것은 **접기** 때문이다. 이미 숨은 것을 접을 수는 없다. 그 판단은\n * 접기에만 맞고, 시스템 자리는 접기와 아무 상관이 없다. 관계없는 것을 한 플래그에 묶었다.\n */\n if (!this.rail) {\n return html`${this.renderMenus(menus || [], activeTopLevel, activeMenu)} ${this.renderSystem()}\n ${this.renderPopped()}`\n }\n\n return html`\n <!--\n 접기 단추 — 맨 위에 둔다. 접힌 뒤 다시 펼치는 길이 같은 자리에 있어야 한다.\n -->\n <button\n class=\"railtoggle\"\n title=${i18next.t(this.collapsed ? 'text.expand-menu' : 'text.collapse-menu')}\n @click=${(e: MouseEvent) => this.toggleCollapsed(e)}\n >\n <md-icon>${this.collapsed ? 'menu' : 'menu_open'}</md-icon>\n <span>${i18next.t('text.collapse-menu')}</span>\n </button>\n ${this.renderMenus(menus || [], activeTopLevel, activeMenu)} ${this.renderSystem()} ${this.renderPopped()}\n `\n }\n\n /**\n * 시스템 항목 — **바닥에 붙는다.**\n *\n * 프로필 · 로그아웃 · 설정은 하루에 한 번 여는 것이고, 일하는 화면들과 같은 깊이에 있으면\n * 자주 여는 것을 밀어낸다. 그래서 메뉴 끝에 두고 사이를 빈 자리로 벌린다.\n *\n * 목록이 비면 이 자리도 없다 — 각 패키지가 권한이 없으면 등록 자체를 안 하므로, 비어 있는\n * 것은 「볼 것이 없다」는 뜻이다. 빈 묶음 이름만 남겨 두면 눌러도 아무 일이 없다.\n *\n * 펼친 상태에서도 팝업으로 연다. 이 항목들은 **주소가 아니라 함수**라, 그 자리에서 펼치면\n * 옆의 하위 메뉴들과 같은 모양이 되어 「눌러도 화면이 안 바뀌는 메뉴」로 읽힌다.\n */\n private renderSystem(): TemplateResult | typeof nothing {\n if (!this.morendas.length) {\n return nothing\n }\n\n /*\n * **보통 메뉴 줄과 같은 모양으로 그린다.**\n *\n * 전에는 button.railitem 으로 따로 그렸고, 그 스타일이 레일에만 있어서 다른 모양에서는\n * 꾸밈이 없었다. ul > li > a 로 두면 레일과 기존 모양이 각자 가진 규칙이 그대로 붙는다 —\n * 여백 · 색 · hover · 접힘 · 화살표 · 팝업 표시까지.\n *\n * `li` 에 `menu` 를 달아도 `.menu` 속성은 주지 않는다. `renderMenus` 의 클릭 처리기가\n * `menuElement?.menu` 로 가르므로, 그것이 없으면 이 줄을 건드리지 않고 아래 `@click` 이 받는다.\n */\n /* 팝업으로 열리는 모양에서는 팝업 상태로, 그 밖에서는 다른 최상위와 같은 상태로. */\n const open = childrenOpenAs(this.mode) === 'popup' ? this.popped === SYSTEM : this.activeTopLevel === SYSTEM\n\n return html`\n <div class=\"railspacer\"></div>\n <div group-label>${i18next.t('text.system')}</div>\n <ul>\n <li menu ?active=${open} ?popped=${childrenOpenAs(this.mode) === 'popup' && open} ?top=${true}>\n <a href=\"#\" title=${i18next.t('text.manage')} @click=${(e: MouseEvent) => this.openSystem(e)}>\n <md-icon filled>admin_panel_settings</md-icon>\n <span>${i18next.t('text.manage')}</span>\n <md-icon filled submenu-button></md-icon>\n </a>\n <!--\n 하위를 **그 자리에** 둔다 — 다른 최상위와 같은 규칙이다.\n\n 펼친 상태에서 팝업으로 열면 이 줄만 다르게 굴고, 특히 메뉴를 감추는 모드(모바일에서\n 쓴다)에서는 화면 어딘가에 고정된 팝업이 뜰 자리가 없다.\n\n 접힌 레일에서는 이 목록이 감춰지고(56px 안에 들어가지 않는다) 팝업이 대신 뜬다 —\n 그것도 다른 최상위와 같다.\n -->\n <ul>\n ${this.morendas.map(\n morenda => html`<li>\n <a href=\"#\" @click=${(e: Event) => this.runMorenda(e, morenda)}>\n ${morenda.icon}<span>${morenda.name}</span>\n </a>\n </li>`\n )}\n </ul>\n </li>\n </ul>\n `\n }\n\n /** 시스템 항목은 주소가 아니라 함수다. 이동을 막고 부른다. */\n private runMorenda(e: Event, morenda: any) {\n e.preventDefault()\n this.popped = undefined\n morenda.action?.()\n }\n\n /**\n * 시스템 줄을 누른다 — **다른 최상위와 똑같은 두 갈래다.**\n *\n * 한때 시스템만 자기 상태(`popped`)를 따로 썼다. 다른 최상위는 `activeTopLevel` 하나를 나눠\n * 쓰는데 시스템만 그 밖에 있었으니, **다른 묶음이 열려 있어도 시스템이 함께 열렸다.** 한 번에\n * 하나만 열리는 규칙에서 이 줄만 빠져 있었다.\n *\n * 이제 같은 상태를 쓴다. 접힌 레일에서만 팝업이고, 그때는 다른 최상위도 팝업이다.\n */\n private openSystem(e: MouseEvent) {\n e.preventDefault()\n /* 이 클릭이 document 로 올라가면 팝업이 바로 닫힌다. */\n e.stopPropagation()\n\n if (childrenOpenAs(this.mode) === 'popup') {\n const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()\n\n this._popLeft = this.getBoundingClientRect().right + 8\n this._popPrefer = rect.top\n this._popTop = rect.top\n this.popped = this.popped === SYSTEM ? undefined : SYSTEM\n return\n }\n\n /* 그 자리에서 열린다 — 하나만 열리는 규칙을 함께 쓴다. */\n this.popped = undefined\n this.dispatchEvent(\n new CustomEvent('active-toplevel', {\n bubbles: true,\n detail: this.activeTopLevel === SYSTEM ? undefined : SYSTEM\n })\n )\n }\n\n /**\n * 접힌 상태에서 하위 메뉴를 팝업으로 낸다.\n *\n * 그 자리에서 열지 않는 이유는 56px 안에 들여쓴 목록이 들어가지 않기 때문이다. 접힌 레일에서\n * 하위를 열 길이 없으면 **접는 순간 화면의 절반에 갈 수 없게 된다** — 접기가 기능이 아니라\n * 함정이 된다.\n */\n private renderPopped(): TemplateResult | typeof nothing {\n const menu = this.popped\n\n if (!menu || childrenOpenAs(this.mode) !== 'popup') {\n return nothing\n }\n\n const system = menu === SYSTEM\n\n return html`\n <!--\n 노브 — 팝업이 어느 항목에서 열렸는지 가리킨다.\n\n **팝업 안에 두면 안 보인다.** 팝업은 목록이 길 때 스크롤해야 해서 overflow-y: auto 인데,\n 그러면 브라우저가 overflow-x 도 auto 로 계산한다. 노브는 팝업의 왼쪽 선 밖으로 나가야\n 하므로 그 스크롤 박스에 잘린다. 그래서 형제로 두고 화면 좌표로 세운다.\n -->\n <div class=\"railknob\" hidden></div>\n <div class=\"railpop\" style=\"left:${this._popLeft}px; top:${this._popTop}px\">\n <div class=\"phead\">${system ? i18next.t('text.manage') : menu.label || menu.name}</div>\n <!--\n 하위를 **레일 안쪽과 똑같이 그린다.**\n\n 한때 팝업 줄을 따로 만들었다(PopRow). 그래서 옮겨 적을 것을 세 번 빠뜨렸다 — 건수 ·\n 지금 페이지 · 그리고 처음에 이름과 아이콘. 빠뜨려도 오류가 나지 않아 화면을 보고서야\n 알았다.\n\n 같은 함수로 그리면 옮겨 적을 것이 없다. 앞으로 하위에 무엇을 더해도 팝업이 따라온다.\n -->\n ${system ? this.renderSystemRows() : this.renderMenus(menu.menus || [], this.activeMenu, this.activeMenu, 1)}\n </div>\n `\n }\n\n /**\n * 시스템 항목 줄들 — **이것만 따로 그린다.**\n *\n * morenda 는 주소가 아니라 함수를 갖는다(`action`). 아이콘과 이름도 Lit 템플릿이라 `Menu` 모양이\n * 아니다. 억지로 `Menu` 로 바꿔 넘기면 그 변환이 또 옮겨 적는 자리가 된다.\n */\n private renderSystemRows(): TemplateResult {\n return html`\n <ul>\n ${this.morendas.map(\n morenda => html`<li>\n <a href=\"#\" @click=${(e: Event) => this.runMorenda(e, morenda)}>${morenda.icon}<span>${morenda.name}</span></a>\n </li>`\n )}\n </ul>\n `\n }\n\n /**\n * 팝업이 화면 안에 들어가게 세로 자리를 맞춘다.\n *\n * ── 왜 그린 뒤에 하나 ───────────────────────────────────────────────────\n * 높이를 알아야 맞출 수 있고, 높이는 그려 봐야 안다. 항목마다 하위 수가 다르다 — 「기준정보」는\n * 열여섯 줄이고 「배우기」는 한 줄이다.\n *\n * ── 규칙 ────────────────────────────────────────────────────────────────\n * 누른 항목의 윗선에서 시작하는 것이 바람이다. 그대로 두면 아래쪽 항목에서 팝업이 화면 밖으로\n * 나간다. 그래서 **아래로 넘치는 만큼 위로 올린다.** 그래도 안 들어가면 위 여백에 붙이고\n * 그때만 스크롤한다.\n *\n * 자리를 반응 상태로 두지 않고 style 에 바로 쓴다 — 값이 바뀔 때마다 다시 그리면 그 렌더에서\n * 또 재게 되고, 재는 값이 그리기에 달려 있으므로 서로 물린다.\n */\n private fitPopup() {\n const pop = this.renderRoot?.querySelector?.('.railpop') as HTMLElement | null\n\n if (!pop) {\n return\n }\n\n const margin = OxMenuPortrait.POP_MARGIN\n const height = pop.offsetHeight\n const room = window.innerHeight - margin * 2\n\n /* 화면보다 크면 위에 붙이고 스크롤한다. 그 아래 다시 재지 않는다. */\n const top = height >= room ? margin : Math.max(margin, Math.min(this._popPrefer, window.innerHeight - margin - height))\n\n if (Math.round(top) !== Math.round(this._popTop)) {\n this._popTop = top\n }\n\n pop.style.top = `${top}px`\n this.fitKnob(pop, top)\n }\n\n /**\n * 노브를 그 항목의 가운데 높이에 둔다.\n *\n * ── 범위를 벗어나면 감추지 않고 끝에 붙인다 ──────────────────────────────\n * 팝업이 위로 밀리는 것은 **아래에 자리가 없어서**이고, 그만큼만 밀린다. 그래서 그 항목은\n * 거의 항상 팝업의 세로 범위 안으로 들어온다. 벗어나는 것은 라운드 모서리에 걸치는 몇 px\n * 뿐이다. 그때 감추면 **자리가 좁을 때만 표시가 사라진다** — 가장 필요한 순간이다.\n *\n * 그래서 모서리 안쪽으로 붙인다. 정확히 그 줄을 가리키지 못해도 「위쪽」·「아래쪽」은\n * 가리키고, 어느 항목인지는 그 항목의 popped 표시가 말한다.\n */\n private fitKnob(pop: HTMLElement, popTop: number) {\n const knob = this.renderRoot?.querySelector?.('.railknob') as HTMLElement | null\n /* 최상위 항목이거나 시스템 단추다 — 둘 다 popped 를 단다. */\n const item = this.renderRoot?.querySelector?.('[popped]') as HTMLElement | null\n\n if (!knob) {\n return\n }\n\n if (!item) {\n knob.hidden = true\n return\n }\n\n const rect = item.getBoundingClientRect()\n const centre = rect.top + rect.height / 2 - popTop\n /* 라운드 모서리에 걸치면 반쪽만 보인다. 그 안쪽까지만 간다. */\n const inset = 14\n const within = Math.max(inset, Math.min(centre, pop.offsetHeight - inset))\n /* 정사각의 반. 가운데가 그 줄의 가운데에 오게 한다. */\n const half = knob.offsetHeight / 2 || 5\n\n knob.hidden = false\n /* 팝업의 왼쪽 선에 반쯤 물리게 — 선이 그 자리에서 끊긴 것처럼 보인다. */\n knob.style.left = `${this._popLeft - half}px`\n knob.style.top = `${popTop + within - half}px`\n }\n\n protected updated() {\n this.fitPopup()\n }\n\n /** 접기/펼치기는 `ox-menu-part` 가 한다 — 폭과 저장이 그쪽 것이다. 여기서는 뜻만 올린다. */\n private toggleCollapsed(e: MouseEvent) {\n e.stopPropagation()\n this.popped = undefined\n this.dispatchEvent(new CustomEvent('collapse-toggle', { bubbles: true, composed: true }))\n }\n\n firstUpdated() {\n this.renderRoot.addEventListener('click', (e: Event) => {\n const menuElement = (e.target as Element)!.closest('[menu]')\n\n /*\n * 팝업 안을 눌렀으면 팝업을 닫는다 — 이동은 라우터가 한다(막지 않는다).\n *\n * 한때 팝업 컨테이너에 stopPropagation 을 걸어 두었다. 그러면 라우터까지 막혀 **페이지\n * 전체가 다시 읽혔다.** 막지 않고 닫기만 한다.\n */\n if ((e.target as Element)!.closest('.railpop')) {\n this.popped = undefined\n }\n\n /*\n * `active-toplevel` 은 **최상위에서만** 나간다.\n *\n * `closest('[menu]')` 는 가장 가까운 줄을 집으므로 하위를 누르면 하위가 잡힌다. 그것을\n * 그대로 올리면 「열린 묶음」이 하위로 바뀌어, 그 묶음이 닫힌 것처럼 된다. 지금까지는 하위\n * 클릭이 곧 이동이라 주소가 바뀌며 저절로 나았지만, 나을 이유가 없는 자리다.\n */\n //@ts-ignore\n if (menuElement?.menu && menuElement.hasAttribute('top')) {\n //@ts-ignore\n let menu = menuElement.menu\n\n /*\n * 접힌 레일에서 하위가 있는 항목은 **팝업으로 연다.**\n *\n * 이 자리에서 갈라야 한다 — 아래로 흘려 보내면 `active-toplevel` 이 나가고, 그러면\n * 56px 안에서 아무것도 안 보이는 채로 「열렸다」가 된다. 팝업이 접힌 레일에서 하위로\n * 가는 유일한 길이다.\n */\n /*\n * 하위가 있는 최상위를 눌렀다. 무엇을 할지는 **모양이 정한다**(§`menu-mode.ts`).\n *\n * 팝업으로 열리는 모양 팝업을 띄운다 (접힌 레일)\n * 이동하지 않는 모양 그 자리에서 펼치기만 한다 (서랍)\n * 나머지 이동하면서 펼친다 — 아래로 흘려 보낸다\n */\n if (menu.menus?.length && !topNavigates(this.mode) && childrenOpenAs(this.mode) === 'inline') {\n e.stopPropagation()\n e.preventDefault()\n\n this.dispatchEvent(\n new CustomEvent('active-toplevel', {\n bubbles: true,\n detail: this.activeTopLevel === menu ? undefined : menu\n })\n )\n\n return\n }\n\n if (menu.menus?.length && childrenOpenAs(this.mode) === 'popup') {\n e.stopPropagation()\n e.preventDefault()\n\n const rect = (menuElement as HTMLElement).getBoundingClientRect()\n\n /*\n * **레일의 오른쪽을 기준으로 띄운다** — 누른 항목의 오른쪽이 아니다.\n *\n * 항목은 레일 안쪽 여백만큼 들어가 있어서, 항목 기준으로 재면 팝업이 레일 끝에 정확히\n * 붙는다(56px 레일에서 left 가 56 이 됐다). 붙으면 레일을 덮은 것처럼 보인다.\n *\n * 틈은 8px 이다. 16px 은 떨어져 떠 있는 것처럼 보였고, 0 은 레일에 얹힌 것처럼\n * 보였다. 8px 이 「레일에서 열린 다른 면」으로 읽히는 자리다.\n */\n this._popLeft = this.getBoundingClientRect().right + 8\n /*\n * 누른 항목의 윗선에서 시작하는 것은 **바람**일 뿐이다. 실제 자리는 그린 뒤 높이를\n * 재서 정한다(`fitPopup`) — 여기서 고정하면 항목이 아래쪽일 때 팝업이 화면 밖으로\n * 나가거나 열여섯 줄짜리 목록에 스크롤이 생긴다.\n */\n this._popPrefer = rect.top\n this._popTop = rect.top\n this.popped = this.popped === menu ? undefined : menu\n\n return\n }\n\n /*\n * ── 화살표가 토글이고, 글자가 이동이다 ────────────────────────────\n * 최상위 항목은 두 일을 한다 — 자기 화면으로 가고, 하위를 펼친다. 글자를 누르면 링크가\n * 주소를 바꾸므로 **접기가 살아남을 수 없다** — 주소가 바뀌면 그 주소의 메뉴가 다시\n * 열린다(§`ox-menu-part` 의 `_closedByUser`).\n *\n * 그래서 접는 자리를 화살표로 옮긴다. 화살표는 이미 「펼침」을 그리고 있어서 사람이 그것을\n * 누르려 한다. 누르면 이동하지 않고 접기만 한다.\n */\n const onCaret = !!(e.target as Element)?.closest?.('[submenu-button]')\n\n if (!menu.path || onCaret) {\n /* protect to act move to href. */\n e.stopPropagation()\n e.preventDefault()\n }\n\n this.dispatchEvent(\n new CustomEvent('active-toplevel', {\n bubbles: true,\n detail: this.activeTopLevel === menu ? undefined : menu\n })\n )\n }\n\n /* to respond even if current acting menu is selected */\n let href = (e.target as HTMLAnchorElement)!.href\n href && location.href === href && navigate(href + '#force', true)\n })\n\n /* to hide scrollbar during transition */\n this.renderRoot.addEventListener('transitionstart', e => {\n ;(e.target as Element).removeAttribute('settled')\n })\n this.renderRoot.addEventListener('transitionend', e => {\n ;(e.target as Element).setAttribute('settled', '')\n })\n }\n}\n"]}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -5,7 +5,44 @@ export type Menu = {
|
|
|
5
5
|
path?: string;
|
|
6
6
|
icon?: string;
|
|
7
7
|
label?: string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 건수 — 값이거나, 값을 내는 함수.
|
|
10
|
+
*
|
|
11
|
+
* `false` 는 「없다」는 뜻이고 그때는 알약을 그리지 않는다.
|
|
12
|
+
*
|
|
13
|
+
* 함수의 반환을 `boolean` 으로 적어 두었었다. 실제로는 **화면에 그릴 수 있는 무엇이든** 온다 —
|
|
14
|
+
* 숫자이거나, 값이 갱신될 때마다 바뀌는 Lit 디렉티브다(`asyncReplace`). 앱들이 그렇게 쓰고
|
|
15
|
+
* 있었고, 적어 둔 타입만 사실이 아니었다.
|
|
16
|
+
*/
|
|
17
|
+
badge?: boolean | string | number | (() => unknown);
|
|
9
18
|
active?: boolean | ((menu?: Menu) => boolean);
|
|
10
19
|
menus?: Menu[];
|
|
20
|
+
/**
|
|
21
|
+
* 이 메뉴를 쓰려면 무엇이 있어야 하나.
|
|
22
|
+
*
|
|
23
|
+
* ── 서버의 문과 **같은 낱말**이다 ──────────────────────────────────────────
|
|
24
|
+
* 화면과 리졸버는 GraphQL 지시자로 문을 지킨다. 그 인자 넷을 그대로 받는다 — 이름을 새로
|
|
25
|
+
* 지으면 두 언어가 되고, 그러면 문과 메뉴가 다른 답을 내는 날이 온다.
|
|
26
|
+
*
|
|
27
|
+
* ```
|
|
28
|
+
* @privilege(category: "employee", privilege: "query", domainOwnerGranted: true)
|
|
29
|
+
* privilege: { category: 'employee', privilege: 'query', domainOwnerGranted: true }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ── 메뉴는 문이 아니다 ─────────────────────────────────────────────────────
|
|
33
|
+
* 이것은 **편의**다. 못 쓰는 것을 목록에서 빼서 헛걸음을 줄이는 것뿐이고, 실제로 막는 것은
|
|
34
|
+
* 화면과 리졸버다. 여기서 뺐다고 그 자리가 잠기지 않는다 — 주소를 치면 들어간다.
|
|
35
|
+
*
|
|
36
|
+
* ── 역할로 걸지 않는 이유 ──────────────────────────────────────────────────
|
|
37
|
+
* 역할은 권한을 묶는 이름이고 그 묶음은 운영 중에 바뀐다. 역할로 걸면 묶음이 바뀌는 날 메뉴와
|
|
38
|
+
* 문이 갈린다. 그리고 브라우저가 든 것은 평탄화된 권한 목록이지 역할이 아니다.
|
|
39
|
+
*
|
|
40
|
+
* 거르는 일은 메뉴를 만드는 쪽이 한다(lite-menu). 이 부품은 받은 것을 그릴 뿐이다.
|
|
41
|
+
*/
|
|
42
|
+
privilege?: {
|
|
43
|
+
category?: string;
|
|
44
|
+
privilege?: string;
|
|
45
|
+
domainOwnerGranted?: boolean;
|
|
46
|
+
superUserGranted?: boolean;
|
|
47
|
+
};
|
|
11
48
|
};
|
package/dist/src/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type Menu = {\n type?: 'group' | 'board'\n name?: string\n description?: string\n path?: string\n icon?: string\n label?: string\n badge?: boolean | string | (() =>
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type Menu = {\n type?: 'group' | 'board'\n name?: string\n description?: string\n path?: string\n icon?: string\n label?: string\n /**\n * 건수 — 값이거나, 값을 내는 함수.\n *\n * `false` 는 「없다」는 뜻이고 그때는 알약을 그리지 않는다.\n *\n * 함수의 반환을 `boolean` 으로 적어 두었었다. 실제로는 **화면에 그릴 수 있는 무엇이든** 온다 —\n * 숫자이거나, 값이 갱신될 때마다 바뀌는 Lit 디렉티브다(`asyncReplace`). 앱들이 그렇게 쓰고\n * 있었고, 적어 둔 타입만 사실이 아니었다.\n */\n badge?: boolean | string | number | (() => unknown)\n active?: boolean | ((menu?: Menu) => boolean)\n menus?: Menu[]\n\n /**\n * 이 메뉴를 쓰려면 무엇이 있어야 하나.\n *\n * ── 서버의 문과 **같은 낱말**이다 ──────────────────────────────────────────\n * 화면과 리졸버는 GraphQL 지시자로 문을 지킨다. 그 인자 넷을 그대로 받는다 — 이름을 새로\n * 지으면 두 언어가 되고, 그러면 문과 메뉴가 다른 답을 내는 날이 온다.\n *\n * ```\n * @privilege(category: \"employee\", privilege: \"query\", domainOwnerGranted: true)\n * privilege: { category: 'employee', privilege: 'query', domainOwnerGranted: true }\n * ```\n *\n * ── 메뉴는 문이 아니다 ─────────────────────────────────────────────────────\n * 이것은 **편의**다. 못 쓰는 것을 목록에서 빼서 헛걸음을 줄이는 것뿐이고, 실제로 막는 것은\n * 화면과 리졸버다. 여기서 뺐다고 그 자리가 잠기지 않는다 — 주소를 치면 들어간다.\n *\n * ── 역할로 걸지 않는 이유 ──────────────────────────────────────────────────\n * 역할은 권한을 묶는 이름이고 그 묶음은 운영 중에 바뀐다. 역할로 걸면 묶음이 바뀌는 날 메뉴와\n * 문이 갈린다. 그리고 브라우저가 든 것은 평탄화된 권한 목록이지 역할이 아니다.\n *\n * 거르는 일은 메뉴를 만드는 쪽이 한다(lite-menu). 이 부품은 받은 것을 그릴 뿐이다.\n */\n privilege?: {\n category?: string\n privilege?: string\n domainOwnerGranted?: boolean\n superUserGranted?: boolean\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { joinedPath, menuForRoute, resolveActive } from '../src/menu-active.js';
|
|
3
|
+
/**
|
|
4
|
+
* 지금 보고 있는 자리에 해당하는 메뉴를 찾는 규칙.
|
|
5
|
+
*
|
|
6
|
+
* 이 규칙이 컴포넌트의 `willUpdate` 안에 있었고 두 번 조용히 깨졌다.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* 최상위가 하위의 표시를 삼켰다 최상위마다 하위 하나가 표시를 못 받았다
|
|
10
|
+
* 손으로 여닫은 것이 되돌려졌다 매 렌더마다 주소로 다시 계산되니까
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 둘 다 화면을 눈으로 보고서야 알았다. 여기서 못박는다.
|
|
14
|
+
*/
|
|
15
|
+
/** 실제 앱(operato-plant)의 모양을 줄여서 쓴다 — 최상위가 자기 하위 하나의 주소를 갖는다. */
|
|
16
|
+
const 메뉴 = [
|
|
17
|
+
{
|
|
18
|
+
name: 'my-work',
|
|
19
|
+
path: 'todo-list',
|
|
20
|
+
menus: [
|
|
21
|
+
{ name: 'todo', path: 'todo-list' },
|
|
22
|
+
{ name: 'approval-pending', path: 'approval-pending-list' },
|
|
23
|
+
{ name: 'done', path: 'done-list' }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'quality',
|
|
28
|
+
path: 'quality-list',
|
|
29
|
+
menus: [
|
|
30
|
+
{ name: 'inspection', path: 'quality-list' },
|
|
31
|
+
{ name: 'disposition', path: 'quality-list/disposition' }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{ name: 'maturity', path: 'maturity-page' }
|
|
35
|
+
];
|
|
36
|
+
describe('활성 메뉴 찾기', () => {
|
|
37
|
+
describe('주소를 한 문자열로 잇는다', () => {
|
|
38
|
+
it('자리가 없으면 화면 이름만', () => {
|
|
39
|
+
expect(joinedPath({ page: 'quality-list' })).to.equal('quality-list');
|
|
40
|
+
});
|
|
41
|
+
it('자리가 있으면 이어 붙인다', () => {
|
|
42
|
+
expect(joinedPath({ page: 'quality-list', resourceId: 'disposition' })).to.equal('quality-list/disposition');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('하위가 최상위를 이긴다', () => {
|
|
46
|
+
it('최상위와 주소가 같은 하위가 표시를 받는다', () => {
|
|
47
|
+
const { top, menu } = menuForRoute(메뉴, { page: 'todo-list' });
|
|
48
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('my-work');
|
|
49
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name, '최상위가 매치를 삼키면 이 하위가 표시를 못 받는다').to.equal('todo');
|
|
50
|
+
});
|
|
51
|
+
it('최상위마다 그 하나가 있다 — 품질도 같다', () => {
|
|
52
|
+
const { top, menu } = menuForRoute(메뉴, { page: 'quality-list' });
|
|
53
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('quality');
|
|
54
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('inspection');
|
|
55
|
+
});
|
|
56
|
+
it('하위가 없는 최상위는 자기가 그 자리다', () => {
|
|
57
|
+
const { top, menu } = menuForRoute(메뉴, { page: 'maturity-page' });
|
|
58
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('maturity');
|
|
59
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('maturity');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('화면 안의 자리까지 가리키는 주소', () => {
|
|
63
|
+
it('`a/b` 를 선언한 하위가 `a` 를 선언한 하위를 이긴다', () => {
|
|
64
|
+
const { menu } = menuForRoute(메뉴, { page: 'quality-list', resourceId: 'disposition' });
|
|
65
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('disposition');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('화면이 스스로 딸린 메뉴를 말할 때', () => {
|
|
69
|
+
it('메뉴에 없는 상세 화면도 표시를 받는다', () => {
|
|
70
|
+
const { top, menu } = menuForRoute(메뉴, {
|
|
71
|
+
page: 'activity-approval',
|
|
72
|
+
resourceId: 'abc-123',
|
|
73
|
+
context: { menu: 'approval-pending-list' }
|
|
74
|
+
});
|
|
75
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('my-work');
|
|
76
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('approval-pending');
|
|
77
|
+
});
|
|
78
|
+
it('말하지 않으면 아무 표시도 없다 — 없는 것을 지어내지 않는다', () => {
|
|
79
|
+
const { top, menu } = menuForRoute(메뉴, { page: 'activity-approval', resourceId: 'abc-123' });
|
|
80
|
+
expect(top).to.be.undefined;
|
|
81
|
+
expect(menu).to.be.undefined;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('앱이 직접 판정을 줄 때', () => {
|
|
85
|
+
it('active 함수가 참이면 그 메뉴다', () => {
|
|
86
|
+
const 판정메뉴 = [
|
|
87
|
+
{ name: 'reports', menus: [{ name: 'daily', active: () => true }] },
|
|
88
|
+
...메뉴
|
|
89
|
+
];
|
|
90
|
+
const { menu } = menuForRoute(판정메뉴, { page: 'whatever' });
|
|
91
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('daily');
|
|
92
|
+
});
|
|
93
|
+
it('선언한 경로가 판정보다 앞이다 — 적어 둔 경로가 더 분명한 뜻이다', () => {
|
|
94
|
+
const 판정메뉴 = [
|
|
95
|
+
{ name: 'guess', menus: [{ name: 'guessed', active: () => true }] },
|
|
96
|
+
{ name: 'exact', path: 'todo-list', menus: [{ name: 'exactly', path: 'todo-list' }] }
|
|
97
|
+
];
|
|
98
|
+
const { menu } = menuForRoute(판정메뉴, { page: 'todo-list' });
|
|
99
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('exactly');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('손으로 정한 펼침이 주소를 덮는다', () => {
|
|
103
|
+
const 여기 = { page: 'todo-list' };
|
|
104
|
+
it('정한 적 없으면 주소가 가리키는 것이 열린다', () => {
|
|
105
|
+
const { top, menu } = resolveActive(메뉴, 여기, undefined);
|
|
106
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('my-work');
|
|
107
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name).to.equal('todo');
|
|
108
|
+
});
|
|
109
|
+
it('닫았으면 아무것도 열리지 않는다 — 표시는 남는다', () => {
|
|
110
|
+
const { top, menu } = resolveActive(메뉴, 여기, null);
|
|
111
|
+
expect(top, '닫기 코드는 있었지만 주소가 매 렌더마다 되돌렸다').to.be.undefined;
|
|
112
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name, '접혀 있어도 어느 화면에 있는지는 알아야 한다').to.equal('todo');
|
|
113
|
+
});
|
|
114
|
+
it('다른 묶음을 열었으면 그것이 열린다 — 표시는 여전히 지금 화면이다', () => {
|
|
115
|
+
const { top, menu } = resolveActive(메뉴, 여기, 메뉴[1]);
|
|
116
|
+
expect(top === null || top === void 0 ? void 0 : top.name, '「닫았다」만 기억하면 이 방향이 되돌려진다').to.equal('quality');
|
|
117
|
+
expect(menu === null || menu === void 0 ? void 0 : menu.name, '다른 묶음을 둘러보는 동안에도 자기 자리는 알아야 한다').to.equal('todo');
|
|
118
|
+
});
|
|
119
|
+
it('주소가 가리키는 묶음을 손으로 다시 열어도 같다', () => {
|
|
120
|
+
const { top } = resolveActive(메뉴, 여기, 메뉴[0]);
|
|
121
|
+
expect(top === null || top === void 0 ? void 0 : top.name).to.equal('my-work');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('메뉴가 아직 없을 때', () => {
|
|
125
|
+
it('빈 목록이면 아무것도 내지 않는다 — 터지지 않는다', () => {
|
|
126
|
+
expect(menuForRoute([], { page: 'todo-list' })).to.deep.equal({ top: undefined, menu: undefined });
|
|
127
|
+
expect(resolveActive([], { page: 'todo-list' }, undefined)).to.deep.equal({ top: undefined, menu: undefined });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=menu-active.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-active.test.js","sourceRoot":"","sources":["../../test/menu-active.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAGzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAE/E;;;;;;;;;;;GAWG;AAEH,gEAAgE;AAChE,MAAM,EAAE,GAAW;IACjB;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE;YACL,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE;YACnC,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,uBAAuB,EAAE;YAC3D,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE;SACpC;KACF;IACD;QACE,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE;YACL,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE;YAC5C,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,0BAA0B,EAAE;SAC1D;KACF;IACD,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE;CAC5C,CAAA;AAED,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,gBAAgB,EAAE,GAAG,EAAE;YACxB,MAAM,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;QACvE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,gBAAgB,EAAE,GAAG,EAAE;YACxB,MAAM,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAA;QAC9G,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;YAE7D,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACrC,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,EAAE,8BAA8B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACrE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAA;YAEhE,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACrC,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;QAC3C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;YAC/B,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAA;YAEjE,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YACtC,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACzC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAA;YAEtF,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;QAC5C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;YAC/B,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE;gBACrC,IAAI,EAAE,mBAAmB;gBACzB,UAAU,EAAE,SAAS;gBACrB,OAAO,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE;aAC3C,CAAC,CAAA;YAEF,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACrC,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAA;YAE5F,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAA;YAC3B,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAA;QAC9B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;YAC9B,MAAM,IAAI,GAAW;gBACnB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE;gBACnE,GAAG,EAAE;aACN,CAAA;YACD,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;YAEzD,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,MAAM,IAAI,GAAW;gBACnB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE;gBACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE;aACtF,CAAA;YACD,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;YAE1D,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QACxC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;QAEhC,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;YAClC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;YAEtD,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACrC,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;YAEjD,MAAM,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAA;YAC1D,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,EAAE,2BAA2B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAClE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YAElD,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,EAAE,yBAAyB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YAChE,MAAM,CAAC,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,IAAI,EAAE,gCAAgC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACvE,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YAE5C,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QACvC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QAC3B,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;YACtC,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;YAClG,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAChH,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport { Menu } from '../src/types.js'\nimport { joinedPath, menuForRoute, resolveActive } from '../src/menu-active.js'\n\n/**\n * 지금 보고 있는 자리에 해당하는 메뉴를 찾는 규칙.\n *\n * 이 규칙이 컴포넌트의 `willUpdate` 안에 있었고 두 번 조용히 깨졌다.\n *\n * ```\n * 최상위가 하위의 표시를 삼켰다 최상위마다 하위 하나가 표시를 못 받았다\n * 손으로 여닫은 것이 되돌려졌다 매 렌더마다 주소로 다시 계산되니까\n * ```\n *\n * 둘 다 화면을 눈으로 보고서야 알았다. 여기서 못박는다.\n */\n\n/** 실제 앱(operato-plant)의 모양을 줄여서 쓴다 — 최상위가 자기 하위 하나의 주소를 갖는다. */\nconst 메뉴: Menu[] = [\n {\n name: 'my-work',\n path: 'todo-list',\n menus: [\n { name: 'todo', path: 'todo-list' },\n { name: 'approval-pending', path: 'approval-pending-list' },\n { name: 'done', path: 'done-list' }\n ]\n },\n {\n name: 'quality',\n path: 'quality-list',\n menus: [\n { name: 'inspection', path: 'quality-list' },\n { name: 'disposition', path: 'quality-list/disposition' }\n ]\n },\n { name: 'maturity', path: 'maturity-page' }\n]\n\ndescribe('활성 메뉴 찾기', () => {\n describe('주소를 한 문자열로 잇는다', () => {\n it('자리가 없으면 화면 이름만', () => {\n expect(joinedPath({ page: 'quality-list' })).to.equal('quality-list')\n })\n\n it('자리가 있으면 이어 붙인다', () => {\n expect(joinedPath({ page: 'quality-list', resourceId: 'disposition' })).to.equal('quality-list/disposition')\n })\n })\n\n describe('하위가 최상위를 이긴다', () => {\n it('최상위와 주소가 같은 하위가 표시를 받는다', () => {\n const { top, menu } = menuForRoute(메뉴, { page: 'todo-list' })\n\n expect(top?.name).to.equal('my-work')\n expect(menu?.name, '최상위가 매치를 삼키면 이 하위가 표시를 못 받는다').to.equal('todo')\n })\n\n it('최상위마다 그 하나가 있다 — 품질도 같다', () => {\n const { top, menu } = menuForRoute(메뉴, { page: 'quality-list' })\n\n expect(top?.name).to.equal('quality')\n expect(menu?.name).to.equal('inspection')\n })\n\n it('하위가 없는 최상위는 자기가 그 자리다', () => {\n const { top, menu } = menuForRoute(메뉴, { page: 'maturity-page' })\n\n expect(top?.name).to.equal('maturity')\n expect(menu?.name).to.equal('maturity')\n })\n })\n\n describe('화면 안의 자리까지 가리키는 주소', () => {\n it('`a/b` 를 선언한 하위가 `a` 를 선언한 하위를 이긴다', () => {\n const { menu } = menuForRoute(메뉴, { page: 'quality-list', resourceId: 'disposition' })\n\n expect(menu?.name).to.equal('disposition')\n })\n })\n\n describe('화면이 스스로 딸린 메뉴를 말할 때', () => {\n it('메뉴에 없는 상세 화면도 표시를 받는다', () => {\n const { top, menu } = menuForRoute(메뉴, {\n page: 'activity-approval',\n resourceId: 'abc-123',\n context: { menu: 'approval-pending-list' }\n })\n\n expect(top?.name).to.equal('my-work')\n expect(menu?.name).to.equal('approval-pending')\n })\n\n it('말하지 않으면 아무 표시도 없다 — 없는 것을 지어내지 않는다', () => {\n const { top, menu } = menuForRoute(메뉴, { page: 'activity-approval', resourceId: 'abc-123' })\n\n expect(top).to.be.undefined\n expect(menu).to.be.undefined\n })\n })\n\n describe('앱이 직접 판정을 줄 때', () => {\n it('active 함수가 참이면 그 메뉴다', () => {\n const 판정메뉴: Menu[] = [\n { name: 'reports', menus: [{ name: 'daily', active: () => true }] },\n ...메뉴\n ]\n const { menu } = menuForRoute(판정메뉴, { page: 'whatever' })\n\n expect(menu?.name).to.equal('daily')\n })\n\n it('선언한 경로가 판정보다 앞이다 — 적어 둔 경로가 더 분명한 뜻이다', () => {\n const 판정메뉴: Menu[] = [\n { name: 'guess', menus: [{ name: 'guessed', active: () => true }] },\n { name: 'exact', path: 'todo-list', menus: [{ name: 'exactly', path: 'todo-list' }] }\n ]\n const { menu } = menuForRoute(판정메뉴, { page: 'todo-list' })\n\n expect(menu?.name).to.equal('exactly')\n })\n })\n\n describe('손으로 정한 펼침이 주소를 덮는다', () => {\n const 여기 = { page: 'todo-list' }\n\n it('정한 적 없으면 주소가 가리키는 것이 열린다', () => {\n const { top, menu } = resolveActive(메뉴, 여기, undefined)\n\n expect(top?.name).to.equal('my-work')\n expect(menu?.name).to.equal('todo')\n })\n\n it('닫았으면 아무것도 열리지 않는다 — 표시는 남는다', () => {\n const { top, menu } = resolveActive(메뉴, 여기, null)\n\n expect(top, '닫기 코드는 있었지만 주소가 매 렌더마다 되돌렸다').to.be.undefined\n expect(menu?.name, '접혀 있어도 어느 화면에 있는지는 알아야 한다').to.equal('todo')\n })\n\n it('다른 묶음을 열었으면 그것이 열린다 — 표시는 여전히 지금 화면이다', () => {\n const { top, menu } = resolveActive(메뉴, 여기, 메뉴[1])\n\n expect(top?.name, '「닫았다」만 기억하면 이 방향이 되돌려진다').to.equal('quality')\n expect(menu?.name, '다른 묶음을 둘러보는 동안에도 자기 자리는 알아야 한다').to.equal('todo')\n })\n\n it('주소가 가리키는 묶음을 손으로 다시 열어도 같다', () => {\n const { top } = resolveActive(메뉴, 여기, 메뉴[0])\n\n expect(top?.name).to.equal('my-work')\n })\n })\n\n describe('메뉴가 아직 없을 때', () => {\n it('빈 목록이면 아무것도 내지 않는다 — 터지지 않는다', () => {\n expect(menuForRoute([], { page: 'todo-list' })).to.deep.equal({ top: undefined, menu: undefined })\n expect(resolveActive([], { page: 'todo-list' }, undefined)).to.deep.equal({ top: undefined, menu: undefined })\n })\n })\n})\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { AUTO_COLLAPSE_UNDER, childrenOpenAs, menuMode, shouldAutoCollapse, showsLabels, topNavigates } from '../src/menu-mode.js';
|
|
3
|
+
/**
|
|
4
|
+
* 왼쪽 메뉴의 **모양 표**.
|
|
5
|
+
*
|
|
6
|
+
* 이 표가 깨진 적이 네 번이다 — 합계 배지 감추기 · 시스템 자리 · 팝업 꾸밈 · 서랍 판정. 넷 다
|
|
7
|
+
* 화면을 눈으로 보고서야 알았고, 넷 다 「이 모양에서는 어떻게 되나」를 그때그때 판단해서 났다.
|
|
8
|
+
*
|
|
9
|
+
* 여기서 표 전체를 못박는다. 규칙을 하나 고치면 다른 칸이 함께 빨개진다.
|
|
10
|
+
*/
|
|
11
|
+
describe('메뉴 모양', () => {
|
|
12
|
+
describe('세 플래그에서 모양 하나를 정한다', () => {
|
|
13
|
+
it('아무것도 켜지 않으면 기존 메뉴다', () => {
|
|
14
|
+
expect(menuMode({})).to.equal('list');
|
|
15
|
+
});
|
|
16
|
+
it('레일만 켜면 펼친 레일이다', () => {
|
|
17
|
+
expect(menuMode({ rail: true })).to.equal('rail');
|
|
18
|
+
});
|
|
19
|
+
it('레일에 접힘을 더하면 접힌 레일이다', () => {
|
|
20
|
+
expect(menuMode({ rail: true, collapsed: true })).to.equal('rail-collapsed');
|
|
21
|
+
});
|
|
22
|
+
it('레일 없이 접힘만 켜도 기존 메뉴다 — 접을 대상이 없다', () => {
|
|
23
|
+
expect(menuMode({ collapsed: true })).to.equal('list');
|
|
24
|
+
});
|
|
25
|
+
it('서랍이 가장 세다 — 레일과 접힘을 함께 켜도 서랍이다', () => {
|
|
26
|
+
expect(menuMode({ drawer: true })).to.equal('drawer');
|
|
27
|
+
expect(menuMode({ drawer: true, rail: true })).to.equal('drawer');
|
|
28
|
+
expect(menuMode({ drawer: true, rail: true, collapsed: true })).to.equal('drawer');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('하위를 어디에 여나', () => {
|
|
32
|
+
it('접힌 레일에서만 팝업이다 — 56px 안에 들여쓴 목록이 안 들어간다', () => {
|
|
33
|
+
expect(childrenOpenAs('rail-collapsed')).to.equal('popup');
|
|
34
|
+
});
|
|
35
|
+
it('나머지 셋은 그 자리에서 열린다', () => {
|
|
36
|
+
expect(childrenOpenAs('list')).to.equal('inline');
|
|
37
|
+
expect(childrenOpenAs('rail')).to.equal('inline');
|
|
38
|
+
expect(childrenOpenAs('drawer')).to.equal('inline');
|
|
39
|
+
});
|
|
40
|
+
it('서랍은 팝업이 아니다 — 화면 위에 뜬 서랍 옆에 고정 팝업이 설 자리가 없다', () => {
|
|
41
|
+
expect(childrenOpenAs(menuMode({ drawer: true }))).to.equal('inline');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('최상위를 누르면 이동하나', () => {
|
|
45
|
+
it('늘 보이는 메뉴에서는 이동한다 — 누르면 어디든 도착한다', () => {
|
|
46
|
+
expect(topNavigates('list')).to.be.true;
|
|
47
|
+
expect(topNavigates('rail')).to.be.true;
|
|
48
|
+
});
|
|
49
|
+
it('서랍에서는 이동하지 않는다 — 이동하면 서랍이 닫혀 나머지 하위를 볼 기회가 없다', () => {
|
|
50
|
+
expect(topNavigates('drawer')).to.be.false;
|
|
51
|
+
});
|
|
52
|
+
it('접힌 레일에서도 이동하지 않는다 — 팝업을 열어 고르게 한다', () => {
|
|
53
|
+
expect(topNavigates('rail-collapsed')).to.be.false;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('이름이 보이나', () => {
|
|
57
|
+
it('접힌 레일만 아이콘뿐이다', () => {
|
|
58
|
+
expect(showsLabels('rail-collapsed')).to.be.false;
|
|
59
|
+
expect(showsLabels('list')).to.be.true;
|
|
60
|
+
expect(showsLabels('rail')).to.be.true;
|
|
61
|
+
expect(showsLabels('drawer')).to.be.true;
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('좁아지면 스스로 접는다', () => {
|
|
65
|
+
it('기준 폭 아래면 접는다 — 이름이 이름 노릇을 못 한다', () => {
|
|
66
|
+
expect(shouldAutoCollapse(120, { rail: true })).to.be.true;
|
|
67
|
+
expect(shouldAutoCollapse(AUTO_COLLAPSE_UNDER, { rail: true })).to.be.true;
|
|
68
|
+
});
|
|
69
|
+
it('기준 폭보다 넓으면 그대로 둔다', () => {
|
|
70
|
+
expect(shouldAutoCollapse(AUTO_COLLAPSE_UNDER + 1, { rail: true })).to.be.false;
|
|
71
|
+
expect(shouldAutoCollapse(208, { rail: true })).to.be.false;
|
|
72
|
+
});
|
|
73
|
+
it('이미 접혔으면 보지 않는다 — 56px 이 이 판정에 다시 걸려 되풀이된다', () => {
|
|
74
|
+
expect(shouldAutoCollapse(56, { rail: true, collapsed: true })).to.be.false;
|
|
75
|
+
});
|
|
76
|
+
it('레일이 아니면 접을 것이 없다', () => {
|
|
77
|
+
expect(shouldAutoCollapse(60, {})).to.be.false;
|
|
78
|
+
expect(shouldAutoCollapse(60, { rail: false })).to.be.false;
|
|
79
|
+
});
|
|
80
|
+
it('폭이 0 이면 판단하지 않는다 — 붙는 순간이거나 감춰진 순간이다', () => {
|
|
81
|
+
expect(shouldAutoCollapse(0, { rail: true })).to.be.false;
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('표 전체 — 네 모양 × 세 답', () => {
|
|
85
|
+
/**
|
|
86
|
+
* 이 배열이 곧 문서다. 규칙을 고치면 여기도 고쳐야 하고, 고치는 순간 그 판단을 한 번 더
|
|
87
|
+
* 들여다보게 된다.
|
|
88
|
+
*/
|
|
89
|
+
const 표 = [
|
|
90
|
+
{ 모양: 'list', 하위: 'inline', 이동: true, 이름: true },
|
|
91
|
+
{ 모양: 'rail', 하위: 'inline', 이동: true, 이름: true },
|
|
92
|
+
{ 모양: 'rail-collapsed', 하위: 'popup', 이동: false, 이름: false },
|
|
93
|
+
{ 모양: 'drawer', 하위: 'inline', 이동: false, 이름: true }
|
|
94
|
+
];
|
|
95
|
+
표.forEach(줄 => {
|
|
96
|
+
it(`${줄.모양} — 하위 ${줄.하위} · 이동 ${줄.이동} · 이름 ${줄.이름}`, () => {
|
|
97
|
+
expect(childrenOpenAs(줄.모양)).to.equal(줄.하위);
|
|
98
|
+
expect(topNavigates(줄.모양)).to.equal(줄.이동);
|
|
99
|
+
expect(showsLabels(줄.모양)).to.equal(줄.이름);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('네 모양을 다 덮는다 — 새 모양이 생기면 이 시험이 먼저 빨개진다', () => {
|
|
103
|
+
const 모양들 = ['list', 'rail', 'rail-collapsed', 'drawer'];
|
|
104
|
+
expect(표.map(줄 => 줄.모양)).to.deep.equal(모양들);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=menu-mode.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-mode.test.js","sourceRoot":"","sources":["../../test/menu-mode.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport {\n AUTO_COLLAPSE_UNDER,\n childrenOpenAs,\n menuMode,\n shouldAutoCollapse,\n showsLabels,\n topNavigates\n} from '../src/menu-mode.js'\n\n/**\n * 왼쪽 메뉴의 **모양 표**.\n *\n * 이 표가 깨진 적이 네 번이다 — 합계 배지 감추기 · 시스템 자리 · 팝업 꾸밈 · 서랍 판정. 넷 다\n * 화면을 눈으로 보고서야 알았고, 넷 다 「이 모양에서는 어떻게 되나」를 그때그때 판단해서 났다.\n *\n * 여기서 표 전체를 못박는다. 규칙을 하나 고치면 다른 칸이 함께 빨개진다.\n */\ndescribe('메뉴 모양', () => {\n describe('세 플래그에서 모양 하나를 정한다', () => {\n it('아무것도 켜지 않으면 기존 메뉴다', () => {\n expect(menuMode({})).to.equal('list')\n })\n\n it('레일만 켜면 펼친 레일이다', () => {\n expect(menuMode({ rail: true })).to.equal('rail')\n })\n\n it('레일에 접힘을 더하면 접힌 레일이다', () => {\n expect(menuMode({ rail: true, collapsed: true })).to.equal('rail-collapsed')\n })\n\n it('레일 없이 접힘만 켜도 기존 메뉴다 — 접을 대상이 없다', () => {\n expect(menuMode({ collapsed: true })).to.equal('list')\n })\n\n it('서랍이 가장 세다 — 레일과 접힘을 함께 켜도 서랍이다', () => {\n expect(menuMode({ drawer: true })).to.equal('drawer')\n expect(menuMode({ drawer: true, rail: true })).to.equal('drawer')\n expect(menuMode({ drawer: true, rail: true, collapsed: true })).to.equal('drawer')\n })\n })\n\n describe('하위를 어디에 여나', () => {\n it('접힌 레일에서만 팝업이다 — 56px 안에 들여쓴 목록이 안 들어간다', () => {\n expect(childrenOpenAs('rail-collapsed')).to.equal('popup')\n })\n\n it('나머지 셋은 그 자리에서 열린다', () => {\n expect(childrenOpenAs('list')).to.equal('inline')\n expect(childrenOpenAs('rail')).to.equal('inline')\n expect(childrenOpenAs('drawer')).to.equal('inline')\n })\n\n it('서랍은 팝업이 아니다 — 화면 위에 뜬 서랍 옆에 고정 팝업이 설 자리가 없다', () => {\n expect(childrenOpenAs(menuMode({ drawer: true }))).to.equal('inline')\n })\n })\n\n describe('최상위를 누르면 이동하나', () => {\n it('늘 보이는 메뉴에서는 이동한다 — 누르면 어디든 도착한다', () => {\n expect(topNavigates('list')).to.be.true\n expect(topNavigates('rail')).to.be.true\n })\n\n it('서랍에서는 이동하지 않는다 — 이동하면 서랍이 닫혀 나머지 하위를 볼 기회가 없다', () => {\n expect(topNavigates('drawer')).to.be.false\n })\n\n it('접힌 레일에서도 이동하지 않는다 — 팝업을 열어 고르게 한다', () => {\n expect(topNavigates('rail-collapsed')).to.be.false\n })\n })\n\n describe('이름이 보이나', () => {\n it('접힌 레일만 아이콘뿐이다', () => {\n expect(showsLabels('rail-collapsed')).to.be.false\n expect(showsLabels('list')).to.be.true\n expect(showsLabels('rail')).to.be.true\n expect(showsLabels('drawer')).to.be.true\n })\n })\n\n describe('좁아지면 스스로 접는다', () => {\n it('기준 폭 아래면 접는다 — 이름이 이름 노릇을 못 한다', () => {\n expect(shouldAutoCollapse(120, { rail: true })).to.be.true\n expect(shouldAutoCollapse(AUTO_COLLAPSE_UNDER, { rail: true })).to.be.true\n })\n\n it('기준 폭보다 넓으면 그대로 둔다', () => {\n expect(shouldAutoCollapse(AUTO_COLLAPSE_UNDER + 1, { rail: true })).to.be.false\n expect(shouldAutoCollapse(208, { rail: true })).to.be.false\n })\n\n it('이미 접혔으면 보지 않는다 — 56px 이 이 판정에 다시 걸려 되풀이된다', () => {\n expect(shouldAutoCollapse(56, { rail: true, collapsed: true })).to.be.false\n })\n\n it('레일이 아니면 접을 것이 없다', () => {\n expect(shouldAutoCollapse(60, {})).to.be.false\n expect(shouldAutoCollapse(60, { rail: false })).to.be.false\n })\n\n it('폭이 0 이면 판단하지 않는다 — 붙는 순간이거나 감춰진 순간이다', () => {\n expect(shouldAutoCollapse(0, { rail: true })).to.be.false\n })\n })\n\n describe('표 전체 — 네 모양 × 세 답', () => {\n /**\n * 이 배열이 곧 문서다. 규칙을 고치면 여기도 고쳐야 하고, 고치는 순간 그 판단을 한 번 더\n * 들여다보게 된다.\n */\n const 표: { 모양: ReturnType<typeof menuMode>; 하위: 'inline' | 'popup'; 이동: boolean; 이름: boolean }[] = [\n { 모양: 'list', 하위: 'inline', 이동: true, 이름: true },\n { 모양: 'rail', 하위: 'inline', 이동: true, 이름: true },\n { 모양: 'rail-collapsed', 하위: 'popup', 이동: false, 이름: false },\n { 모양: 'drawer', 하위: 'inline', 이동: false, 이름: true }\n ]\n\n 표.forEach(줄 => {\n it(`${줄.모양} — 하위 ${줄.하위} · 이동 ${줄.이동} · 이름 ${줄.이름}`, () => {\n expect(childrenOpenAs(줄.모양)).to.equal(줄.하위)\n expect(topNavigates(줄.모양)).to.equal(줄.이동)\n expect(showsLabels(줄.모양)).to.equal(줄.이름)\n })\n })\n\n it('네 모양을 다 덮는다 — 새 모양이 생기면 이 시험이 먼저 빨개진다', () => {\n const 모양들: ReturnType<typeof menuMode>[] = ['list', 'rail', 'rail-collapsed', 'drawer']\n expect(표.map(줄 => 줄.모양)).to.deep.equal(모양들)\n })\n })\n})\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { permittedMenus } from '../src/menu-privilege.js';
|
|
3
|
+
/**
|
|
4
|
+
* 쓸 수 있는 메뉴만 남기는 규칙.
|
|
5
|
+
*
|
|
6
|
+
* 이 판단이 앱마다 있었다. operato-plant 는 「소유자면 조직 묶음을 배열에 넣는다」였고,
|
|
7
|
+
* operato-twin 은 항목마다 `superOnly` · `ownerOnly` 플래그였다. 같은 물음에 답이 둘이었고,
|
|
8
|
+
* 둘 다 **서버의 문과 다른 낱말**이라 문이 열어 주는 사람에게 메뉴가 안 보이는 일이 났다.
|
|
9
|
+
*
|
|
10
|
+
* 규칙을 한 자리에 두고 여기서 못박는다.
|
|
11
|
+
*/
|
|
12
|
+
/** 못 가진 요구 하나 — 이것이 걸리면 그 줄은 사라져야 한다. */
|
|
13
|
+
const 없는권한 = { category: '없는분류', privilege: 'query' };
|
|
14
|
+
const 있는권한 = { category: 'employee', privilege: 'query' };
|
|
15
|
+
/** 시험용 판정 — 「employee/query 만 가졌다」. */
|
|
16
|
+
const 가진사람 = p => p.category === 있는권한.category && p.privilege === 있는권한.privilege;
|
|
17
|
+
const 이름들 = (menus) => menus.map(menu => menu.name);
|
|
18
|
+
describe('쓸 수 있는 메뉴만 남긴다', () => {
|
|
19
|
+
it('요구가 없는 줄은 그대로 둔다', async () => {
|
|
20
|
+
const 남은것 = await permittedMenus([{ name: 'plain', path: 'a' }], 가진사람);
|
|
21
|
+
expect(이름들(남은것)).to.deep.equal(['plain']);
|
|
22
|
+
});
|
|
23
|
+
it('가진 요구면 남고, 못 가진 요구면 빠진다', async () => {
|
|
24
|
+
const 남은것 = await permittedMenus([
|
|
25
|
+
{ name: 'mine', path: 'a', privilege: 있는권한 },
|
|
26
|
+
{ name: 'theirs', path: 'b', privilege: 없는권한 }
|
|
27
|
+
], 가진사람);
|
|
28
|
+
expect(이름들(남은것)).to.deep.equal(['mine']);
|
|
29
|
+
});
|
|
30
|
+
it('부모가 걸리면 자식까지 뺀다 — 묶음에 못 들어가면 그 안도 갈 수 없다', async () => {
|
|
31
|
+
const 남은것 = await permittedMenus([{ name: 'group', path: 'a', privilege: 없는권한, menus: [{ name: 'kid', path: 'a1' }] }], 가진사람);
|
|
32
|
+
expect(남은것).to.deep.equal([]);
|
|
33
|
+
});
|
|
34
|
+
it('자식이 다 빠진 부모도 뺀다 — 눌러도 아무 데도 못 간다', async () => {
|
|
35
|
+
const 남은것 = await permittedMenus([{ name: 'group', path: 'a', menus: [{ name: 'kid', path: 'a1', privilege: 없는권한 }] }], 가진사람);
|
|
36
|
+
expect(남은것).to.deep.equal([]);
|
|
37
|
+
});
|
|
38
|
+
it('자식이 하나라도 남으면 부모도 남는다 — 남은 것만 데리고', async () => {
|
|
39
|
+
const 남은것 = await permittedMenus([
|
|
40
|
+
{
|
|
41
|
+
name: 'group',
|
|
42
|
+
path: 'a',
|
|
43
|
+
menus: [
|
|
44
|
+
{ name: 'stay', path: 'a1' },
|
|
45
|
+
{ name: 'go', path: 'a2', privilege: 없는권한 }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
], 가진사람);
|
|
49
|
+
expect(이름들(남은것)).to.deep.equal(['group']);
|
|
50
|
+
expect(이름들(남은것[0].menus)).to.deep.equal(['stay']);
|
|
51
|
+
});
|
|
52
|
+
it('원래 자식이 없던 잎은 남는다 — 「자식이 다 빠졌다」로 읽지 않는다', async () => {
|
|
53
|
+
const 남은것 = await permittedMenus([{ name: 'leaf', path: 'a' }], 가진사람);
|
|
54
|
+
expect(이름들(남은것), '이 갈래가 없으면 잎이 전부 사라진다').to.deep.equal(['leaf']);
|
|
55
|
+
});
|
|
56
|
+
it('원래 것을 고치지 않는다 — 다시 거를 때 앞의 결과가 섞이지 않게', async () => {
|
|
57
|
+
const 원래 = [
|
|
58
|
+
{
|
|
59
|
+
name: 'group',
|
|
60
|
+
path: 'a',
|
|
61
|
+
menus: [
|
|
62
|
+
{ name: 'stay', path: 'a1' },
|
|
63
|
+
{ name: 'go', path: 'a2', privilege: 없는권한 }
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
await permittedMenus(원래, 가진사람);
|
|
68
|
+
expect(원래[0].menus, '입력을 고치면 로그인할 때마다 메뉴가 줄어든다').to.have.lengthOf(2);
|
|
69
|
+
});
|
|
70
|
+
it('판정이 늦게 와도 기다린다 — 프로필은 비동기로 온다', async () => {
|
|
71
|
+
const 느린판정 = async (p) => {
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
73
|
+
return 가진사람(p);
|
|
74
|
+
};
|
|
75
|
+
const 남은것 = await permittedMenus([
|
|
76
|
+
{ name: 'mine', path: 'a', privilege: 있는권한 },
|
|
77
|
+
{ name: 'theirs', path: 'b', privilege: 없는권한 }
|
|
78
|
+
], 느린판정);
|
|
79
|
+
expect(이름들(남은것)).to.deep.equal(['mine']);
|
|
80
|
+
});
|
|
81
|
+
it('여러 층을 내려간다', async () => {
|
|
82
|
+
const 남은것 = await permittedMenus([
|
|
83
|
+
{
|
|
84
|
+
name: 'top',
|
|
85
|
+
menus: [
|
|
86
|
+
{ name: 'mid', menus: [{ name: 'deep', path: 'x', privilege: 없는권한 }] },
|
|
87
|
+
{ name: 'keep', path: 'y' }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
], 가진사람);
|
|
91
|
+
expect(이름들(남은것[0].menus), '깊은 곳이 비면 그 부모도 함께 빠진다').to.deep.equal(['keep']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=menu-privilege.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-privilege.test.js","sourceRoot":"","sources":["../../test/menu-privilege.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing'\n\nimport { Menu } from '../src/types.js'\nimport { permittedMenus } from '../src/menu-privilege.js'\n\n/**\n * 쓸 수 있는 메뉴만 남기는 규칙.\n *\n * 이 판단이 앱마다 있었다. operato-plant 는 「소유자면 조직 묶음을 배열에 넣는다」였고,\n * operato-twin 은 항목마다 `superOnly` · `ownerOnly` 플래그였다. 같은 물음에 답이 둘이었고,\n * 둘 다 **서버의 문과 다른 낱말**이라 문이 열어 주는 사람에게 메뉴가 안 보이는 일이 났다.\n *\n * 규칙을 한 자리에 두고 여기서 못박는다.\n */\n\n/** 못 가진 요구 하나 — 이것이 걸리면 그 줄은 사라져야 한다. */\nconst 없는권한 = { category: '없는분류', privilege: 'query' }\nconst 있는권한 = { category: 'employee', privilege: 'query' }\n\n/** 시험용 판정 — 「employee/query 만 가졌다」. */\nconst 가진사람: (p: NonNullable<Menu['privilege']>) => boolean = p =>\n p.category === 있는권한.category && p.privilege === 있는권한.privilege\n\nconst 이름들 = (menus: Menu[]) => menus.map(menu => menu.name)\n\ndescribe('쓸 수 있는 메뉴만 남긴다', () => {\n it('요구가 없는 줄은 그대로 둔다', async () => {\n const 남은것 = await permittedMenus([{ name: 'plain', path: 'a' }], 가진사람)\n\n expect(이름들(남은것)).to.deep.equal(['plain'])\n })\n\n it('가진 요구면 남고, 못 가진 요구면 빠진다', async () => {\n const 남은것 = await permittedMenus(\n [\n { name: 'mine', path: 'a', privilege: 있는권한 },\n { name: 'theirs', path: 'b', privilege: 없는권한 }\n ],\n 가진사람\n )\n\n expect(이름들(남은것)).to.deep.equal(['mine'])\n })\n\n it('부모가 걸리면 자식까지 뺀다 — 묶음에 못 들어가면 그 안도 갈 수 없다', async () => {\n const 남은것 = await permittedMenus(\n [{ name: 'group', path: 'a', privilege: 없는권한, menus: [{ name: 'kid', path: 'a1' }] }],\n 가진사람\n )\n\n expect(남은것).to.deep.equal([])\n })\n\n it('자식이 다 빠진 부모도 뺀다 — 눌러도 아무 데도 못 간다', async () => {\n const 남은것 = await permittedMenus(\n [{ name: 'group', path: 'a', menus: [{ name: 'kid', path: 'a1', privilege: 없는권한 }] }],\n 가진사람\n )\n\n expect(남은것).to.deep.equal([])\n })\n\n it('자식이 하나라도 남으면 부모도 남는다 — 남은 것만 데리고', async () => {\n const 남은것 = await permittedMenus(\n [\n {\n name: 'group',\n path: 'a',\n menus: [\n { name: 'stay', path: 'a1' },\n { name: 'go', path: 'a2', privilege: 없는권한 }\n ]\n }\n ],\n 가진사람\n )\n\n expect(이름들(남은것)).to.deep.equal(['group'])\n expect(이름들(남은것[0].menus!)).to.deep.equal(['stay'])\n })\n\n it('원래 자식이 없던 잎은 남는다 — 「자식이 다 빠졌다」로 읽지 않는다', async () => {\n const 남은것 = await permittedMenus([{ name: 'leaf', path: 'a' }], 가진사람)\n\n expect(이름들(남은것), '이 갈래가 없으면 잎이 전부 사라진다').to.deep.equal(['leaf'])\n })\n\n it('원래 것을 고치지 않는다 — 다시 거를 때 앞의 결과가 섞이지 않게', async () => {\n const 원래: Menu[] = [\n {\n name: 'group',\n path: 'a',\n menus: [\n { name: 'stay', path: 'a1' },\n { name: 'go', path: 'a2', privilege: 없는권한 }\n ]\n }\n ]\n\n await permittedMenus(원래, 가진사람)\n\n expect(원래[0].menus, '입력을 고치면 로그인할 때마다 메뉴가 줄어든다').to.have.lengthOf(2)\n })\n\n it('판정이 늦게 와도 기다린다 — 프로필은 비동기로 온다', async () => {\n const 느린판정 = async (p: NonNullable<Menu['privilege']>) => {\n await new Promise(resolve => setTimeout(resolve, 5))\n return 가진사람(p)\n }\n\n const 남은것 = await permittedMenus(\n [\n { name: 'mine', path: 'a', privilege: 있는권한 },\n { name: 'theirs', path: 'b', privilege: 없는권한 }\n ],\n 느린판정\n )\n\n expect(이름들(남은것)).to.deep.equal(['mine'])\n })\n\n it('여러 층을 내려간다', async () => {\n const 남은것 = await permittedMenus(\n [\n {\n name: 'top',\n menus: [\n { name: 'mid', menus: [{ name: 'deep', path: 'x', privilege: 없는권한 }] },\n { name: 'keep', path: 'y' }\n ]\n }\n ],\n 가진사람\n )\n\n expect(이름들(남은것[0].menus!), '깊은 곳이 비면 그 부모도 함께 빠진다').to.deep.equal(['keep'])\n })\n})\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../src/ox-menu-portrait.js';
|