Просмотр исходного кода

Merge pull request #6397 from weseek/imprv/switch-content-width

imprv: Switch content width
Yuki Takei 3 лет назад
Родитель
Сommit
4eb1376423

+ 21 - 2
packages/app/src/client/services/page-operation.ts

@@ -37,8 +37,27 @@ export const toggleBookmark = async(pageId: string, currentValue?: boolean): Pro
   }
 };
 
-export const toggleContentWidth = async(pageId: string, currentValue: boolean): Promise<void> => {
-  await apiv3Put(`/page/${pageId}/content-width`, { isContainerFluid: !currentValue });
+// Utility to update body class
+const updateBodyClassByView = (expandContentWidth: boolean): void => {
+  const bodyClasses = document.body.classList;
+  const isLayoutFluid = bodyClasses.contains('growi-layout-fluid');
+
+  if (expandContentWidth && !isLayoutFluid) {
+    bodyClasses.add('growi-layout-fluid');
+  }
+  else if (isLayoutFluid) {
+    bodyClasses.remove('growi-layout-fluid');
+  }
+};
+
+export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
+  try {
+    await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
+    updateBodyClassByView(newValue);
+  }
+  catch (err) {
+    toastError(err);
+  }
 };
 
 export const bookmark = async(pageId: string): Promise<void> => {

+ 12 - 59
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -41,13 +41,12 @@ type CommonProps = {
   onClickDuplicateMenuItem?: (pageId: string) => Promise<void> | void,
   onClickDeleteMenuItem?: (pageId: string, pageInfo: IPageInfoAll | undefined) => Promise<void> | void,
   onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
-  onClickSwitchContentWidthMenuItem?: (pageId: string, isContainerFluid?: boolean) => Promise<void>,
   onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void,
 
+  additionalMenuItemOnTopRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   isInstantRename?: boolean,
   alignRight?: boolean,
-  isContainerFluid?: boolean
 }
 
 
@@ -57,37 +56,18 @@ type DropdownMenuProps = CommonProps & {
   operationProcessData?: IPageOperationProcessData,
 }
 
-// Utility to update body class
-const updateBodyClassByView = (isContainerFluid: boolean): void => {
-  const bodyClasses = document.body.classList;
-  const isLayoutFluid = bodyClasses.contains('growi-layout-fluid');
-
-  if (isContainerFluid && !isLayoutFluid) {
-    bodyClasses.add('growi-layout-fluid');
-  }
-  else if (isLayoutFluid) {
-    bodyClasses.remove('growi-layout-fluid');
-  }
-};
-
 const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => {
   const { t } = useTranslation('');
 
   const {
     pageId, isLoading, pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
     onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
-    onClickRevertMenuItem, onClickPathRecoveryMenuItem, onClickSwitchContentWidthMenuItem,
-    additionalMenuItemRenderer: AdditionalMenuItems, isInstantRename, alignRight,
+    onClickRevertMenuItem, onClickPathRecoveryMenuItem,
+    additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
+    additionalMenuItemRenderer: AdditionalMenuItems,
+    isInstantRename, alignRight,
   } = props;
 
-  const switchContentWidthHandler = useCallback(async() => {
-    if (!isIPageInfoForOperation(pageInfo) || onClickSwitchContentWidthMenuItem == null) {
-      return;
-    }
-    await onClickSwitchContentWidthMenuItem(pageId, pageInfo.isContainerFluid);
-    updateBodyClassByView(!pageInfo.isContainerFluid);
-  }, [onClickSwitchContentWidthMenuItem, pageId, pageInfo]);
-
   // eslint-disable-next-line react-hooks/rules-of-hooks
   const bookmarkItemClickedHandler = useCallback(async() => {
     if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) {
@@ -172,31 +152,15 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
           </DropdownItem>
         ) }
 
-        {/* Content width switcher */}
-        { !forceHideMenuItems?.includes(MenuItemType.SWITCH_CONTENT_WIDTH) && isEnableActions && !pageInfo.isEmpty && isIPageInfoForOperation(pageInfo) && (
-          <DropdownItem
-            onClick={switchContentWidthHandler}
-            className="grw-page-control-dropdown-item"
-          >
-            <div className="custom-control custom-switch ml-1">
-              <input
-                id="switchContentWidth"
-                className="custom-control-input"
-                type="checkbox"
-                checked={pageInfo.isContainerFluid}
-                onChange={() => {}}
-              />
-              <label className="custom-control-label" htmlFor="switchContentWidth">
-                { t('wide_view') }
-              </label>
-            </div>
-          </DropdownItem>
+        { AdditionalMenuItemsOnTop && (
+          <>
+            <AdditionalMenuItemsOnTop pageInfo={pageInfo} />
+            <DropdownItem divider />
+          </>
         ) }
-        { !forceHideMenuItems?.includes(MenuItemType.SWITCH_CONTENT_WIDTH) && !pageInfo.isEmpty && <DropdownItem divider /> }
-
 
         {/* Bookmark */}
-        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && !pageInfo.isEmpty && isIPageInfoForOperation(pageInfo) && (
+        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && isIPageInfoForOperation(pageInfo) && (
           <DropdownItem
             onClick={bookmarkItemClickedHandler}
             className="grw-page-control-dropdown-item"
@@ -299,7 +263,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 
   const {
     pageId, pageInfo: presetPageInfo, fetchOnInit, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
-    onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem, onClickSwitchContentWidthMenuItem,
+    onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
   } = props;
 
   const [isOpen, setIsOpen] = useState(false);
@@ -317,16 +281,6 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
     }
   }, [isOpen, presetPageInfo, shouldFetch]);
 
-  const switchContentWidthMenuItemHandler = useCallback(async(_pageId: string, _isContainerFluid: boolean) => {
-    if (onClickSwitchContentWidthMenuItem != null) {
-      await onClickSwitchContentWidthMenuItem(_pageId, _isContainerFluid);
-    }
-
-    if (shouldFetch) {
-      mutatePageInfo();
-    }
-  }, [mutatePageInfo, onClickSwitchContentWidthMenuItem, shouldFetch]);
-
   // mutate after handle event
   const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => {
     if (onClickBookmarkMenuItem != null) {
@@ -384,7 +338,6 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
         onClickRenameMenuItem={renameMenuItemClickHandler}
         onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
         onClickDeleteMenuItem={deleteMenuItemClickHandler}
-        onClickSwitchContentWidthMenuItem={switchContentWidthMenuItemHandler}
         onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
       />
     </Dropdown>

+ 53 - 6
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -1,7 +1,10 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { DropdownItem } from 'reactstrap';
 
 import {
-  toggleBookmark, toggleLike, toggleSubscribe, toggleContentWidth,
+  toggleBookmark, toggleLike, toggleSubscribe, updateContentWidth,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/apiNotification';
 import {
@@ -22,6 +25,43 @@ import SubscribeButton from '../SubscribeButton';
 import SeenUserInfo from '../User/SeenUserInfo';
 
 
+type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
+  onClickMenuItem: (newValue: boolean) => void,
+}
+
+const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    pageInfo, onClickMenuItem,
+  } = props;
+
+  if (!isIPageInfoForEntity(pageInfo)) {
+    return <></>;
+  }
+
+  return (
+    <DropdownItem
+      onClick={() => onClickMenuItem(!pageInfo.expandContentWidth)}
+      className="grw-page-control-dropdown-item"
+    >
+      <div className="custom-control custom-switch ml-1">
+        <input
+          id="switchContentWidth"
+          className="custom-control-input"
+          type="checkbox"
+          checked={pageInfo.expandContentWidth}
+          onChange={() => {}}
+        />
+        <label className="custom-control-label" htmlFor="switchContentWidth">
+          { t('wide_view') }
+        </label>
+      </div>
+    </DropdownItem>
+  );
+};
+
+
 type CommonProps = {
   isCompactMode?: boolean,
   disableSeenUserInfoPopover?: boolean,
@@ -143,15 +183,15 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     onClickDeleteMenuItem(pageToDelete);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
-  const switchContentWidthClickHandler = useCallback(async() => {
+  const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
     if (isGuestUser == null || isGuestUser) {
       return;
     }
-    if (!isIPageInfoForOperation(pageInfo)) {
+    if (!isIPageInfoForEntity(pageInfo)) {
       return;
     }
     try {
-      await toggleContentWidth(pageId, pageInfo.isContainerFluid);
+      await updateContentWidth(pageId, newValue);
       mutatePageInfo();
     }
     catch (err) {
@@ -159,6 +199,13 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     }
   }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
 
+  const wideviewMenuItemRenderer = useMemo(() => {
+    if (!isIPageInfoForEntity(pageInfo)) {
+      return undefined;
+    }
+    return props => <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} />;
+  }, [pageInfo, switchContentWidthClickHandler]);
+
   if (!isIPageInfoForOperation(pageInfo)) {
     return <></>;
   }
@@ -210,11 +257,11 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
+          additionalMenuItemOnTopRenderer={wideviewMenuItemRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickRenameMenuItem={renameMenuItemClickHandler}
           onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
           onClickDeleteMenuItem={deleteMenuItemClickHandler}
-          onClickSwitchContentWidthMenuItem={switchContentWidthClickHandler}
         />
       )}
     </div>

+ 2 - 13
packages/app/src/components/PageList/PageListItemL.tsx

@@ -14,7 +14,7 @@ import urljoin from 'url-join';
 
 
 import { ISelectable } from '~/client/interfaces/selectable-all';
-import { bookmark, unbookmark, toggleContentWidth } from '~/client/services/page-operation';
+import { bookmark, unbookmark } from '~/client/services/page-operation';
 import {
   IPageInfoAll, isIPageInfoForListing, isIPageInfoForEntity, IPageWithMeta, IPageInfoForListing,
 } from '~/interfaces/page';
@@ -31,7 +31,6 @@ import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 import { useSWRxPageInfo } from '../../stores/page';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
-import { toastError } from '~/client/util/apiNotification';
 
 type Props = {
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
@@ -100,7 +99,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
 
   useEffect(() => {
-    if (isIPageInfoForEntity(pageInfo) && pageInfo != null) {
+    if (isIPageInfoForEntity(pageInfo)) {
       // likerCount
       setLikerCount(pageInfo.likerIds?.length ?? 0);
       // bookmarkCount
@@ -125,15 +124,6 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     await bookmarkOperation(_pageId);
   };
 
-  const switchContentWidthMenuItemClickHandler = async(_pageId: string, _isContainerFluid: boolean): Promise<void> => {
-    try {
-      await toggleContentWidth(_pageId, _isContainerFluid);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  };
-
   const duplicateMenuItemClickHandler = useCallback(() => {
     const page = {
       pageId: pageData._id,
@@ -245,7 +235,6 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                   onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
                   onClickDeleteMenuItem={deleteMenuItemClickHandler}
                   onClickRevertMenuItem={revertMenuItemClickHandler}
-                  onClickSwitchContentWidthMenuItem={switchContentWidthMenuItemClickHandler}
                 />
               </div>
             </div>

+ 1 - 3
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -176,8 +176,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       ? page.revision
       : page.revision._id;
 
-    const forceHideMenuItemsWithSwitchContentWidth = forceHideMenuItems ?? [];
-    forceHideMenuItemsWithSwitchContentWidth.push(MenuItemType.SWITCH_CONTENT_WIDTH);
 
     return (
       <div className="d-flex flex-column align-items-end justify-content-center py-md-2">
@@ -186,7 +184,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           revisionId={revisionId}
           path={page.path}
           showPageControlDropdown={showPageControlDropdown}
-          forceHideMenuItems={forceHideMenuItemsWithSwitchContentWidth}
+          forceHideMenuItems={forceHideMenuItems}
           additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
           isCompactMode
           onClickDuplicateMenuItem={duplicateItemClickedHandler}

+ 2 - 4
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -17,7 +17,7 @@ import { useSWRxPageInfoForList } from '~/stores/page';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useFullTextSearchTermManager } from '~/stores/search';
 
-import { ForceHideMenuItems, MenuItemType } from '../Common/Dropdown/PageItemControl';
+import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageListItemL } from '../PageList/PageListItemL';
 
 
@@ -126,8 +126,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     advanceFts();
   };
 
-  const forceHideMenuItemsWithSwitchContent = forceHideMenuItems ?? [];
-  forceHideMenuItemsWithSwitchContent.push(MenuItemType.SWITCH_CONTENT_WIDTH);
   return (
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
       { (injectedPages ?? pages).map((page, i) => {
@@ -139,7 +137,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
             page={page}
             isEnableActions={!isGuestUser}
             isSelected={page.data._id === selectedPageId}
-            forceHideMenuItems={forceHideMenuItemsWithSwitchContent}
+            forceHideMenuItems={forceHideMenuItems}
             onClickItem={clickItemHandler}
             onCheckboxChanged={props.onCheckboxChanged}
             onPageDuplicated={duplicatedHandler}

+ 3 - 3
packages/app/src/interfaces/page.ts

@@ -29,7 +29,7 @@ export interface IPage {
   pageIdOnHackmd: string,
   revisionHackmdSynced: Ref<IRevision>,
   hasDraftOnHackmd: boolean,
-  isContainerFluid: boolean,
+  expandContentWidth?: boolean,
   deleteUser: Ref<IUser>,
   deletedAt: Date,
 }
@@ -54,7 +54,6 @@ export type IPageInfo = {
   isDeletable: boolean,
   isAbleToDeleteCompletely: boolean,
   isRevertible: boolean,
-  isContainerFluid: boolean,
 }
 
 export type IPageInfoForEntity = IPageInfo & {
@@ -63,6 +62,7 @@ export type IPageInfoForEntity = IPageInfo & {
   likerIds: string[],
   sumOfSeenUsers: number,
   seenUserIds: string[],
+  expandContentWidth?: boolean,
 }
 
 export type IPageInfoForOperation = IPageInfoForEntity & {
@@ -84,7 +84,7 @@ export const isIPageInfoForEntity = (pageInfo: any | undefined): pageInfo is IPa
 export const isIPageInfoForOperation = (pageInfo: any | undefined): pageInfo is IPageInfoForOperation => {
   return pageInfo != null
     && isIPageInfoForEntity(pageInfo)
-    && ('isBookmarked' in pageInfo || 'isLiked' in pageInfo || 'subscriptionStatus' in pageInfo || 'isContainerFluid' in pageInfo);
+    && ('isBookmarked' in pageInfo || 'isLiked' in pageInfo || 'subscriptionStatus' in pageInfo);
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any

+ 3 - 3
packages/app/src/server/models/obsolete-page.js

@@ -683,7 +683,7 @@ export const getPageSchema = (crowi) => {
     const Revision = crowi.model('Revision');
     const format = options.format || 'markdown';
     const grantUserGroupId = options.grantUserGroupId || null;
-    const isContainerFluid = crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
+    const expandContentWidth = crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // sanitize path
     path = crowi.xss.process(path); // eslint-disable-line no-param-reassign
@@ -705,8 +705,8 @@ export const getPageSchema = (crowi) => {
     page.creator = user;
     page.lastUpdateUser = user;
     page.status = STATUS_PUBLISHED;
-    if (isContainerFluid != null) {
-      page.isContainerFluid = isContainerFluid;
+    if (expandContentWidth != null) {
+      page.expandContentWidth = expandContentWidth;
     }
     await validateAppliedScope(user, grant, grantUserGroupId);
     page.applyScope(user, grant, grantUserGroupId);

+ 1 - 1
packages/app/src/server/models/page.ts

@@ -102,7 +102,7 @@ const schema = new Schema<PageDocument, PageModel>({
   pageIdOnHackmd: { type: String },
   revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
   hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
-  isContainerFluid: { type: Boolean, default: false },
+  expandContentWidth: { type: Boolean },
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   deleteUser: { type: ObjectId, ref: 'User' },
   deletedAt: { type: Date },

+ 11 - 4
packages/app/src/server/routes/apiv3/page.js

@@ -165,8 +165,9 @@ module.exports = (crowi) => {
   const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
+  const configManager = crowi.configManager;
+
   const globalNotificationService = crowi.getGlobalNotificationService();
-  const socketIoService = crowi.socketIoService;
   const { Page, GlobalNotificationSetting, Bookmark } = crowi.models;
   const { pageService, exportService } = crowi;
 
@@ -220,7 +221,7 @@ module.exports = (crowi) => {
       query('pageId').isString(),
     ],
     contentWidth: [
-      body('isContainerFluid').isBoolean(),
+      body('expandContentWidth').isBoolean(),
     ],
   };
 
@@ -823,10 +824,16 @@ module.exports = (crowi) => {
   router.put('/:pageId/content-width', accessTokenParser, loginRequiredStrictly, csrf,
     validator.contentWidth, apiV3FormValidator, async(req, res) => {
       const { pageId } = req.params;
-      const { isContainerFluid } = req.body;
+      const { expandContentWidth } = req.body;
+
+      const isContainerFluidBySystem = configManager.getConfig('crowi', 'customize:isContainerFluid');
 
       try {
-        const page = await Page.updateOne({ _id: pageId }, { $set: { isContainerFluid } });
+        const updateQuery = expandContentWidth === isContainerFluidBySystem
+          ? { $unset: { expandContentWidth } } // remove if the specified value is the same to the system's one
+          : { $set: { expandContentWidth } };
+
+        const page = await Page.updateOne({ _id: pageId }, updateQuery);
         return res.apiv3({ page });
       }
       catch (err) {

+ 5 - 10
packages/app/src/server/service/page.ts

@@ -246,7 +246,6 @@ class PageService {
           isDeletable: false,
           isAbleToDeleteCompletely: false,
           isRevertible: false,
-          isContainerFluid: page.isContainerFluid,
         },
       };
     }
@@ -273,9 +272,6 @@ class PageService {
     const isLiked: boolean = page.isLiked(user);
 
     const subscription = await Subscription.findByUserIdAndTargetId(user._id, pageId);
-    const isContainerFluid: boolean = page.isContainerFluid != null
-      ? page.isContainerFluid
-      : this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     let creatorId = page.creator;
     if (page.isEmpty) {
@@ -296,7 +292,6 @@ class PageService {
         isBookmarked,
         isLiked,
         subscriptionStatus: subscription?.status,
-        isContainerFluid,
       },
     };
   }
@@ -2173,12 +2168,12 @@ class PageService {
         isDeletable: false,
         isAbleToDeleteCompletely: false,
         isRevertible: false,
-        isContainerFluid: false,
       };
     }
 
     const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
     const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
+    const expandContentWidth = page.expandContentWidth ?? this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     return {
       isV5Compatible: isTopPage(page.path) || page.parent != null,
@@ -2191,7 +2186,7 @@ class PageService {
       isDeletable: isMovable,
       isAbleToDeleteCompletely: false,
       isRevertible: isTrashPage(page.path),
-      isContainerFluid: page.isContainerFluid,
+      expandContentWidth,
     };
 
   }
@@ -3361,7 +3356,7 @@ class PageService {
   async create(path: string, body: string, user, options: PageCreateOptions = {}): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
-    const isContainerFluid = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
+    const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // Switch method
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
@@ -3409,8 +3404,8 @@ class PageService {
       const parent = await this.getParentAndFillAncestorsByUser(user, path);
       page.parent = parent._id;
     }
-    if (isContainerFluid != null) {
-      page.isContainerFluid = isContainerFluid;
+    if (expandContentWidth != null) {
+      page.expandContentWidth = expandContentWidth;
     }
     // Save
     let savedPage = await page.save();

+ 9 - 1
packages/app/src/server/views/layout/layout.html

@@ -61,9 +61,17 @@
 {% block html_body %}
 {% set additionalBodyClasses = []; %}
 {% block html_additional_body_classes %}{% endblock %}
-{% if getConfig('crowi', 'customize:isContainerFluid') || page.isContainerFluid %}
+
+{% if page.expandContentWidth !== undefined %}
+  {% set isContainerFluid = page.expandContentWidth; %}
+{% else %}
+  {% set isContainerFluid = getConfig('crowi', 'customize:isContainerFluid'); %}
+{% endif %}
+
+{% if isContainerFluid  %}
   {% set additionalBodyClasses = additionalBodyClasses|push('growi-layout-fluid') %}
 {% endif %}
+
 <body
   class="{% block html_base_css %}{% endblock %} growi {{ additionalBodyClasses|join(' ') }}"
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"