diff --git a/app/actions/definitions/navigation.tsx b/app/actions/definitions/navigation.tsx index 0334862db..41f71c0f2 100644 --- a/app/actions/definitions/navigation.tsx +++ b/app/actions/definitions/navigation.tsx @@ -174,7 +174,6 @@ export const rootNavigationActions = [ navigateToTemplates, navigateToArchive, navigateToTrash, - navigateToSettings, openAPIDocumentation, openFeedbackUrl, openBugReportUrl, diff --git a/app/components/CommandBar.tsx b/app/components/CommandBar.tsx index cfb8fb840..809f1ecbd 100644 --- a/app/components/CommandBar.tsx +++ b/app/components/CommandBar.tsx @@ -10,6 +10,7 @@ import CommandBarResults from "~/components/CommandBarResults"; import SearchActions from "~/components/SearchActions"; import rootActions from "~/actions/root"; import useCommandBarActions from "~/hooks/useCommandBarActions"; +import useSettingsActions from "~/hooks/useSettingsAction"; import useStores from "~/hooks/useStores"; import { CommandBarAction } from "~/types"; import { metaDisplay } from "~/utils/keyboard"; @@ -18,8 +19,13 @@ import Text from "./Text"; function CommandBar() { const { t } = useTranslation(); const { ui } = useStores(); + const settingsActions = useSettingsActions(); + const commandBarActions = React.useMemo( + () => [...rootActions, settingsActions], + [settingsActions] + ); - useCommandBarActions(rootActions); + useCommandBarActions(commandBarActions); const { rootAction } = useKBar((state) => ({ rootAction: state.currentRootActionId diff --git a/app/hooks/useAuthorizedSettingsConfig.ts b/app/hooks/useAuthorizedSettingsConfig.ts new file mode 100644 index 000000000..fe96f5648 --- /dev/null +++ b/app/hooks/useAuthorizedSettingsConfig.ts @@ -0,0 +1,196 @@ +import { + NewDocumentIcon, + EmailIcon, + ProfileIcon, + PadlockIcon, + CodeIcon, + UserIcon, + GroupIcon, + LinkIcon, + TeamIcon, + BeakerIcon, + DownloadIcon, +} from "outline-icons"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import Details from "~/scenes/Settings/Details"; +import Export from "~/scenes/Settings/Export"; +import Features from "~/scenes/Settings/Features"; +import Groups from "~/scenes/Settings/Groups"; +import Import from "~/scenes/Settings/Import"; +import Members from "~/scenes/Settings/Members"; +import Notifications from "~/scenes/Settings/Notifications"; +import Profile from "~/scenes/Settings/Profile"; +import Security from "~/scenes/Settings/Security"; +import Shares from "~/scenes/Settings/Shares"; +import Slack from "~/scenes/Settings/Slack"; +import Tokens from "~/scenes/Settings/Tokens"; +import Zapier from "~/scenes/Settings/Zapier"; +import SlackIcon from "~/components/SlackIcon"; +import ZapierIcon from "~/components/ZapierIcon"; +import env from "~/env"; +import useCurrentTeam from "./useCurrentTeam"; +import usePolicy from "./usePolicy"; + +type SettingsGroups = "Account" | "Team" | "Integrations"; +type SettingsPage = + | "Profile" + | "Notifications" + | "Api" + | "Details" + | "Security" + | "Features" + | "Members" + | "Groups" + | "Shares" + | "Import" + | "Export" + | "Slack" + | "Zapier"; + +export type ConfigItem = { + name: string; + path: string; + icon: React.FC; + component: () => JSX.Element; + enabled: boolean; + group: SettingsGroups; +}; + +type ConfigType = { + [key in SettingsPage]: ConfigItem; +}; + +const isHosted = env.DEPLOYMENT === "hosted"; + +const useAuthorizedSettingsConfig = () => { + const team = useCurrentTeam(); + const can = usePolicy(team.id); + const { t } = useTranslation(); + + const config: ConfigType = React.useMemo( + () => ({ + Profile: { + name: t("Profile"), + path: "/settings", + component: Profile, + enabled: true, + group: t("Account"), + icon: ProfileIcon, + }, + Notifications: { + name: t("Notifications"), + path: "/settings/notifications", + component: Notifications, + enabled: true, + group: t("Account"), + icon: EmailIcon, + }, + Api: { + name: t("API Tokens"), + path: "/settings/tokens", + component: Tokens, + enabled: can.createApiKey, + group: t("Account"), + icon: CodeIcon, + }, + // Team group + Details: { + name: t("Details"), + path: "/settings/details", + component: Details, + enabled: can.update, + group: t("Team"), + icon: TeamIcon, + }, + Security: { + name: t("Security"), + path: "/settings/security", + component: Security, + enabled: can.update, + group: t("Team"), + icon: PadlockIcon, + }, + Features: { + name: t("Features"), + path: "/settings/features", + component: Features, + enabled: can.update, + group: t("Team"), + icon: BeakerIcon, + }, + Members: { + name: t("Members"), + path: "/settings/members", + component: Members, + enabled: true, + group: t("Team"), + icon: UserIcon, + }, + Groups: { + name: t("Groups"), + path: "/settings/groups", + component: Groups, + enabled: true, + group: t("Team"), + icon: GroupIcon, + }, + Shares: { + name: t("Share Links"), + path: "/settings/shares", + component: Shares, + enabled: true, + group: t("Team"), + icon: LinkIcon, + }, + Import: { + name: t("Import"), + path: "/settings/import", + component: Import, + enabled: can.manage, + group: t("Team"), + icon: NewDocumentIcon, + }, + Export: { + name: t("Export"), + path: "/settings/export", + component: Export, + enabled: can.export, + group: t("Team"), + icon: DownloadIcon, + }, + // Intergrations + Slack: { + name: "Slack", + path: "/settings/integrations/slack", + component: Slack, + enabled: can.update && (!!env.SLACK_KEY || isHosted), + group: t("Integrations"), + icon: SlackIcon, + }, + Zapier: { + name: "Zapier", + path: "/settings/integrations/zapier", + component: Zapier, + enabled: can.update && isHosted, + group: t("Integrations"), + icon: ZapierIcon, + }, + }), + [can.createApiKey, can.export, can.manage, can.update, t] + ); + + const enabledConfigs = React.useMemo( + () => + Object.keys(config).reduce( + (acc, key: SettingsPage) => + config[key].enabled ? [...acc, config[key]] : acc, + [] + ), + [config] + ); + + return enabledConfigs; +}; + +export default useAuthorizedSettingsConfig; diff --git a/app/hooks/useSettingsAction.tsx b/app/hooks/useSettingsAction.tsx new file mode 100644 index 000000000..28e64b54f --- /dev/null +++ b/app/hooks/useSettingsAction.tsx @@ -0,0 +1,38 @@ +import { SettingsIcon } from "outline-icons"; +import * as React from "react"; +import { createAction } from "~/actions"; +import { NavigationSection } from "~/actions/sections"; +import history from "~/utils/history"; +import useAuthorizedSettingsConfig from "./useAuthorizedSettingsConfig"; + +const useSettingsActions = () => { + const config = useAuthorizedSettingsConfig(); + const actions = React.useMemo(() => { + return config.map((item) => { + const Icon = item.icon; + return { + id: item.path, + name: item.name, + icon: , + section: NavigationSection, + perform: () => history.push(item.path), + }; + }); + }, [config]); + + const navigateToSettings = React.useMemo( + () => + createAction({ + name: ({ t }) => t("Settings"), + section: NavigationSection, + shortcut: ["g", "s"], + icon: , + children: () => actions, + }), + [actions] + ); + + return navigateToSettings; +}; + +export default useSettingsActions; diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx index 7319c7b72..4efa52b22 100644 --- a/app/routes/settings.tsx +++ b/app/routes/settings.tsx @@ -1,42 +1,21 @@ import * as React from "react"; import { Switch, Redirect } from "react-router-dom"; -import Details from "~/scenes/Settings/Details"; -import Export from "~/scenes/Settings/Export"; -import Features from "~/scenes/Settings/Features"; -import Groups from "~/scenes/Settings/Groups"; -import Import from "~/scenes/Settings/Import"; -import Members from "~/scenes/Settings/Members"; -import Notifications from "~/scenes/Settings/Notifications"; -import Profile from "~/scenes/Settings/Profile"; -import Security from "~/scenes/Settings/Security"; -import Shares from "~/scenes/Settings/Shares"; -import Slack from "~/scenes/Settings/Slack"; -import Tokens from "~/scenes/Settings/Tokens"; -import Zapier from "~/scenes/Settings/Zapier"; import Route from "~/components/ProfiledRoute"; -import env from "~/env"; - -const isHosted = env.DEPLOYMENT === "hosted"; +import useAuthorizedSettingsConfig from "~/hooks/useAuthorizedSettingsConfig"; export default function SettingsRoutes() { + const configs = useAuthorizedSettingsConfig(); + return ( - - - - - - - - - - - {isHosted && ( - - )} - - - + {configs.map((config) => ( + + ))} {/* old routes */} diff --git a/app/scenes/Settings/Zapier.tsx b/app/scenes/Settings/Zapier.tsx index 4b37ceb5e..afc51aa00 100644 --- a/app/scenes/Settings/Zapier.tsx +++ b/app/scenes/Settings/Zapier.tsx @@ -9,8 +9,8 @@ import ZapierIcon from "~/components/ZapierIcon"; function Zapier() { const { t } = useTranslation(); return ( - }> - {t("Zapier")} + }> + Zapier Zapier is a platform that allows Outline to easily integrate with diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index a036cc1ba..2f0abd40c 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -622,7 +622,6 @@ "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the developer documentation.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the developer documentation.", "Tokens": "Tokens", "Create a token": "Create a token", - "Zapier": "Zapier", "Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'": "Zapier is a platform that allows Outline to easily integrate with thousands of other business tools. Head over to Zapier to setup a \"Zap\" and start programmatically interacting with Outline.'", "Open Zapier": "Open Zapier", "There are no templates just yet.": "There are no templates just yet.",