import { getServiceAppConfig, METADATA_TYPE, refreshApp } from "."; import _ = require("lodash"); import { defaultsDeep } from "@steedos/utils"; import { translationApp, translationObjectLabel, translationTabLabel, } from "@steedos/i18n"; import { getAssignedApps, getObject as _getObject, absoluteUrl, } from "@steedos/objectql"; import { getNotLicensedTabNames } from "./getNotLicensedTabNames"; function cacherKey(appApiName: string): string { return `$steedos.#${METADATA_TYPE}.${appApiName}`; } async function registerApp(ctx, appApiName, data, meta) { return await ctx.broker.call( "metadata.add", { key: cacherKey(appApiName), data: data }, { meta: meta }, ); } async function getSpaceApp(ctx: any, appApiName: string) { const allApps = await getAllApps(ctx); const userSession = ctx.meta.user; const spaceId = userSession.spaceId; const userApps = _.filter(allApps, function (metadataConfig) { const config = metadataConfig.metadata; //只判断是否启用 if (!config.visible) { return false; } if (_.has(config, "space") && config.space) { return config.space === spaceId; } if ( !_.isEmpty(config.tabs) || !_.isEmpty(config.objects) || !_.isEmpty(config.mobile_objects) ) { return true; } return true; }); return _.find(userApps, function (metadataConfig) { const config = metadataConfig.metadata; return config.space && config.code === appApiName; }); } async function get(ctx: any) { const spaceAppMetadataConfig = await getSpaceApp(ctx, ctx.params.appApiName); if (spaceAppMetadataConfig) { return spaceAppMetadataConfig; } else { const metadataConfig = await ctx.broker.call( "metadata.get", { key: cacherKey(ctx.params.appApiName) }, { meta: ctx.meta }, ); if (metadataConfig) { return metadataConfig; } else { const allApps = await getAllApps(ctx); const userSession = ctx.meta.user; const spaceId = userSession.spaceId; const userApps = _.filter(allApps, function (metadataConfig) { const config = metadataConfig.metadata; //只判断是否启用 if (!config.visible) { return false; } if (_.has(config, "space") && config.space) { return config.space === spaceId; } if ( !_.isEmpty(config.tabs) || !_.isEmpty(config.objects) || !_.isEmpty(config.mobile_objects) ) { return true; } return true; }); if (ctx.params.appApiName === "-") { return _.first(_.sortBy(userApps, ["metadata.sort"])); } return _.find(userApps, function (metadataConfig) { const app = metadataConfig.metadata; return app.code === ctx.params.appApiName; }); } } } async function getAllApps(ctx: any) { return await ctx.broker.call( "metadata.filter", { key: cacherKey("*") }, { meta: ctx.meta }, ); } // async function getObject(ctx: any, objectApiName: string){ // return await ctx.broker.call('objects.get', {objectApiName}) // } async function getAllTabs(ctx: any) { return await ctx.broker.call("tabs.getAll"); } async function getContext(ctx: any) { const userSession = ctx.meta.user; const allTabs = await getAllTabs(ctx); if (userSession.is_space_admin) { return { tabs: allTabs, hiddenTabNames: [], }; } // const allObject = await getSteedosSchema().getAllObject() let hiddenTabNames = await getHiddenTabNames(ctx, allTabs); const notLicensedTabNames = await getNotLicensedTabNames(ctx, allTabs); hiddenTabNames = hiddenTabNames.concat(notLicensedTabNames); return { tabs: allTabs, // objects: allObject, hiddenTabNames: hiddenTabNames, }; } async function getTab(ctx: any, tabApiName: string) { const metadataConfig = await ctx.broker.call("tabs.get", { tabApiName }); return metadataConfig?.metadata; } // async function getChildren(ctx: any, tabApiName: string){ // return await ctx.broker.call('tabs.getChildren', {tabApiName}); // } function getTabChildren(context: any, tabApiName: string) { if (context) { const { tabs } = context; return _.filter(tabs, function (tab) { return tab?.metadata.parent === tabApiName; }); } } /** * 判断tab是否符合mobile配置 * @param tab * @param mobile 是否是在移动设备上显示tab * @returns */ function checkTabMobile(tab, mobile) { let isChecked = false; if (mobile === true || mobile === "true") { isChecked = tab.mobile !== false; } else { isChecked = tab.desktop !== false; } return isChecked; } function checkAppMobile(app, mobile) { let isChecked = false; // 手机端访问 if (mobile === true || mobile === "true") { isChecked = app.mobile === true; } else { // 桌面端访问 isChecked = app.is_creator === true; } return isChecked; } /** * 根据选项卡权限的配置决定选项卡是否可见,默认可见,关闭的和隐藏的选项卡不可见 * @param ctx * @return ['tabName', ...] */ async function getHiddenTabNames(ctx, allTabs) { const userSession = ctx.meta.user; if (!userSession) { throw new Error("no permission."); } const hiddenTabNames = []; const permissionTabs = await getPermissionTabs(ctx, userSession); const showTabNames = []; // 同一个选项卡在不同权限集中的权限叠加,如有一个是默认打开的则选项卡默认打开 for (const permissionTab of permissionTabs) { if (permissionTab.permission === "on") { showTabNames.push(permissionTab.tab); } } for (const permissionTab of permissionTabs) { if ( (permissionTab.permission === "off" || permissionTab.permission === "hidden") && !showTabNames.includes(permissionTab.tab) ) { hiddenTabNames.push(permissionTab.tab); } } // .tab.yml中配置了hidden:true for (const config of allTabs) { if (config.metadata && config.metadata.hidden) { hiddenTabNames.push(config.metadata.name); } } return hiddenTabNames; } async function getPermissionTabs(ctx, userSession): Promise { const { roles, spaceId } = userSession; const permissionTabs = []; for (const role of roles) { const pattern = `${role}_*`; const filterResult = await ctx.broker.call( "permission_tabs.filter", { pattern: pattern, }, { user: { spaceId: spaceId }, }, ); for (const ptConfig of filterResult) { if ( ptConfig.metadata.permission_set === role && (!_.has(ptConfig.metadata, "space") || ptConfig.metadata.space === spaceId) ) { permissionTabs.push(ptConfig.metadata); } } } return permissionTabs; } async function tabMenus( ctx: any, appPath, tabApiName, menu, mobile, userSession, context, props: any = {}, ) { try { // const objectsConfigs = context.objects; const tab = await getTab(ctx, tabApiName); if (props.group) { props.group = _.find(menu.tab_groups, { id: props.group })?.group_name || props.group; } if (tabApiName) { props.tabApiName = tabApiName; } if (tab) { const isMobileChecked = checkTabMobile(tab, mobile); if (!isMobileChecked) { return; } const tabChildren = getTabChildren(context, tabApiName); if (tabChildren && tabChildren.length > 0) { const tabMenu = { id: tab.name, icon: tab.icon, name: `${tab.label}`, children: [], ...props, }; for (const { metadata: tabChild } of tabChildren) { if (tabChild && tabChild.apiName) { await tabMenus( ctx, appPath, tabChild.apiName, tabMenu, mobile, userSession, context, ); } } menu.children.push(tabMenu); } else { if (tab.type === "object") { const allowRead = await objectAllowRead(tab.object, userSession); if (!allowRead) { return; } // const objectMetadata = await getObject(ctx, tab.object); // const objectMetadata = _.find(objectsConfigs, (config) => { // return config && config.metadata.name === tab.object // }); const objectConfig = await _getObject(tab.object).getConfig(); if (objectConfig) { // const objectConfig = objectMetadata.metadata; const objectLabel = translationObjectLabel( userSession.language, objectConfig.name, objectConfig.label || objectConfig.name, ); menu.children.push({ id: objectConfig.name, type: tab.type, icon: objectConfig.icon, path: `${appPath}/${objectConfig.name}`, name: `${objectLabel}`, ...props, }); } } if (tab.type === "url") { tab.label = translationTabLabel( userSession.language, tab.name, tab.label || tab.name, ); let urlMenu: any = { id: `${tab.name}`, type: tab.type, icon: tab.icon, path: `${tab.url}`, name: `${tab.label}`, ...props, }; if (tab.is_new_window) { urlMenu.target = "_blank"; } else if (tab.is_use_iframe) { urlMenu.is_use_iframe = true; urlMenu.path = `${appPath}/tab_iframe/${tab.name}/?url=${encodeURIComponent(tab.url)}`; } menu.children.push(urlMenu); } if (tab.type === "page") { tab.label = translationTabLabel( userSession.language, tab.name, tab.label || tab.name, ); menu.children.push({ id: `${tab.name}`, icon: tab.icon, type: tab.type, page: tab.page, path: `${appPath}/${tab.type}/${tab.page}`, name: `${tab.label}`, ...props, }); } if (tab.type === "analytics_dashboard") { const url = `/analytics/embed/dashboard/${tab.analytics_dashboard}?titled=false&bordered=false`; tab.label = translationTabLabel( userSession.language, tab.name, tab.label || tab.name, ); let urlMenu: any = { id: `${tab.name}`, type: tab.type, icon: tab.icon, path: `${url}`, name: `${tab.label}`, ...props, }; if (tab.is_new_window) { urlMenu.target = "_blank"; } else { urlMenu.is_use_iframe = true; urlMenu.path = `${appPath}/tab_iframe/${tab.name}/?url=${url}`; } menu.children.push(urlMenu); } } } } catch (error) { ctx.broker.logger.info(error.message); } } async function objectAllowRead(objectApiName: string, userSession) { return await _getObject(objectApiName).allowRead(userSession); } async function transformAppToMenus(ctx, app, mobile, userSession, context) { if (!app.code && !app._id) { return; } const isAppShow = checkAppMobile(app, mobile); if (!isAppShow) { return; } if (!app.code) { app.code = app._id; } translationApp(userSession.language, app.code, app); var appPath = `/app/${app.code}`; if (app.url) { if (/^http(s?):\/\//.test(app.url)) { if (app.secret) { appPath = absoluteUrl("/api/external/app/" + (app._id || app.code)); } else { appPath = app.url; } } else { appPath = app.url; } } const menu: any = { id: app.code, path: appPath, name: `${app.label || app.name}`, icon: app.icon_slds, color: app.color, dark: app.dark, showSidebar: app.showSidebar, description: app.description, children: [], blank: app.is_new_window, on_click: app.on_click, isExternalUrl: !!app.url, tab_groups: app.tab_groups, is_hide_mobile_menu: app.is_hide_mobile_menu, visible_on: app.visible_on || "${true}", default_tab: app.default_tab, }; if ( app.enable_nav_schema && app.nav_schema && (!mobile || mobile === "false") ) { menu.nav_schema = _.isString(app.nav_schema) ? JSON.parse(app.nav_schema) : app.nav_schema; } const hiddenTabNames = context.hiddenTabNames || []; if (app.tab_items) { // app.tab_items is array if (_.isArray(app.tab_items)) { for (const item of app.tab_items) { try { if (hiddenTabNames.includes(item.tab_name)) continue; await tabMenus( ctx, appPath, item.tab_name, menu, mobile, userSession, context, item, ); } catch (error) { ctx.broker.logger.info(error.message); } } } else { for (const tabApiName in app.tab_items) { try { if (hiddenTabNames.includes(tabApiName)) continue; const props = app.tab_items[tabApiName]; await tabMenus( ctx, appPath, tabApiName, menu, mobile, userSession, context, props, ); } catch (error) { ctx.broker.logger.info(error.message); } } } } else if (_.isArray(app.tabs)) { for (const tabApiName of app.tabs) { try { if (hiddenTabNames.includes(tabApiName)) continue; await tabMenus( ctx, appPath, tabApiName, menu, mobile, userSession, context, ); } catch (error) { ctx.broker.logger.info(error.message); } } } const objects = mobile ? app.mobile_objects : app.objects; // const objectsConfigs = context.objects; if (_.isArray(objects)) { const getChildrenPromises = []; for (const objectApiName of objects) { getChildrenPromises.push( getMenuChildren({ objectApiName, userSession, ctx, appPath, app, }), ); } const children = await Promise.all(getChildrenPromises); for (const child of children) { if (child) { menu.children.push(child); } } } if (menu.default_tab && _.isString(menu.default_tab)) { const defaultTabStr = menu.default_tab; let defaultTab = _.find(menu.children, (item) => { return item.id === defaultTabStr || item.tabApiName === defaultTabStr; }); if (!defaultTab && context && context.tabs) { const tabConfig = _.find( context.tabs, (t) => t.metadata && t.metadata.name === defaultTabStr, ); if (tabConfig && tabConfig.metadata) { const tab = tabConfig.metadata; // 尝试从 menu.children 中查找(如果已存在) if (tab.type === "object" && tab.object) { defaultTab = _.find(menu.children, (item) => item.id === tab.object); } else { defaultTab = _.find(menu.children, (item) => item.id === tab.name); } // 如果找不到,则手动构造一个轻量级的 defaultTab 对象用于路由跳转 if (!defaultTab) { try { if (tab.type === "object") { // 为了性能,不再计算label,也不校验权限,仅为了路由跳转 // const objectConfig = await _getObject(tab.object).getConfig(); defaultTab = { id: tab.object, type: tab.type, icon: tab.icon, path: `${appPath}/${tab.object}`, name: tab.label || tab.name, tabApiName: tab.name, }; } else if (tab.type === "url") { defaultTab = { id: tab.name, type: tab.type, icon: tab.icon, path: tab.url, name: tab.label || tab.name, tabApiName: tab.name, }; if (tab.is_new_window) { defaultTab.target = "_blank"; } else if (tab.is_use_iframe) { defaultTab.is_use_iframe = true; defaultTab.path = `${appPath}/tab_iframe/${tab.name}/?url=${encodeURIComponent(tab.url)}`; } } else if (tab.type === "page") { defaultTab = { id: tab.name, icon: tab.icon, type: tab.type, page: tab.page, path: `${appPath}/${tab.type}/${tab.page}`, name: tab.label || tab.name, tabApiName: tab.name, }; } else if (tab.type === "analytics_dashboard") { const url = `/analytics/embed/dashboard/${tab.analytics_dashboard}?titled=false&bordered=false`; defaultTab = { id: tab.name, type: tab.type, icon: tab.icon, path: url, name: tab.label || tab.name, tabApiName: tab.name, }; if (tab.is_new_window) { defaultTab.target = "_blank"; } else { defaultTab.is_use_iframe = true; defaultTab.path = `${appPath}/tab_iframe/${tab.name}/?url=${url}`; } } } catch (e) { console.error(e); } } } } if (defaultTab) { menu.default_tab = defaultTab; } } return menu; } async function getMenuChildren({ objectApiName, userSession, ctx, appPath, app, }) { try { const objectConfig = await _getObject(objectApiName).getConfig(); if (!objectConfig) { ctx.broker.logger.error( `${objectApiName} is not found in the objects of app ${app.code} `, ); return; } const allowRead = await objectAllowRead(objectApiName, userSession); if (!allowRead) { return; } if (objectConfig) { // const objectConfig = objectMetadata.metadata; const objectLabel = translationObjectLabel( userSession.language, objectConfig.name, objectConfig.label || objectConfig.name, ); return { id: objectConfig.name, icon: objectConfig.icon, path: `${appPath}/${objectConfig.name}`, name: `${objectLabel}`, }; } } catch (error) { ctx.broker.logger.error(error); } } async function getAppsMenus(ctx) { const userSession = ctx.meta.user; if (!userSession) { throw new Error("no permission."); } let assigned_apps = await getAssignedApps(userSession); let mobile = ctx.params.mobile; if (typeof mobile !== "boolean") { mobile = mobile === "true" ? true : false; } const spaceId = userSession.spaceId; const metadataApps = await getAllApps(ctx); const context = await getContext(ctx); const allApps = _.map(metadataApps, "metadata"); if (assigned_apps && assigned_apps.length) { assigned_apps = _.filter(allApps, (item) => { return assigned_apps.includes(item.code); }); } else { assigned_apps = allApps; } const _userApps = _.filter(assigned_apps, function (config) { if (!config.visible) { return false; } if (config._id === config.code) { let dbApp = _.find(assigned_apps, (item) => { return ( item.code === config.code && item._id != item.code && item.space === spaceId ); }); if (dbApp) { return dbApp.visible; } } if (_.has(config, "space") && config.space) { return config.space === spaceId; } return true; }); const menus = []; let userApps = []; _.each(_.sortBy(_userApps, ["sort"]), function (app) { if (!app.code) { app.code = app._id; } const _appIndex = _.findIndex(userApps, function (item) { return item.code === app.code; }); if (_appIndex < 0) { userApps.push(app); } else { const _app = userApps[_appIndex]; if (!_app.space && app.space) { userApps[_appIndex] = app; } } }); for (const app of _.sortBy(userApps, ["sort"])) { const menu = await transformAppToMenus( ctx, app, mobile, userSession, context, ); if (menu) { menus.push(menu); } } // if (!mobile) { // const setupApp = { // code: 'admin', // name: '设置', // icon_slds: 'settings', // description: '管理员设置公司、人员、权限等。', // children: [], // mobile: true, // is_creator: true, // } // const menu = await transformAppToMenus(ctx, setupApp, mobile, userSession, context); // menus.push(menu); // } return menus; } async function getAppMenus(ctx) { const userSession = ctx.meta.user; const { mobile } = ctx.params; if (!userSession) { throw new Error("no permission."); } const spaceId = userSession.spaceId; const metadataConf = await get(ctx); if (metadataConf) { const appConfig = metadataConf.metadata; if ( _.has(appConfig, "space") && appConfig.space && appConfig.space != spaceId ) { return; } const context = await getContext(ctx); const appMenus = await transformAppToMenus( ctx, appConfig, mobile, userSession, context, ); if ( userSession.is_space_admin && appConfig._id && appConfig.code && appConfig._id != appConfig.code ) { appMenus.allowEditApp = true; } return appMenus; } } export const ActionHandlers = { async get(ctx: any): Promise { return await ctx.broker.call( "metadata.get", { key: cacherKey(ctx.params.appApiName) }, { meta: ctx.meta }, ); }, async getAll(ctx: any): Promise { return await getAllApps(ctx); }, async getMenus(ctx: any): Promise { const menus = await getAppsMenus(ctx); return menus; }, async getAppMenus(ctx: any): Promise { return await getAppMenus(ctx); }, async add(ctx: any): Promise { let config = ctx.params.data; const serviceName = ctx.meta.metadataServiceName; const metadataApiName = ctx.params.appApiName; const metadataConfig = await getServiceAppConfig( ctx, serviceName, metadataApiName, ); if (metadataConfig && metadataConfig.metadata) { config = defaultsDeep(config, metadataConfig.metadata); } await ctx.broker.call( "metadata.addServiceMetadata", { key: cacherKey(metadataApiName), data: config }, { meta: Object.assign({}, ctx.meta, { metadataType: METADATA_TYPE, metadataApiName: metadataApiName, }), }, ); const appConfig = await refreshApp(ctx, metadataApiName); return await registerApp(ctx, metadataApiName, appConfig, ctx.meta); }, async delete(ctx: any): Promise { return await ctx.broker.call( "metadata.delete", { key: cacherKey(ctx.params.appApiName) }, { meta: ctx.meta }, ); }, async verify(ctx: any): Promise { console.log("verify"); return true; }, async refresh(ctx) { const { isClear, metadataApiNames } = ctx.params; if (isClear) { for (const metadataApiName of metadataApiNames) { const appConfig = await refreshApp(ctx, metadataApiName); if (!appConfig) { await ctx.broker.call("metadata.delete", { key: cacherKey(metadataApiName), }); } else { await registerApp(ctx, metadataApiName, appConfig, {}); } } } }, };