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

Merge branch 'feat/134360-able-to-use-handsontable-modal-in-editor' into feat/134432-able-to-open-edit-table-modal

soumaeda 2 лет назад
Родитель
Сommit
9219f4ee46
42 измененных файлов с 435 добавлено и 234 удалено
  1. 0 5
      apps/app/_obsolete/src/styles/_override.scss
  2. 1 1
      apps/app/public/static/locales/en_US/translation.json
  3. 1 1
      apps/app/public/static/locales/ja_JP/commons.json
  4. 1 1
      apps/app/public/static/locales/ja_JP/translation.json
  5. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  6. 18 21
      apps/app/src/components/Comments.tsx
  7. 14 10
      apps/app/src/components/Common/PageViewLayout.module.scss
  8. 8 5
      apps/app/src/components/Common/PageViewLayout.tsx
  9. 0 76
      apps/app/src/components/CreateTemplateModal.jsx
  10. 91 0
      apps/app/src/components/CreateTemplateModal.tsx
  11. 1 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  12. 2 2
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  13. 1 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  14. 47 2
      apps/app/src/components/Navbar/hooks.tsx
  15. 18 10
      apps/app/src/components/Page/PageView.tsx
  16. 49 55
      apps/app/src/components/PageComment.tsx
  17. 4 4
      apps/app/src/components/PageCreateModal.jsx
  18. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx
  19. 0 2
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  20. 1 1
      apps/app/src/components/Sidebar/Sidebar.tsx
  21. 2 0
      apps/app/src/interfaces/template.ts
  22. 2 5
      apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts
  23. 2 2
      apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts
  24. 1 1
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts
  25. 5 2
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts
  26. 2 1
      apps/app/src/styles/organisms/_wiki.scss
  27. 1 1
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  28. 1 2
      packages/core/scss/_flex-expand.scss
  29. 14 0
      packages/core/scss/bootstrap/_variables-dark.scss
  30. 22 0
      packages/core/scss/bootstrap/_variables.scss
  31. 2 1
      packages/core/scss/bootstrap/apply.scss
  32. 1 0
      packages/core/scss/bootstrap/init.scss
  33. 59 0
      packages/core/scss/bootstrap/mixins/_button-outline-variant.scss
  34. 3 0
      packages/core/scss/bootstrap/override/_badge.scss
  35. 21 0
      packages/core/scss/bootstrap/override/_buttons.scss
  36. 7 5
      packages/core/scss/bootstrap/theming/_buttons-dark.scss
  37. 17 0
      packages/core/scss/bootstrap/theming/_buttons-light.scss
  38. 3 1
      packages/core/scss/bootstrap/theming/apply-dark.scss
  39. 7 0
      packages/core/scss/bootstrap/theming/apply-light.scss
  40. 0 1
      packages/editor/vite.config.ts
  41. 2 6
      packages/preset-themes/src/styles/default.scss
  42. 2 6
      packages/preset-themes/src/styles/mono-blue.scss

+ 0 - 5
packages/core/scss/bootstrap/_override.scss → apps/app/_obsolete/src/styles/_override.scss

@@ -99,11 +99,6 @@
 //   }
 // }
 
-// Badges
-.badge {
-  @extend .rounded-pill;
-}
-
 // //Modals
 // .modal-open {
 //   width: 100%;

+ 1 - 1
apps/app/public/static/locales/en_US/translation.json

@@ -464,7 +464,7 @@
       "label": "Template for children",
       "desc": "Applies only to the same level pages which the template exists"
     },
-    "decendants": {
+    "descendants": {
       "label": "Template for descendants",
       "desc": "Applies to all decendant pages"
     }

+ 1 - 1
apps/app/public/static/locales/ja_JP/commons.json

@@ -79,7 +79,7 @@
     "template": {
       "desc": "テンプレートページの作成/編集",
       "children": "同一階層テンプレート",
-      "decendants": "下位層テンプレート"
+      "descendants": "下位層テンプレート"
     }
   },
 

+ 1 - 1
apps/app/public/static/locales/ja_JP/translation.json

@@ -497,7 +497,7 @@
       "label": "同一階層テンプレート",
       "desc": "テンプレートページが存在する階層にのみ適用されます"
     },
-    "decendants": {
+    "descendants": {
       "label": "下位層テンプレート",
       "desc": "テンプレートページが存在する下位層のすべてのページに適用されます"
     }

+ 1 - 1
apps/app/public/static/locales/zh_CN/translation.json

@@ -451,7 +451,7 @@
 			"label": "子模板",
 			"desc": "仅应用于模板存在的同一级别页"
 		},
-		"decendants": {
+		"descendants": {
 			"label": "子代模板",
 			"desc": "适用于所有分散页"
 		}

+ 18 - 21
apps/app/src/components/Comments.tsx

@@ -55,7 +55,7 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
     // > You can call observe() multiple times on the same MutationObserver
     // > to watch for changes to different parts of the DOM tree and/or different types of changes.
-  }, [onLoaded]);
+  }, [onLoadedDebounced]);
 
   const isTopPagePath = isTopPage(pagePath);
 
@@ -69,29 +69,26 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   };
 
   return (
-    <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
-      <div className="container-lg">
-        <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
-          <PageComment
+    <div className="page-comments-row mt-5 py-4 border-top border-3 d-edit-none d-print-none">
+      <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
+        <PageComment
+          pageId={pageId}
+          pagePath={pagePath}
+          revision={revision}
+          currentUser={currentUser}
+          isReadOnly={false}
+        />
+      </div>
+      {!isDeleted && (
+        <div id="page-comment-write">
+          <CommentEditor
             pageId={pageId}
-            pagePath={pagePath}
-            revision={revision}
-            currentUser={currentUser}
-            isReadOnly={false}
-            titleAlign="left"
+            isForNewComment
+            onCommentButtonClicked={onCommentButtonClickHandler}
+            revisionId={revision._id}
           />
         </div>
-        {!isDeleted && (
-          <div id="page-comment-write">
-            <CommentEditor
-              pageId={pageId}
-              isForNewComment
-              onCommentButtonClicked={onCommentButtonClickHandler}
-              revisionId={revision._id}
-            />
-          </div>
-        )}
-      </div>
+      )}
     </div>
   );
 

+ 14 - 10
apps/app/src/components/Common/PageViewLayout.module.scss

@@ -3,9 +3,22 @@
 @use '~/styles/variables' as var;
 
 
+$subnavigation-height: 50px;
+$page-view-layout-margin-top: 32px;
+
+.page-view-layout :global {
+  $page-content-footer-min-heigh: 130px;
+  min-height: calc(100vh - #{$subnavigation-height + $page-view-layout-margin-top + $page-content-footer-min-heigh});
+}
+
+// md/lg layout padding
 .page-view-layout :global {
-  min-height: calc(100vh - 48px - 250px); // 100vh - subnavigation height - page-comments-row minimum height
+  @include bs.media-breakpoint-between(md, xl) {
+    padding-left: var.$grw-sidebar-nav-width;
+  }
+}
 
+.page-view-layout :global {
   .grw-side-contents-container {
     margin-bottom: 1rem;
 
@@ -17,20 +30,11 @@
   }
 }
 
-// md/lg layout padding
-.page-view-layout :global {
-  @include bs.media-breakpoint-between(md, xl) {
-    padding-left: var.$grw-sidebar-nav-width;
-  }
-}
-
 // sticky side contents
 .page-view-layout :global {
   .grw-side-contents-sticky-container {
     position: sticky;
 
-    $subnavigation-height: 50px;
-    $page-view-layout-margin-top: 32px;
     $page-path-nav-height: 99px;
     top: calc($subnavigation-height + $page-view-layout-margin-top + $page-path-nav-height + 4px);
   }

+ 8 - 5
apps/app/src/components/Common/PageViewLayout.tsx

@@ -2,6 +2,9 @@ import type { ReactNode } from 'react';
 
 import styles from './PageViewLayout.module.scss';
 
+const pageViewLayoutClass = styles['page-view-layout'] ?? '';
+const footerLayoutClass = styles['footer-layout'] ?? '';
+
 type Props = {
   children?: ReactNode,
   headerContents?: ReactNode,
@@ -16,13 +19,13 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
   return (
     <>
-      <div id="main" className={`main ${styles['page-view-layout']}`}>
-        <div id="content-main" className="content-main container-lg grw-container-convertible">
+      <div id="main" className={`main ${pageViewLayoutClass} flex-expand-vert`}>
+        <div id="content-main" className="content-main container-lg grw-container-convertible flex-expand-vert">
           { headerContents != null && headerContents }
           { sideContents != null
             ? (
-              <div className="d-flex gap-3">
-                <div className="flex-grow-1 flex-basis-0 mw-0">
+              <div className="flex-expand-horiz gap-3">
+                <div className="flex-expand-vert flex-basis-0 mw-0">
                   {children}
                 </div>
                 <div className="grw-side-contents-container col-lg-3  d-edit-none d-print-none" data-vrt-blackout-side-contents>
@@ -40,7 +43,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
       </div>
 
       { footerContents != null && (
-        <footer className="footer d-edit-none">
+        <footer className={`footer d-edit-none ${footerLayoutClass}`}>
           {footerContents}
         </footer>
       ) }

+ 0 - 76
apps/app/src/components/CreateTemplateModal.jsx

@@ -1,76 +0,0 @@
-import React from 'react';
-
-import { pathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
-import urljoin from 'url-join';
-
-const CreateTemplateModal = (props) => {
-  const { t } = useTranslation();
-  const { path } = props;
-
-  const parentPath = pathUtils.addTrailingSlash(path);
-
-  function generateUrl(label) {
-    return encodeURI(urljoin(parentPath, label, '#edit'));
-  }
-
-  /**
-   * @param {string} target Which hierarchy to create [children, decendants]
-   */
-  function renderTemplateCard(target, label) {
-    return (
-      <div className="card card-select-template">
-        <div className="card-header">{ t(`template.${target}.label`) }</div>
-        <div className="card-body">
-          <p className="text-center"><code>{label}</code></p>
-          <p className="form-text text-muted text-center"><small>{t(`template.${target}.desc`) }</small></p>
-        </div>
-        <div className="card-footer text-center">
-          <a
-            data-testid={`template-button-${target}`}
-            href={generateUrl(label)}
-            className="btn btn-sm btn-primary"
-            id={`template-button-${target}`}
-          >
-            { t('Edit') }
-          </a>
-        </div>
-      </div>
-    );
-  }
-
-  return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose} data-testid="page-template-modal">
-      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
-        {t('template.modal_label.Create/Edit Template Page')}
-      </ModalHeader>
-      <ModalBody>
-        <div>
-          <label className="form-label mb-4">
-            <code>{parentPath}</code><br />
-            { t('template.modal_label.Create template under') }
-          </label>
-          <div className="row row-cols-2">
-            <div className="col">
-              {renderTemplateCard('children', '_template')}
-            </div>
-            <div className="col">
-              {renderTemplateCard('decendants', '__template')}
-            </div>
-          </div>
-        </div>
-      </ModalBody>
-    </Modal>
-
-  );
-};
-
-CreateTemplateModal.propTypes = {
-  path: PropTypes.string.isRequired,
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-};
-
-export default CreateTemplateModal;

+ 91 - 0
apps/app/src/components/CreateTemplateModal.tsx

@@ -0,0 +1,91 @@
+import React from 'react';
+
+import { pathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+import { TargetType, LabelType } from '~/interfaces/template';
+
+import { useOnTemplateButtonClicked } from './Navbar/hooks';
+
+type TemplateCardProps = {
+  target: TargetType;
+  label: LabelType;
+  isPageCreating: boolean;
+  onClickHandler: () => void;
+};
+
+const TemplateCard: React.FC<TemplateCardProps> = ({
+  target, label, isPageCreating, onClickHandler,
+}) => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="card card-select-template">
+      <div className="card-header">{t(`template.${target}.label`)}</div>
+      <div className="card-body">
+        <p className="text-center"><code>{label}</code></p>
+        <p className="form-text text-muted text-center"><small>{t(`template.${target}.desc`)}</small></p>
+      </div>
+      <div className="card-footer text-center">
+        <button
+          disabled={isPageCreating}
+          data-testid={`template-button-${target}`}
+          className="btn btn-sm btn-primary"
+          id={`template-button-${target}`}
+          onClick={onClickHandler}
+          type="button"
+        >
+          {t('Edit')}
+        </button>
+      </div>
+    </div>
+  );
+};
+
+type CreateTemplateModalProps = {
+  path: string;
+  isOpen: boolean;
+  onClose: () => void;
+};
+
+export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
+  path, isOpen, onClose,
+}) => {
+  const { t } = useTranslation();
+
+  const { onClickHandler: onClickTemplateButton, isPageCreating } = useOnTemplateButtonClicked(path);
+
+  const parentPath = pathUtils.addTrailingSlash(path);
+
+  const renderTemplateCard = (target: TargetType, label: LabelType) => (
+    <div className="col">
+      <TemplateCard
+        target={target}
+        label={label}
+        isPageCreating={isPageCreating}
+        onClickHandler={() => onClickTemplateButton(label)}
+      />
+    </div>
+  );
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
+      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
+        {t('template.modal_label.Create/Edit Template Page')}
+      </ModalHeader>
+      <ModalBody>
+        <div>
+          <label className="form-label mb-4">
+            <code>{parentPath}</code><br />
+            {t('template.modal_label.Create template under')}
+          </label>
+          <div className="row row-cols-2">
+            {renderTemplateCard('children', '_template')}
+            {renderTemplateCard('descendants', '__template')}
+          </div>
+        </div>
+      </ModalBody>
+    </Modal>
+  );
+};

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

@@ -31,7 +31,7 @@ import {
   useSelectedGrant,
 } from '~/stores/ui';
 
-import CreateTemplateModal from '../CreateTemplateModal';
+import { CreateTemplateModal } from '../CreateTemplateModal';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';

+ 2 - 2
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -31,7 +31,7 @@
 // == Colors
 @include bs.color-mode(light) {
   .grw-page-editor-mode-manager :global {
-    .btn-outline-primary {
+    .btn {
       $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-700));
       $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-100));
       $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-100-rgb));
@@ -51,7 +51,7 @@
 }
 @include bs.color-mode(dark) {
   .grw-page-editor-mode-manager :global {
-    .btn-outline-primary {
+    .btn {
       $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-300));
       $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-800));
       $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-800-rgb));

+ 1 - 1
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -22,7 +22,7 @@ const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
     currentEditorMode, isBtnDisabled, editorMode, children, onClick,
   } = props;
 
-  const classNames = ['btn btn-outline-primary py-1 px-2 d-flex align-items-center justify-content-center'];
+  const classNames = ['btn py-1 px-2 d-flex align-items-center justify-content-center'];
   if (currentEditorMode === editorMode) {
     classNames.push('active');
   }

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

@@ -1,10 +1,11 @@
-import { useCallback } from 'react';
+import { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 
-import { createPage } from '~/client/services/page-operation';
+import { createPage, exist } 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';
@@ -56,3 +57,47 @@ 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 };
+};

+ 18 - 10
apps/app/src/components/Page/PageView.tsx

@@ -112,14 +112,6 @@ export const PageView = (props: Props): JSX.Element => {
   const footerContents = !isIdenticalPathPage && !isNotFound
     ? (
       <>
-        <div id="comments-container" ref={commentsContainerRef}>
-          <Comments
-            pageId={page._id}
-            pagePath={pagePath}
-            revision={page.revision}
-            onLoaded={() => setCommentsLoaded(true)}
-          />
-        </div>
         {(isUsersHomepagePath && page.creator != null) && (
           <UsersHomepageFooter creatorId={page.creator._id} />
         )}
@@ -139,11 +131,27 @@ export const PageView = (props: Props): JSX.Element => {
     return (
       <>
         <PageContentsUtilities />
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+
+        <div className="flex-expand-vert justify-content-between">
+          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+
+          { !isIdenticalPathPage && !isNotFound && (
+            <div id="comments-container" ref={commentsContainerRef}>
+              <Comments
+                pageId={page._id}
+                pagePath={pagePath}
+                revision={page.revision}
+                onLoaded={() => setCommentsLoaded(true)}
+              />
+            </div>
+          ) }
+        </div>
       </>
     );
   };
 
+  const mobileClass = isMobile ? styles['page-mobile'] : '';
+
   return (
     <PageViewLayout
       headerContents={headerContents}
@@ -156,7 +164,7 @@ export const PageView = (props: Props): JSX.Element => {
       {specialContents == null && (
         <>
           {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
-          <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
+          <div className={`flex-expand-vert ${mobileClass}`}>
             <Contents />
           </div>
         </>

+ 49 - 55
apps/app/src/components/PageComment.tsx

@@ -31,14 +31,13 @@ export type PageCommentProps = {
   revision: string | IRevisionHasId,
   currentUser: any,
   isReadOnly: boolean,
-  titleAlign?: 'center' | 'left' | 'right',
 }
 
 export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
 
   const {
     rendererOptions: rendererOptionsByProps,
-    pageId, pagePath, revision, currentUser, isReadOnly, titleAlign,
+    pageId, pagePath, revision, currentUser, isReadOnly,
   } = props;
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
@@ -112,9 +111,6 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
     return <></>;
   }
 
-  let commentTitleClasses = 'border-bottom py-3 mb-3';
-  commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
-
   const rendererOptions = rendererOptionsByProps ?? rendererOptionsForCurrentPage;
 
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
@@ -156,58 +152,56 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
 
   return (
     <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
-      <div className="container-lg">
-        <div className="page-comments">
-          <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
-          <div className="page-comments-list" id="page-comments-list">
-            {commentsExceptReply.map((comment) => {
-
-              const defaultCommentThreadClasses = 'page-comment-thread pb-5';
-              const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
-
-              let commentThreadClasses = '';
-              commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
-
-              return (
-                <div key={comment._id} className={commentThreadClasses}>
-                  {commentElement(comment)}
-                  {hasReply && replyCommentsElement(allReplies[comment._id])}
-                  {(!isReadOnly && !showEditorIds.has(comment._id)) && (
-                    <div className="d-flex flex-row-reverse">
-                      <NotAvailableForGuest>
-                        <NotAvailableForReadOnlyUser>
-                          <Button
-                            data-testid="comment-reply-button"
-                            outline
-                            color="secondary"
-                            size="sm"
-                            className="btn-comment-reply"
-                            onClick={() => onReplyButtonClickHandler(comment._id)}
-                          >
-                            <i className="icon-fw icon-action-undo"></i> Reply
-                          </Button>
-                        </NotAvailableForReadOnlyUser>
-                      </NotAvailableForGuest>
-                    </div>
-                  )}
-                  {(!isReadOnly && showEditorIds.has(comment._id)) && (
-                    <CommentEditor
-                      pageId={pageId}
-                      replyTo={comment._id}
-                      onCancelButtonClicked={() => {
-                        removeShowEditorId(comment._id);
-                      }}
-                      onCommentButtonClicked={() => onCommentButtonClickHandler(comment._id)}
-                      revisionId={revisionId}
-                    />
-                  )}
-                </div>
-              );
-
-            })}
-          </div>
+      <div className="page-comments">
+        <div className="page-comments-list" id="page-comments-list">
+          {commentsExceptReply.map((comment) => {
+
+            const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+            const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
+
+            let commentThreadClasses = '';
+            commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
+
+            return (
+              <div key={comment._id} className={commentThreadClasses}>
+                {commentElement(comment)}
+                {hasReply && replyCommentsElement(allReplies[comment._id])}
+                {(!isReadOnly && !showEditorIds.has(comment._id)) && (
+                  <div className="d-flex flex-row-reverse">
+                    <NotAvailableForGuest>
+                      <NotAvailableForReadOnlyUser>
+                        <Button
+                          data-testid="comment-reply-button"
+                          outline
+                          color="secondary"
+                          size="sm"
+                          className="btn-comment-reply"
+                          onClick={() => onReplyButtonClickHandler(comment._id)}
+                        >
+                          <i className="icon-fw icon-action-undo"></i> Reply
+                        </Button>
+                      </NotAvailableForReadOnlyUser>
+                    </NotAvailableForGuest>
+                  </div>
+                )}
+                {(!isReadOnly && showEditorIds.has(comment._id)) && (
+                  <CommentEditor
+                    pageId={pageId}
+                    replyTo={comment._id}
+                    onCancelButtonClicked={() => {
+                      removeShowEditorId(comment._id);
+                    }}
+                    onCommentButtonClicked={() => onCommentButtonClickHandler(comment._id)}
+                    revisionId={revisionId}
+                  />
+                )}
+              </div>
+            );
+
+          })}
         </div>
       </div>
+
       {!isReadOnly && (
         <DeleteCommentModal
           isShown={isDeleteConfirmModalShown}

+ 4 - 4
apps/app/src/components/PageCreateModal.jsx

@@ -281,16 +281,16 @@ const PageCreateModal = () => {
               <DropdownToggle id="template-type" caret>
                 {template == null && t('template.option_label.select')}
                 {template === 'children' && t('template.children.label')}
-                {template === 'decendants' && t('template.decendants.label')}
+                {template === 'descendants' && t('template.descendants.label')}
               </DropdownToggle>
               <DropdownMenu>
                 <DropdownItem onClick={() => onChangeTemplateHandler('children')}>
                   {t('template.children.label')} (_template)<br className="d-block d-md-none" />
                   <small className="text-muted text-wrap">- {t('template.children.desc')}</small>
                 </DropdownItem>
-                <DropdownItem onClick={() => onChangeTemplateHandler('decendants')}>
-                  {t('template.decendants.label')} (__template) <br className="d-block d-md-none" />
-                  <small className="text-muted">- {t('template.decendants.desc')}</small>
+                <DropdownItem onClick={() => onChangeTemplateHandler('descendants')}>
+                  {t('template.descendants.label')} (__template) <br className="d-block d-md-none" />
+                  <small className="text-muted">- {t('template.descendants.desc')}</small>
                 </DropdownItem>
               </DropdownMenu>
             </UncontrolledButtonDropdown>

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

@@ -60,7 +60,7 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
           onClick={onClickTemplateForDescendantsButtonHandler}
           type="button"
         >
-          {t('create_page_dropdown.template.decendants')}
+          {t('create_page_dropdown.template.descendants')}
         </button>
       </li>
     </ul>

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

@@ -168,8 +168,6 @@ export const PageCreateButton = React.memo((): JSX.Element => {
     }
   }, [currentPage, isLoading, router]);
 
-  // TODO: update button design
-  // https://redmine.weseek.co.jp/issues/132683
   return (
     <div
       className="d-flex flex-row"

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

@@ -201,7 +201,7 @@ export const Sidebar = (): JSX.Element => {
         </DrawerToggler>
       ) }
       { sidebarMode != null && !isDockMode() && <AppTitleOnSubnavigation /> }
-      <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
+      <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end flex-expand-vh-100`} data-testid="grw-sidebar">
         <ResizableContainer>
           { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }
           <SidebarHead />

+ 2 - 0
apps/app/src/interfaces/template.ts

@@ -0,0 +1,2 @@
+export type TargetType = 'children' | 'descendants';
+export type LabelType = '_template' | '__template';

+ 2 - 5
apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts

@@ -26,20 +26,17 @@ export const certifySharedPageAttachmentMiddleware = async(req: RequestToAllowSh
 
   const validReferer = validateReferer(referer);
   if (!validReferer) {
-    logger.info('invalid referer.');
     return next();
   }
 
-  logger.info('referer is valid.');
-
   const shareLink = await retrieveValidShareLinkByReferer(validReferer);
   if (shareLink == null) {
-    logger.info(`No valid ShareLink document found by the referer (${validReferer.referer}})`);
+    logger.warn(`No valid ShareLink document found by the referer (${validReferer.referer}})`);
     return next();
   }
 
   if (!(await validateAttachment(fileId, shareLink))) {
-    logger.info(`No valid ShareLink document found by the fileId (${fileId}) and referer (${validReferer.referer}})`);
+    logger.warn(`No valid ShareLink document found by the fileId (${fileId}) and referer (${validReferer.referer}})`);
     return next();
   }
 

+ 2 - 2
apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts

@@ -15,9 +15,9 @@ export const retrieveValidShareLinkByReferer = async(referer: ValidReferer): Pro
     return null;
   }
 
-  const shareLinkId = referer;
+  const { shareLinkId } = referer;
   const shareLink = await ShareLink.findOne({
-    id: shareLinkId,
+    _id: shareLinkId,
   });
   if (shareLink == null || shareLink.isExpired()) {
     logger.info(`ShareLink ('${shareLinkId}') is not found or has already expired.`);

+ 1 - 1
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts

@@ -2,7 +2,7 @@ import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:middlewares:certify-shared-file:validate-referer:retrieve-site-url');
+const logger = loggerFactory('growi:middlewares:certify-shared-page-attachment:validate-referer:retrieve-site-url');
 
 
 export const retrieveSiteUrl = (): URL | null => {

+ 5 - 2
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts

@@ -7,7 +7,7 @@ import { ValidReferer } from '../interfaces';
 import { retrieveSiteUrl } from './retrieve-site-url';
 
 
-const logger = loggerFactory('growi:middlewares:certify-shared-file:validate-referer');
+const logger = loggerFactory('growi:middlewares:certify-shared-page-attachment:validate-referer');
 
 
 export const validateReferer = (referer: string | undefined): ValidReferer | false => {
@@ -51,7 +51,10 @@ export const validateReferer = (referer: string | undefined): ValidReferer | fal
   // validate pathname
   // https://regex101.com/r/M5Bp6E/1
   const match = refererUrl.pathname.match(/^\/share\/(?<shareLinkId>[a-f0-9]{24})$/i);
-  if (match == null || match.groups?.shareLinkId == null) {
+  if (match == null) {
+    return false;
+  }
+  if (match.groups?.shareLinkId == null) {
     logger.warn(`The pathname ('${refererUrl.pathname}') is invalid.`, match);
     return false;
   }

+ 2 - 1
apps/app/src/styles/organisms/_wiki.scss

@@ -294,7 +294,7 @@
 
 // == Colors
 .wiki {
-  a {
+  a:not(.alert-link) {
     @extend .link-underline-opacity-25;
     @extend .link-underline-opacity-100-hover;
 
@@ -309,5 +309,6 @@
         var(--bs-link-opacity, 1)
       );
     }
+
   }
 }

+ 1 - 1
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts

@@ -303,7 +303,7 @@ context('Access to Template Editing Mode', () => {
     cy.getByTestid('open-page-template-modal-btn').filter(':visible').click({force: true});
     cy.getByTestid('page-template-modal').should('be.visible');
 
-    cy.getByTestid('template-button-decendants').click(({force: true}))
+    cy.getByTestid('template-button-descendants').click(({force: true}))
     cy.waitUntilSkeletonDisappear();
 
     cy.getByTestid('navbar-editor').should('be.visible').then(()=>{

+ 1 - 2
packages/core/scss/_flex-expand.scss

@@ -2,14 +2,12 @@
   display: flex;
   flex-direction: row;
   flex-grow: 1;
-  height: 100%;
 }
 
 .flex-expand-vert {
   display: flex;
   flex: 1;
   flex-direction: column;
-  height: 100%;
 }
 
 .flex-expand-vh-100 {
@@ -17,6 +15,7 @@
 
   .flex-expand-horiz,
   .flex-expand-vert {
+    height: 100%;
     overflow-y: auto;
   }
 }

+ 14 - 0
packages/core/scss/bootstrap/_variables-dark.scss

@@ -0,0 +1,14 @@
+$success-text-emphasis-dark:        mix(#fff, $success, 20%) !default;
+$info-text-emphasis-dark:           mix(#fff, $info, 20%) !default;
+$warning-text-emphasis-dark:        mix(#fff, $warning, 20%) !default;
+$danger-text-emphasis-dark:         mix(#fff, $danger, 20%) !default;
+
+$success-bg-subtle-dark:            mix($gray-900, $success, 85%) !default;
+$info-bg-subtle-dark:               mix($gray-900, $info, 85%) !default;
+$warning-bg-subtle-dark:            mix($gray-900, $warning, 85%) !default;
+$danger-bg-subtle-dark:             mix($gray-900, $danger, 85%) !default;
+
+$success-border-subtle-dark:        mix($gray-900, $success, 50%) !default;
+$info-border-subtle-dark:           mix($gray-900, $info, 50%) !default;
+$warning-border-subtle-dark:        mix($gray-900, $warning, 50%) !default;
+$danger-border-subtle-dark:         mix($gray-900, $danger, 50%) !default;

+ 22 - 0
packages/core/scss/bootstrap/_variables.scss

@@ -5,6 +5,28 @@
 // Variables should follow the `$component-state-property-size` formula for
 // consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.
 
+// Color system
+$success:       #4a9017 !default;
+$info:          #4689aa !default;
+$warning:       #c99818 !default;
+$danger:        #de4d4d !default;
+
+$success-text-emphasis:       mix($gray-900, $success, 30%) !default;
+$info-text-emphasis:          mix($gray-900, $info, 30%) !default;
+$warning-text-emphasis:       mix($gray-900, $warning, 30%) !default;
+$danger-text-emphasis:        mix($gray-900, $danger, 30%) !default;
+
+$success-bg-subtle:           mix(#fff, $success, 90%) !default;
+$info-bg-subtle:              mix(#fff, $info, 90%) !default;
+$warning-bg-subtle:           mix(#fff, $warning, 90%) !default;
+$danger-bg-subtle:            mix(#fff, $danger, 90%) !default;
+
+$success-border-subtle:       mix(#fff, $success, 70%) !default;
+$info-border-subtle:          mix(#fff, $info, 70%) !default;
+$warning-border-subtle:       mix(#fff, $warning, 70%) !default;
+$danger-border-subtle:        mix(#fff, $danger, 70%) !default;
+
+
 // Options
 //
 // Quickly modify global styling by enabling or disabling optional features.

+ 2 - 1
packages/core/scss/bootstrap/apply.scss

@@ -42,4 +42,5 @@
 @import 'bootstrap/scss/utilities/api';
 
 // override
-@import './override';
+@import './override/badge';
+@import './override/buttons';

+ 1 - 0
packages/core/scss/bootstrap/init.scss

@@ -2,6 +2,7 @@
 
 @import './theming/variables';
 @import './variables';
+@import './variables-dark';
 @import 'bootstrap/scss/variables';
 @import 'bootstrap/scss/variables-dark';
 

+ 59 - 0
packages/core/scss/bootstrap/mixins/_button-outline-variant.scss

@@ -0,0 +1,59 @@
+@mixin button-outline-variant-light(
+  $color,
+  $background: mix(#fff, $color, 90%),
+  $border: $color,
+  $hover-background: mix(#fff, $color, 85%),
+  $hover-border: $border,
+  $hover-color: $color,
+  $active-background: mix(#fff, $color, 70%),
+  $active-border: $border,
+  $active-color: $color,
+  $disabled-background: $background,
+  $disabled-border: $border,
+  $disabled-color: $color
+) {
+
+  --#{$prefix}btn-color: #{$color};
+  --#{$prefix}btn-bg: #{$background};
+  --#{$prefix}btn-border-color: #{$border};
+  --#{$prefix}btn-hover-color: #{$hover-color};
+  --#{$prefix}btn-hover-bg: #{$hover-background};
+  --#{$prefix}btn-hover-border-color: #{$hover-border};
+  --#{$prefix}btn-active-color: #{$active-color};
+  --#{$prefix}btn-active-bg: #{$active-background};
+  --#{$prefix}btn-active-border-color: #{$active-border};
+  --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow};
+  --#{$prefix}btn-disabled-color: #{$disabled-color};
+  --#{$prefix}btn-disabled-bg: #{$disabled-background};
+  --#{$prefix}btn-disabled-border-color: #{$disabled-border};
+}
+
+@mixin button-outline-variant-dark(
+  $color,
+  $background: mix($gray-900, $color, 85%),
+  $border: mix($gray-900, $color, 50%),
+  $hover-background: mix($gray-900, $color, 80%),
+  $hover-border: $border,
+  $hover-color: $color,
+  $active-background: mix($gray-900, $color, 65%),
+  $active-border: $border,
+  $active-color: $color,
+  $disabled-background: $background,
+  $disabled-border: $border,
+  $disabled-color: $color
+) {
+
+  --#{$prefix}btn-color: #{$color};
+  --#{$prefix}btn-bg: #{$background};
+  --#{$prefix}btn-border-color: #{$border};
+  --#{$prefix}btn-hover-color: #{$hover-color};
+  --#{$prefix}btn-hover-bg: #{$hover-background};
+  --#{$prefix}btn-hover-border-color: #{$hover-border};
+  --#{$prefix}btn-active-color: #{$active-color};
+  --#{$prefix}btn-active-bg: #{$active-background};
+  --#{$prefix}btn-active-border-color: #{$active-border};
+  --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow};
+  --#{$prefix}btn-disabled-color: #{$disabled-color};
+  --#{$prefix}btn-disabled-bg: #{$disabled-background};
+  --#{$prefix}btn-disabled-border-color: #{$disabled-border};
+}

+ 3 - 0
packages/core/scss/bootstrap/override/_badge.scss

@@ -0,0 +1,3 @@
+.badge {
+  @extend .rounded-pill;
+}

+ 21 - 0
packages/core/scss/bootstrap/override/_buttons.scss

@@ -0,0 +1,21 @@
+@import '../mixins/button-outline-variant';
+
+:root[data-bs-theme='light'] {
+  @each $color, $value in $theme-colors {
+    @if $color != 'primary' and $color != 'secondary' {
+      .btn-outline-#{$color} {
+        @include button-outline-variant-light($value);
+      }
+    }
+  }
+}
+
+:root[data-bs-theme='dark'] {
+  @each $color, $value in $theme-colors {
+    @if $color != 'primary' and $color != 'secondary' {
+      .btn-outline-#{$color} {
+        @include button-outline-variant-dark($value);
+      }
+    }
+  }
+}

+ 7 - 5
packages/core/scss/bootstrap/theming/_buttons.scss → packages/core/scss/bootstrap/theming/_buttons-dark.scss

@@ -1,15 +1,17 @@
+@import '../mixins/button-outline-variant';
+
 .btn-primary {
   @include button-variant($primary, $primary);
 }
 
-.btn-outline-primary {
-  @include button-outline-variant($primary);
-}
-
 .btn-secondary {
   @include button-variant($secondary, $secondary);
 }
 
+.btn-outline-primary {
+  @include button-outline-variant-dark($primary);
+}
+
 .btn-outline-secondary {
-  @include button-outline-variant($secondary);
+  @include button-outline-variant-dark($secondary);
 }

+ 17 - 0
packages/core/scss/bootstrap/theming/_buttons-light.scss

@@ -0,0 +1,17 @@
+@import '../mixins/button-outline-variant';
+
+.btn-primary {
+  @include button-variant($primary, $primary);
+}
+
+.btn-secondary {
+  @include button-variant($secondary, $secondary);
+}
+
+.btn-outline-primary {
+  @include button-outline-variant-light($primary);
+}
+
+.btn-outline-secondary {
+  @include button-outline-variant-light($secondary);
+}

+ 3 - 1
packages/core/scss/bootstrap/theming/apply.scss → packages/core/scss/bootstrap/theming/apply-dark.scss

@@ -1,5 +1,7 @@
+@import './root';
+@import './root-dark';
 @import './tables';
-@import './buttons';
+@import './buttons-dark';
 @import './pagination';
 @import './progress';
 @import './list-group';

+ 7 - 0
packages/core/scss/bootstrap/theming/apply-light.scss

@@ -0,0 +1,7 @@
+@import './root';
+@import './root-light';
+@import './tables';
+@import './buttons-light';
+@import './pagination';
+@import './progress';
+@import './list-group';

+ 0 - 1
packages/editor/vite.config.ts

@@ -8,7 +8,6 @@ import dts from 'vite-plugin-dts';
 
 
 const excludeFiles = [
-  '**/@types/*',
   '**/components/playground/*',
   '**/main.tsx',
   '**/vite-env.d.ts',

+ 2 - 6
packages/preset-themes/src/styles/default.scss

@@ -27,9 +27,7 @@
 
   @import '@growi/core/scss/bootstrap/init-stage-2';
 
-  @import '@growi/core/scss/bootstrap/theming/root';
-  @import '@growi/core/scss/bootstrap/theming/root-light';
-  @import '@growi/core/scss/bootstrap/theming/apply';
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
 
   --grw-wiki-link-color-rgb: var(--grw-highlight-800-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-highlight-900-rgb);
@@ -65,9 +63,7 @@
 
   @import '@growi/core/scss/bootstrap/init-stage-2';
 
-  @import '@growi/core/scss/bootstrap/theming/root';
-  @import '@growi/core/scss/bootstrap/theming/root-dark';
-  @import '@growi/core/scss/bootstrap/theming/apply';
+  @import '@growi/core/scss/bootstrap/theming/apply-dark';
 
   --grw-wiki-link-color-rgb: var(--grw-highlight-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-highlight-300-rgb);

+ 2 - 6
packages/preset-themes/src/styles/mono-blue.scss

@@ -27,9 +27,7 @@
 
   @import '@growi/core/scss/bootstrap/init-stage-2';
 
-  @import '@growi/core/scss/bootstrap/theming/root';
-  @import '@growi/core/scss/bootstrap/theming/root-light';
-  @import '@growi/core/scss/bootstrap/theming/apply';
+  @import '@growi/core/scss/bootstrap/theming/apply-light';
 
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-700-rgb);
@@ -64,9 +62,7 @@
 
   @import '@growi/core/scss/bootstrap/init-stage-2';
 
-  @import '@growi/core/scss/bootstrap/theming/root';
-  @import '@growi/core/scss/bootstrap/theming/root-dark';
-  @import '@growi/core/scss/bootstrap/theming/apply';
+  @import '@growi/core/scss/bootstrap/theming/apply-dark';
 
   --grw-wiki-link-color-rgb: var(--grw-primary-500-rgb);
   --grw-wiki-link-hover-color-rgb: var(--grw-primary-300-rgb);