Explorar o código

Merge branch 'dev/7.0.x' into imprv/133905-replace-icons-material-symbols-outlined

Meiri Kikuta %!s(int64=2) %!d(string=hai) anos
pai
achega
1cbcfa87de
Modificáronse 29 ficheiros con 314 adicións e 277 borrados
  1. 20 11
      apps/app/src/client/services/layout.ts
  2. 49 0
      apps/app/src/client/services/use-on-template-button-clicked.ts
  3. 18 0
      apps/app/src/components/Common/PageViewLayout.module.scss
  4. 7 2
      apps/app/src/components/Common/PageViewLayout.tsx
  5. 13 3
      apps/app/src/components/CreateTemplateModal.tsx
  6. 6 6
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  7. 2 47
      apps/app/src/components/Navbar/hooks.tsx
  8. 4 0
      apps/app/src/components/Page/PageView.tsx
  9. 9 0
      apps/app/src/components/PageControls/PageControls.tsx
  10. 5 1
      apps/app/src/components/PageEditor/PageEditor.tsx
  11. 9 20
      apps/app/src/components/PageEditor/Preview.module.scss
  12. 6 2
      apps/app/src/components/PageEditor/Preview.tsx
  13. 8 0
      apps/app/src/components/SearchPage/SearchResultContent.module.scss
  14. 13 13
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  15. 4 0
      apps/app/src/components/ShareLinkPageView.tsx
  16. 2 1
      apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss
  17. 6 6
      apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx
  18. 21 145
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  19. 95 0
      apps/app/src/components/Sidebar/PageCreateButton/hooks.tsx
  20. 1 1
      apps/app/src/components/Sidebar/Tag.tsx
  21. 3 5
      apps/app/src/pages/[[...path]].page.tsx
  22. 1 1
      apps/app/src/pages/me/[[...path]].page.tsx
  23. 2 5
      apps/app/src/pages/share/[[...path]].page.tsx
  24. 1 1
      apps/app/src/pages/tags.page.tsx
  25. 1 1
      apps/app/src/pages/trash.page.tsx
  26. 0 5
      apps/app/src/styles/_layout.scss
  27. 1 0
      apps/app/src/styles/_mixins.scss
  28. 4 0
      apps/app/src/styles/mixins/_fluid-layout.scss
  29. 3 1
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

+ 20 - 11
apps/app/src/client/services/layout.ts

@@ -9,20 +9,29 @@ export const useEditorModeClassName = (): string => {
   return `${getClassNamesByEditorMode().join(' ') ?? ''}`;
 };
 
-export const useLayoutFluidClassName = (expandContentWidth?: boolean | null): string => {
+const useDetermineExpandContent = (expandContentWidth?: boolean | null): boolean => {
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 
   const isContainerFluidDefault = dataIsContainerFluid;
-  const isContainerFluid = expandContentWidth ?? isContainerFluidDefault;
-
-  return isContainerFluid ? 'growi-layout-fluid' : '';
+  return expandContentWidth ?? isContainerFluidDefault ?? false;
 };
 
-export const useLayoutFluidClassNameByPage = (initialPage?: IPage): string => {
-  const page = initialPage;
-  const expandContentWidth = page == null || !('expandContentWidth' in page)
-    ? null
-    : page.expandContentWidth;
-
-  return useLayoutFluidClassName(expandContentWidth);
+export const useShouldExpandContent = (data?: IPage | boolean | null): boolean => {
+  const expandContentWidth = (() => {
+    // when data is null
+    if (data == null) {
+      return null;
+    }
+    // when data is boolean
+    if (data === true || data === false) {
+      return data;
+    }
+    // when IPage does not have expandContentWidth
+    if (!('expandContentWidth' in data)) {
+      return null;
+    }
+    return data.expandContentWidth;
+  })();
+
+  return useDetermineExpandContent(expandContentWidth);
 };

+ 49 - 0
apps/app/src/client/services/use-on-template-button-clicked.ts

@@ -0,0 +1,49 @@
+import { useCallback, useState } from 'react';
+
+import { useRouter } from 'next/router';
+
+import { createPage, exist } from '~/client/services/page-operation';
+import { LabelType } from '~/interfaces/template';
+
+export const useOnTemplateButtonClicked = (
+    currentPagePath?: string,
+): {
+  onClickHandler: (label: LabelType) => Promise<void>,
+  isPageCreating: boolean
+} => {
+  const router = useRouter();
+  const [isPageCreating, setIsPageCreating] = useState(false);
+
+  const onClickHandler = useCallback(async(label: LabelType) => {
+    try {
+      setIsPageCreating(true);
+
+      const path = currentPagePath == null || currentPagePath === '/'
+        ? `/${label}`
+        : `${currentPagePath}/${label}`;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: 4,
+      // grant: currentPage?.grant || 1,
+      // grantUserGroupId: currentPage?.grantedGroup?._id,
+      };
+
+      const res = await exist(JSON.stringify([path]));
+      if (!res.pages[path]) {
+        await createPage(path, '', params);
+      }
+
+      router.push(`${path}#edit`);
+    }
+    catch (err) {
+      throw err;
+    }
+    finally {
+      setIsPageCreating(false);
+    }
+  }, [currentPagePath, router]);
+
+  return { onClickHandler, isPageCreating };
+};

+ 18 - 0
apps/app/src/components/Common/PageViewLayout.module.scss

@@ -1,5 +1,6 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '~/styles/mixins';
 @use '~/styles/variables' as var;
 
 
@@ -18,6 +19,23 @@ $page-view-layout-margin-top: 32px;
   }
 }
 
+// container padding
+.page-view-layout :global,
+.footer-layout :global {
+  @include bs.media-breakpoint-up(lg) {
+    .container-lg {
+      --bs-gutter-x: 3rem;
+    }
+  }
+}
+
+// fluid layout
+.fluid-layout :global {
+  .grw-container-convertible {
+    @include mixins.fluid-layout();
+  }
+}
+
 .page-view-layout :global {
   .grw-side-contents-container {
     margin-bottom: 1rem;

+ 7 - 2
apps/app/src/components/Common/PageViewLayout.tsx

@@ -4,22 +4,27 @@ import styles from './PageViewLayout.module.scss';
 
 const pageViewLayoutClass = styles['page-view-layout'] ?? '';
 const footerLayoutClass = styles['footer-layout'] ?? '';
+const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 
 type Props = {
   children?: ReactNode,
   headerContents?: ReactNode,
   sideContents?: ReactNode,
   footerContents?: ReactNode,
+  expandContentWidth?: boolean,
 }
 
 export const PageViewLayout = (props: Props): JSX.Element => {
   const {
     children, headerContents, sideContents, footerContents,
+    expandContentWidth,
   } = props;
 
+  const fluidLayoutClass = expandContentWidth ? _fluidLayoutClass : '';
+
   return (
     <>
-      <div id="main" className={`main ${pageViewLayoutClass} flex-expand-vert`}>
+      <div id="main" className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert`}>
         <div id="content-main" className="content-main container-lg grw-container-convertible flex-expand-vert">
           { headerContents != null && headerContents }
           { sideContents != null
@@ -43,7 +48,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
       </div>
 
       { footerContents != null && (
-        <footer className={`footer d-edit-none ${footerLayoutClass}`}>
+        <footer className={`footer d-edit-none ${footerLayoutClass} ${fluidLayoutClass}`}>
           {footerContents}
         </footer>
       ) }

+ 13 - 3
apps/app/src/components/CreateTemplateModal.tsx

@@ -1,12 +1,13 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
+import { useOnTemplateButtonClicked } from '~/client/services/use-on-template-button-clicked';
+import { toastError } from '~/client/util/toastr';
 import { TargetType, LabelType } from '~/interfaces/template';
 
-import { useOnTemplateButtonClicked } from './Navbar/hooks';
 
 type TemplateCardProps = {
   target: TargetType;
@@ -56,6 +57,15 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
 
   const { onClickHandler: onClickTemplateButton, isPageCreating } = useOnTemplateButtonClicked(path);
 
+  const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
+    try {
+      await onClickTemplateButton(label);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [onClickTemplateButton]);
+
   const parentPath = pathUtils.addTrailingSlash(path);
 
   const renderTemplateCard = (target: TargetType, label: LabelType) => (
@@ -64,7 +74,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
         target={target}
         label={label}
         isPageCreating={isPageCreating}
-        onClickHandler={() => onClickTemplateButton(label)}
+        onClickHandler={() => onClickTemplateButtonHandler(label)}
       />
     </div>
   );

+ 6 - 6
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -11,11 +11,12 @@ import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 
+import { useShouldExpandContent } from '~/client/services/layout';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId, useIsContainerFluid,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores/context';
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal,
@@ -28,7 +29,6 @@ import { mutatePageTree } from '~/stores/page-listing';
 import {
   useEditorMode, useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
-  useSelectedGrant,
 } from '~/stores/ui';
 
 import { CreateTemplateModal } from '../CreateTemplateModal';
@@ -196,8 +196,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
-  const { data: isContainerFluid } = useIsContainerFluid();
-  const { data: grantData } = useSelectedGrant();
+
+  const shouldExpandContent = useShouldExpandContent(currentPage);
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
@@ -299,7 +299,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     <>
       <div
         className={`${styles['grw-contextual-sub-navigation']}
-          d-flex align-items-center justify-content-end px-2 py-1 gap-2 gap-md-4 d-print-none
+          d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
         `}
         data-testid="grw-contextual-sub-nav"
       >
@@ -309,7 +309,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             revisionId={revisionId}
             shareLinkId={shareLinkId}
             path={path ?? currentPathname} // If the page is empty, "path" is undefined
-            expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
+            expandContentWidth={shouldExpandContent}
             disableSeenUserInfoPopover={isSharedUser}
             showPageControlDropdown={isAbleToShowPageManagement}
             additionalMenuItemRenderer={additionalMenuItemsRenderer}

+ 2 - 47
apps/app/src/components/Navbar/hooks.tsx

@@ -1,11 +1,10 @@
-import { useCallback, useState } from 'react';
+import { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 
-import { createPage, exist } from '~/client/services/page-operation';
+import { createPage } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
-import { LabelType } from '~/interfaces/template';
 import { useIsNotFound } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
@@ -57,47 +56,3 @@ export const useOnPageEditorModeButtonClicked = (
     mutateEditorMode(editorMode);
   }, [isNotFound, mutateEditorMode, path, router, setIsCreating, t]);
 };
-
-export const useOnTemplateButtonClicked = (
-    currentPagePath: string,
-): {
-  onClickHandler: (label: LabelType) => Promise<void>,
-  isPageCreating: boolean
-} => {
-  const router = useRouter();
-  const [isPageCreating, setIsPageCreating] = useState(false);
-
-  const onClickHandler = useCallback(async(label: LabelType) => {
-    try {
-      setIsPageCreating(true);
-
-      const path = currentPagePath == null || currentPagePath === '/'
-        ? `/${label}`
-        : `${currentPagePath}/${label}`;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
-      };
-
-      const res = await exist(JSON.stringify([path]));
-      if (!res.pages[path]) {
-        await createPage(path, '', params);
-      }
-
-      router.push(`${path}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsPageCreating(false);
-    }
-  }, [currentPagePath, router]);
-
-  return { onClickHandler, isPageCreating };
-};

+ 4 - 0
apps/app/src/components/Page/PageView.tsx

@@ -6,6 +6,7 @@ import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import dynamic from 'next/dynamic';
 
+import { useShouldExpandContent } from '~/client/services/layout';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
@@ -70,6 +71,8 @@ export const PageView = (props: Props): JSX.Element => {
   const isNotFound = isNotFoundMeta || page?.revision == null;
   const isUsersHomepagePath = isUsersHomepage(pagePath);
 
+  const shouldExpandContent = useShouldExpandContent(page);
+
 
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
@@ -157,6 +160,7 @@ export const PageView = (props: Props): JSX.Element => {
       headerContents={headerContents}
       sideContents={sideContents}
       footerContents={footerContents}
+      expandContentWidth={shouldExpandContent}
     >
       <PageAlerts />
 

+ 9 - 0
apps/app/src/components/PageControls/PageControls.tsx

@@ -16,6 +16,7 @@ import { toastError } from '~/client/util/toastr';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
 import { EditorMode, useEditorMode } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
@@ -32,6 +33,9 @@ import SubscribeButton from './SubscribeButton';
 
 import styles from './PageControls.module.scss';
 
+const logger = loggerFactory('growi:components/PageControls');
+
+
 type TagsProps = {
   onClickEditTagsButton: () => void,
 }
@@ -199,6 +203,11 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 
   const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
     if (onClickSwitchContentWidth == null || (isGuestUser ?? true) || (isReadOnlyUser ?? true)) {
+      logger.warn('Could not switch content width', {
+        onClickSwitchContentWidth: onClickSwitchContentWidth == null ? 'null' : 'not null',
+        isGuestUser,
+        isReadOnlyUser,
+      });
       return;
     }
     if (!isIPageInfoForEntity(pageInfo)) {

+ 5 - 1
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -16,6 +16,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 
+import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
@@ -57,11 +58,11 @@ import loggerFactory from '~/utils/logger';
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 // import { ConflictDiffModal } from './ConflictDiffModal';
 // import Editor from './Editor';
+import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import scrollSyncHelper from './ScrollSyncHelper';
 
 import '@growi/editor/dist/style.css';
-import EditorNavbarBottom from './EditorNavbarBottom';
 
 
 const logger = loggerFactory('growi:PageEditor');
@@ -127,6 +128,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
 
+  const shouldExpandContent = useShouldExpandContent(currentPage);
+
   const saveOrUpdate = useSaveOrUpdate();
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
@@ -597,6 +600,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             rendererOptions={rendererOptions}
             markdown={markdownToPreview}
             pagePath={currentPagePath}
+            expandContentWidth={shouldExpandContent}
             // TODO: implement
             // refs: https://redmine.weseek.co.jp/issues/126519
             // onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}

+ 9 - 20
apps/app/src/components/PageEditor/Preview.module.scss

@@ -1,29 +1,18 @@
-@use '~/styles/variables' as var;
 @use '~/styles/mixins';
 
-@include mixins.editing(true) {
-  .page-editor-preview-body :global {
+.page-editor-preview-body :global {
+  .wiki {
+    max-width: 980px;
+    margin: 0 auto;
   }
 }
 
 // modify width for fluid layout
-@include mixins.editing(true) {
-  .dynamic-layout-root:not(.growi-layout-fluid) {
-    :local {
-      .page-editor-preview-body :global {
-        .wiki {
-          max-width: 980px;
-          margin: 0 auto;
-        }
-      }
-    }
-  }
-  .dynamic-layout-root.growi-layout-fluid {
-    :local {
-      .page-editor-preview-body :global {
-        .wiki {
-          margin: 0 auto;
-        }
+.page-editor-preview-body {
+  &:global {
+    &.fluid-layout {
+      .wiki {
+        @include mixins.fluid-layout();
       }
     }
   }

+ 6 - 2
apps/app/src/components/PageEditor/Preview.tsx

@@ -9,13 +9,14 @@ import RevisionRenderer from '../Page/RevisionRenderer';
 
 import styles from './Preview.module.scss';
 
-const moduleClass = styles['page-editor-preview-body'];
+const moduleClass = styles['page-editor-preview-body'] ?? '';
 
 
 type Props = {
   rendererOptions: RendererOptions,
   markdown?: string,
   pagePath?: string | null,
+  expandContentWidth?: boolean,
   onScroll?: (scrollTop: number) => void,
 }
 
@@ -24,11 +25,14 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
   const {
     rendererOptions,
     markdown, pagePath,
+    expandContentWidth,
   } = props;
 
+  const fluidLayoutClass = expandContentWidth ? 'fluid-layout' : '';
+
   return (
     <div
-      className={`${moduleClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
+      className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       ref={ref}
       onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
         if (props.onScroll != null) {

+ 8 - 0
apps/app/src/components/SearchPage/SearchResultContent.module.scss

@@ -1,2 +1,10 @@
+@use '~/styles/mixins';
+
 .search-result-content :global {
 }
+
+.fluid-layout :global {
+  .grw-container-convertible {
+    @include mixins.fluid-layout();
+  }
+}

+ 13 - 13
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -10,12 +10,12 @@ import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
-import { useLayoutFluidClassName } from '~/client/services/layout';
+import { useShouldExpandContent } from '~/client/services/layout';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
-import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
+import { useCurrentUser } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
@@ -32,9 +32,10 @@ import type { PageContentFooterProps } from '../PageContentFooter';
 import styles from './SearchResultContent.module.scss';
 
 const moduleClass = styles['search-result-content'];
+const _fluidLayoutClass = styles['fluid-layout'];
 
 
-const SubNavButtons = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
+const PageControls = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
 const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
 const PageComment = dynamic<PageCommentProps>(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
 const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
@@ -123,12 +124,8 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: rendererOptions } = useSearchResultOptions(pageWithMeta.data.path, highlightKeywords);
   const { data: currentUser } = useCurrentUser();
-  const { data: isContainerFluid } = useIsContainerFluid();
 
-  const [isExpandContentWidth, setIsExpandContentWidth] = useState(page.expandContentWidth);
-
-  // TODO: determine className by the 'expandContentWidth' from the updated page
-  const growiLayoutFluidClass = useLayoutFluidClassName(isExpandContentWidth);
+  const shouldExpandContent = useShouldExpandContent(page);
 
   const duplicateItemClickedHandler = useCallback(async(pageToDuplicate) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -176,7 +173,8 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
     await updateContentWidth(pageId, value);
-    setIsExpandContentWidth(value);
+
+    // TODO: revalidate page data and update shouldExpandContent
   }, []);
 
   const RightComponent = useCallback(() => {
@@ -188,11 +186,11 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
 
     return (
       <div className="d-flex flex-column align-items-end justify-content-center px-2 py-1">
-        <SubNavButtons
+        <PageControls
           pageId={page._id}
           revisionId={revisionId}
           path={page.path}
-          expandContentWidth={isExpandContentWidth ?? isContainerFluid}
+          expandContentWidth={shouldExpandContent}
           showPageControlDropdown={showPageControlDropdown}
           forceHideMenuItems={forceHideMenuItems}
           additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
@@ -203,16 +201,18 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
         />
       </div>
     );
-  }, [page, isExpandContentWidth, showPageControlDropdown, forceHideMenuItems, isContainerFluid,
+  }, [page, shouldExpandContent, showPageControlDropdown, forceHideMenuItems,
       duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
 
   const isRenderable = page != null && rendererOptions != null;
 
+  const fluidLayoutClass = shouldExpandContent ? _fluidLayoutClass : '';
+
   return (
     <div
       key={page._id}
       data-testid="search-result-content"
-      className={`dynamic-layout-root ${growiLayoutFluidClass} ${moduleClass}`}
+      className={`dynamic-layout-root ${moduleClass} ${fluidLayoutClass}`}
     >
       <RightComponent />
 

+ 4 - 0
apps/app/src/components/ShareLinkPageView.tsx

@@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 
+import { useShouldExpandContent } from '~/client/services/layout';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
@@ -44,6 +45,8 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
   const { data: viewOptions } = useViewOptions();
 
+  const shouldExpandContent = useShouldExpandContent(page);
+
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
 
   const specialContents = useMemo(() => {
@@ -93,6 +96,7 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     <PageViewLayout
       headerContents={headerContents}
       sideContents={sideContents}
+      expandContentWidth={shouldExpandContent}
     >
       { specialContents }
       { specialContents == null && (

+ 2 - 1
apps/app/src/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -40,13 +40,14 @@
   // set width for truncation
   $grw-page-controls-width: 226px;
   $grw-page-editor-mode-manager-width: 90px;
-  $grw-contextual-subnavigation-padding-right: 8px;
+  $grw-contextual-subnavigation-padding-right: 12px;
   $gap: 8px;
   width: calc(100vw - #{$grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
 
   @include bs.media-breakpoint-up(md) {
     $grw-page-editor-mode-manager-width: 140px;
     $gap: 24px;
+    $grw-contextual-subnavigation-padding-right: 24px;
     width: calc(100vw - #{var.$grw-sidebar-nav-width + $grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
   }
 }

+ 6 - 6
apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx

@@ -2,12 +2,13 @@ import React from 'react';
 
 import { useTranslation } from 'react-i18next';
 
+import { LabelType } from '~/interfaces/template';
+
 type DropendMenuProps = {
   todaysPath: string,
   onClickCreateNewPageButtonHandler: () => Promise<void>
   onClickCreateTodaysButtonHandler: () => Promise<void>
-  onClickTemplateForChildrenButtonHandler: () => Promise<void>
-  onClickTemplateForDescendantsButtonHandler: () => Promise<void>
+  onClickTemplateButtonHandler: (label: LabelType) => Promise<void>
 }
 
 export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element => {
@@ -15,8 +16,7 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
     todaysPath,
     onClickCreateNewPageButtonHandler,
     onClickCreateTodaysButtonHandler,
-    onClickTemplateForChildrenButtonHandler,
-    onClickTemplateForDescendantsButtonHandler,
+    onClickTemplateButtonHandler,
   } = props;
 
   const { t } = useTranslation('commons');
@@ -48,7 +48,7 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
       <li>
         <button
           className="dropdown-item"
-          onClick={onClickTemplateForChildrenButtonHandler}
+          onClick={() => onClickTemplateButtonHandler('_template')}
           type="button"
         >
           {t('create_page_dropdown.template.children')}
@@ -57,7 +57,7 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
       <li>
         <button
           className="dropdown-item"
-          onClick={onClickTemplateForDescendantsButtonHandler}
+          onClick={() => onClickTemplateButtonHandler('__template')}
           type="button"
         >
           {t('create_page_dropdown.template.descendants')}

+ 21 - 145
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -1,172 +1,49 @@
-import React, { useCallback, useState } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { format } from 'date-fns';
-import { useRouter } from 'next/router';
 
-import { createPage, exist } from '~/client/services/page-operation';
+import { useOnTemplateButtonClicked } from '~/client/services/use-on-template-button-clicked';
 import { toastError } from '~/client/util/toastr';
+import { LabelType } from '~/interfaces/template';
 import { useCurrentUser } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
 
 import { CreateButton } from './CreateButton';
 import { DropendMenu } from './DropendMenu';
 import { DropendToggle } from './DropendToggle';
-
-const logger = loggerFactory('growi:cli:PageCreateButton');
+import { useOnNewButtonClicked, useOnTodaysButtonClicked } from './hooks';
 
 export const PageCreateButton = React.memo((): JSX.Element => {
-  const router = useRouter();
   const { data: currentPage, isLoading } = useSWRxCurrentPage();
   const { data: currentUser } = useCurrentUser();
 
   const [isHovered, setIsHovered] = useState(false);
-  const [isCreating, setIsCreating] = useState(false);
 
   const now = format(new Date(), 'yyyy/MM/dd');
   const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
   const todaysPath = `${userHomepagePath}/memo/${now}`;
 
-  const onMouseEnterHandler = () => {
-    setIsHovered(true);
-  };
-
-  const onMouseLeaveHandler = () => {
-    setIsHovered(false);
-  };
-
-  const onClickCreateNewPageButtonHandler = useCallback(async() => {
-    if (isLoading) return;
-
-    try {
-      setIsCreating(true);
-
-      const parentPath = currentPage == null
-        ? '/'
-        : currentPage.path;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
-        shouldGeneratePath: true,
-      };
-
-      const response = await createPage(parentPath, '', params);
-
-      router.push(`${response.page.id}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentPage, isLoading, router]);
-
-  const onClickCreateTodaysButtonHandler = useCallback(async() => {
-    if (currentUser == null) {
-      return;
-    }
-
-    try {
-      setIsCreating(true);
-
-      // TODO: get grant, grantUserGroupId data from parent page
-      // https://redmine.weseek.co.jp/issues/133892
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-      };
-
-      const res = await exist(JSON.stringify([todaysPath]));
-      if (!res.pages[todaysPath]) {
-        await createPage(todaysPath, '', params);
-      }
-
-      router.push(`${todaysPath}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentUser, router, todaysPath]);
-
-  const onClickTemplateForChildrenButtonHandler = useCallback(async() => {
-    if (isLoading) return;
+  const { onClickHandler: onClickNewButton, isPageCreating: isNewPageCreating } = useOnNewButtonClicked(isLoading, currentPage);
+  const { onClickHandler: onClickTodaysButton, isPageCreating: isTodaysPageCreating } = useOnTodaysButtonClicked(todaysPath, currentUser);
+  const { onClickHandler: onClickTemplateButton, isPageCreating: isTemplatePageCreating } = useOnTemplateButtonClicked(currentPage?.path);
 
+  const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
     try {
-      setIsCreating(true);
-
-      const path = currentPage == null || currentPage.path === '/'
-        ? '/_template'
-        : `${currentPage.path}/_template`;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
-      };
-
-      const res = await exist(JSON.stringify([path]));
-      if (!res.pages[path]) {
-        await createPage(path, '', params);
-      }
-
-      router.push(`${path}#edit`);
+      await onClickTemplateButton(label);
     }
     catch (err) {
-      logger.warn(err);
       toastError(err);
     }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentPage, isLoading, router]);
-
-  const onClickTemplateForDescendantsButtonHandler = useCallback(async() => {
-    if (isLoading) return;
-
-    try {
-      setIsCreating(true);
+  }, [onClickTemplateButton]);
 
-      const path = currentPage == null || currentPage.path === '/'
-        ? '/__template'
-        : `${currentPage.path}/__template`;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
-      };
-
-      const res = await exist(JSON.stringify([path]));
-      if (!res.pages[path]) {
-        await createPage(path, '', params);
-      }
+  const onMouseEnterHandler = () => {
+    setIsHovered(true);
+  };
 
-      router.push(`${path}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentPage, isLoading, router]);
+  const onMouseLeaveHandler = () => {
+    setIsHovered(false);
+  };
 
   return (
     <div
@@ -177,8 +54,8 @@ export const PageCreateButton = React.memo((): JSX.Element => {
       <div className="btn-group flex-grow-1">
         <CreateButton
           className="z-2"
-          onClick={onClickCreateNewPageButtonHandler}
-          disabled={isCreating}
+          onClick={onClickNewButton}
+          disabled={isNewPageCreating || isTodaysPageCreating || isTemplatePageCreating}
         />
       </div>
       { isHovered && (
@@ -190,10 +67,9 @@ export const PageCreateButton = React.memo((): JSX.Element => {
           />
           <DropendMenu
             todaysPath={todaysPath}
-            onClickCreateNewPageButtonHandler={onClickCreateNewPageButtonHandler}
-            onClickCreateTodaysButtonHandler={onClickCreateTodaysButtonHandler}
-            onClickTemplateForChildrenButtonHandler={onClickTemplateForChildrenButtonHandler}
-            onClickTemplateForDescendantsButtonHandler={onClickTemplateForDescendantsButtonHandler}
+            onClickCreateNewPageButtonHandler={onClickNewButton}
+            onClickCreateTodaysButtonHandler={onClickTodaysButton}
+            onClickTemplateButtonHandler={onClickTemplateButtonHandler}
           />
         </div>
       )}

+ 95 - 0
apps/app/src/components/Sidebar/PageCreateButton/hooks.tsx

@@ -0,0 +1,95 @@
+import { useCallback, useState } from 'react';
+
+import type { Nullable, IPagePopulatedToShowRevision, IUserHasId } from '@growi/core';
+import { useRouter } from 'next/router';
+
+import { createPage, exist } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/toastr';
+
+export const useOnNewButtonClicked = (
+    isLoading: boolean,
+    currentPage?: IPagePopulatedToShowRevision | null,
+): {
+  onClickHandler: () => Promise<void>,
+  isPageCreating: boolean
+} => {
+  const router = useRouter();
+  const [isPageCreating, setIsPageCreating] = useState(false);
+
+  const onClickHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsPageCreating(true);
+
+      const parentPath = currentPage == null
+        ? '/'
+        : currentPage.path;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: 4,
+        // grant: currentPage?.grant || 1,
+        // grantUserGroupId: currentPage?.grantedGroup?._id,
+        shouldGeneratePath: true,
+      };
+
+      const response = await createPage(parentPath, '', params);
+
+      router.push(`${response.page.id}#edit`);
+    }
+    catch (err) {
+      toastError(err);
+    }
+    finally {
+      setIsPageCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  return { onClickHandler, isPageCreating };
+};
+
+export const useOnTodaysButtonClicked = (
+    todaysPath: string,
+    currentUser?: Nullable<IUserHasId> | undefined,
+): {
+  onClickHandler: () => Promise<void>,
+  isPageCreating: boolean
+} => {
+  const router = useRouter();
+  const [isPageCreating, setIsPageCreating] = useState(false);
+
+  const onClickHandler = useCallback(async() => {
+    if (currentUser == null) {
+      return;
+    }
+
+    try {
+      setIsPageCreating(true);
+
+      // TODO: get grant, grantUserGroupId data from parent page
+      // https://redmine.weseek.co.jp/issues/133892
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: 4,
+      };
+
+      const res = await exist(JSON.stringify([todaysPath]));
+      if (!res.pages[todaysPath]) {
+        await createPage(todaysPath, '', params);
+      }
+
+      router.push(`${todaysPath}#edit`);
+    }
+    catch (err) {
+      toastError(err);
+    }
+    finally {
+      setIsPageCreating(false);
+    }
+  }, [currentUser, router, todaysPath]);
+
+  return { onClickHandler, isPageCreating };
+};

+ 1 - 1
apps/app/src/components/Sidebar/Tag.tsx

@@ -44,7 +44,7 @@ const Tag: FC = () => {
 
   // todo: adjust design by XD
   return (
-    <div className="grw-container-convertible container-lg px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
+    <div className="container-lg px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">{t('Tags')}</h3>
         <SidebarHeaderReloadButton onClick={() => onReload()} />

+ 3 - 5
apps/app/src/pages/[[...path]].page.tsx

@@ -5,7 +5,7 @@ import EventEmitter from 'events';
 
 import { isIPageInfoForEntity } from '@growi/core';
 import type {
-  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
+  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision,
 } from '@growi/core';
 import {
   isClient, pagePathUtils, pathUtils,
@@ -20,7 +20,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useEditorModeClassName, useLayoutFluidClassNameByPage } from '~/client/services/layout';
+import { useEditorModeClassName } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript'; import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -249,8 +249,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
 
-  const growiLayoutFluidClass = useLayoutFluidClassNameByPage(pageWithMeta?.data);
-
   // Store initial data (When revisionBody is not SSR)
   useEffect(() => {
     if (!props.skipSSR) {
@@ -323,7 +321,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       <Head>
         <title>{title}</title>
       </Head>
-      <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
+      <div className="dynamic-layout-root justify-content-between">
         <nav className="sticky-top">
           <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
         </nav>

+ 1 - 1
apps/app/src/pages/me/[[...path]].page.tsx

@@ -125,7 +125,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
         <div id="main" className="main">
-          <div id="content-main" className="content-main container-lg grw-container-convertible">
+          <div id="content-main" className="content-main container-lg">
             {targetPage.component}
           </div>
         </div>

+ 2 - 5
apps/app/src/pages/share/[[...path]].page.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect } from 'react';
 
-import { type IUserHasId, type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
+import { type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
 import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -8,7 +8,6 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 import superjson from 'superjson';
 
-import { useLayoutFluidClassNameByPage } from '~/client/services/layout';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
@@ -108,8 +107,6 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   }, [mutateCurrentPage, props.isNotFound, props.shareLink?.relatedPage._id, props.skipSSR]);
 
 
-  const growiLayoutFluidClass = useLayoutFluidClassNameByPage(props.shareLinkRelatedPage);
-
   const pagePath = props.shareLinkRelatedPage?.path ?? '';
 
   const title = generateCustomTitleForPage(props, pagePath);
@@ -120,7 +117,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
         <title>{title}</title>
       </Head>
 
-      <div className={`dynamic-layout-root ${growiLayoutFluidClass} justify-content-between`}>
+      <div className="dynamic-layout-root justify-content-between">
         <nav className="sticky-top">
           <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
         </nav>

+ 1 - 1
apps/app/src/pages/tags.page.tsx

@@ -73,7 +73,7 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
         <title>{title}</title>
       </Head>
       <div className="dynamic-layout-root">
-        <div className="grw-container-convertible container-lg mb-5 pb-5" data-testid="tags-page">
+        <div className="container-lg mb-5 pb-5" data-testid="tags-page">
           <h2 className="my-3">{`${t('Tags')}(${totalCount})`}</h2>
           <div className="px-3 mb-5 text-center">
             <TagCloudBox tags={tagData} minSize={20} />

+ 1 - 1
apps/app/src/pages/trash.page.tsx

@@ -68,7 +68,7 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
           TODO: implement navigation for /trash
         </nav>
 
-        <div className="content-main container-lg grw-container-convertible mb-5 pb-5">
+        <div className="content-main container-lg mb-5 pb-5">
           <PagePathNavSticky pagePath="/trash" />
           <TrashPageList />
         </div>

+ 0 - 5
apps/app/src/styles/_layout.scss

@@ -6,11 +6,6 @@
   @extend .flex-expand-vert;
 }
 
-.dynamic-layout-root.growi-layout-fluid .grw-container-convertible {
-  width: 100%;
-  max-width: none;
-}
-
 .grw-bg-image-wrapper {
   position: fixed;
   width: 100%;

+ 1 - 0
apps/app/src/styles/_mixins.scss

@@ -2,6 +2,7 @@
 @use './variables' as var;
 
 @import './mixins/editing';
+@import './mixins/fluid-layout';
 @import './mixins/share-link';
 
 @mixin variable-font-size($basesize) {

+ 4 - 0
apps/app/src/styles/mixins/_fluid-layout.scss

@@ -0,0 +1,4 @@
+@mixin fluid-layout() {
+  width: 100%;
+  max-width: none;
+}

+ 3 - 1
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -269,7 +269,9 @@ describe('Access to sidebar', () => {
         });
 
         it('Succesfully click all tags button', () => {
-          cy.get('.grw-container-convertible > div > .btn-primary').click({force: true});
+          cy.getByTestid('grw-sidebar-content-tags').within(() => {
+            cy.get('.btn-primary').click({force: true});
+          });
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');