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

Merge branch 'dev/5.0.x' into imprv/sort-pt-items

Taichi Masuyama 4 лет назад
Родитель
Сommit
d60b1dcb6c

+ 3 - 0
packages/app/resource/locales/en_US/translation.json

@@ -930,5 +930,8 @@
     "success_to_send_email": "Success to send email",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
+  },
+  "pagetree": {
+    "private_legacy_pages": "Private Legacy Pages"
   }
 }

+ 3 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -923,5 +923,8 @@
     "success_to_send_email": "メールを送信しました",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
+  },
+  "pagetree": {
+    "private_legacy_pages": "待避所"
   }
 }

+ 3 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -933,5 +933,8 @@
     "success_to_send_email": "我发了一封电子邮件",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
+  },
+  "pagetree": {
+    "private_legacy_pages": "私人遗留页面"
   }
 }

+ 1 - 1
packages/app/src/client/app.jsx

@@ -113,7 +113,7 @@ Object.assign(componentMappings, {
   'renamed-alert': <RenamedAlert />,
   'not-found-alert': <NotFoundAlert
     isGuestUserMode={appContainer.isGuestUser}
-    isHidden={!!(pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage)}
+    isHidden={pageContainer.state.pageId != null ? (pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage) : false} // !!DO NOT MOVE THIS!! https://github.com/weseek/growi/pull/4899
   />,
 });
 

+ 1 - 1
packages/app/src/components/Navbar/GlobalSearch.jsx

@@ -82,7 +82,7 @@ class GlobalSearch extends React.Component {
             dropup={dropup}
           />
           <div className="btn-group-submit-search">
-            <span className="btn-link text-decoration-none" onClick={this.search}>
+            <span role="button" className="btn-link text-decoration-none" onClick={this.search}>
               <i className="icon-magnifier"></i>
             </span>
           </div>

+ 1 - 1
packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx

@@ -34,7 +34,7 @@ const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
         id="check-all-pages"
         type="checkbox"
         name="check-all-pages"
-        className="custom-control custom-checkbox ml-1 align-self-center"
+        className="custom-control custom-checkbox align-self-center"
         disabled={props.isSelectAllCheckboxDisabled}
         onClick={onClickCheckbox}
         checked={selectAllCheckboxType !== CheckboxType.NONE_CHECKED}

+ 28 - 24
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -98,8 +98,8 @@ const SearchControl: FC <Props> = (props: Props) => {
         </div>
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
-      <div className="search-control d-flex align-items-center py-2 border-bottom border-gray">
-        <div className="d-flex mr-auto ml-4">
+      <div className="search-control d-flex align-items-center py-md-2 py-3 px-md-3 px-3 border-bottom border-gray">
+        <div className="d-flex pl-md-2">
           {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
           <DeleteSelectedPageGroup
             isSelectAllCheckboxDisabled={searchResultCount === 0}
@@ -109,7 +109,7 @@ const SearchControl: FC <Props> = (props: Props) => {
           />
         </div>
         {/** filter option */}
-        <div className="d-lg-none mr-4">
+        <div className="d-lg-none ml-auto">
           <button
             type="button"
             className="btn"
@@ -118,28 +118,32 @@ const SearchControl: FC <Props> = (props: Props) => {
             <i className="icon-equalizer"></i>
           </button>
         </div>
-        <div className="d-none d-lg-flex align-items-center mr-4">
-          <div className="border border-gray mr-3">
-            <label className="px-3 py-2 mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
-              <input
-                className="mr-2"
-                type="checkbox"
-                id="flexCheckDefault"
-                onClick={switchExcludeUserPagesHandler}
-              />
-              {t('Include Subordinated Target Page', { target: '/user' })}
-            </label>
+        <div className="d-none d-lg-flex align-items-center ml-auto search-control-include-options">
+          <div className="card mr-3 mb-0">
+            <div className="card-body">
+              <label className="search-include-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
+                <input
+                  className="mr-2"
+                  type="checkbox"
+                  id="flexCheckDefault"
+                  onClick={switchExcludeUserPagesHandler}
+                />
+                {t('Include Subordinated Target Page', { target: '/user' })}
+              </label>
+            </div>
           </div>
-          <div className="border border-gray">
-            <label className="px-3 py-2 mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckChecked">
-              <input
-                className="mr-2"
-                type="checkbox"
-                id="flexCheckChecked"
-                onClick={switchExcludeTrashPagesHandler}
-              />
-              {t('Include Subordinated Target Page', { target: '/trash' })}
-            </label>
+          <div className="card mb-0">
+            <div className="card-body">
+              <label className="search-include-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckChecked">
+                <input
+                  className="mr-2"
+                  type="checkbox"
+                  id="flexCheckChecked"
+                  onClick={switchExcludeTrashPagesHandler}
+                />
+                {t('Include Subordinated Target Page', { target: '/trash' })}
+              </label>
+            </div>
           </div>
         </div>
       </div>

+ 1 - 1
packages/app/src/components/SearchPage/SearchOptionModal.tsx

@@ -43,7 +43,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
         Search Option
       </ModalHeader>
       <ModalBody>
-        <div className="d-flex p-3">
+        <div className="d-flex p-2">
           <div className="border border-gray mr-3">
             <label className="px-3 py-2 mb-0 d-flex align-items-center">
               <input

+ 5 - 5
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -49,9 +49,9 @@ class SearchPageForm extends React.Component {
             onInputChange={this.onInputChange}
           />
           <div className="btn-group-submit-search">
-            <button
-              className="btn border-0 pb-1"
-              type="button"
+            <span
+              role="button"
+              className="text-decoration-none"
               onClick={() => {
                 try {
                   this.search();
@@ -61,8 +61,8 @@ class SearchPageForm extends React.Component {
                 }
               }}
             >
-              <i className="pr-2 icon-magnifier"></i>
-            </button>
+              <i className="icon-magnifier"></i>
+            </span>
           </div>
         </div>
       </div>

+ 5 - 7
packages/app/src/components/Sidebar/PageTree.tsx

@@ -64,13 +64,11 @@ const PageTree: FC = memo(() => {
         />
       </div>
 
-      <div className="grw-sidebar-content-footer">
-        {
-          !isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
-            <PrivateLegacyPages />
-          )
-        }
-      </div>
+      {!isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
+        <div className="grw-pagetree-footer border-top p-3 w-100">
+          <PrivateLegacyPages />
+        </div>
+      )}
     </>
   );
 });

+ 3 - 8
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -177,17 +177,12 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     }
   }, [data, isOpen]);
 
-  // TODO: improve style
-  const opacityStyle = { opacity: 1.0 };
-  if (page.isTarget) opacityStyle.opacity = 0.7;
-
-  const buttonClass = isOpen ? 'grw-pagetree-open' : '';
   return (
     <>
-      <div style={opacityStyle} className="grw-pagetree-item d-flex align-items-center">
+      <div className={`grw-pagetree-item d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}>
         <button
           type="button"
-          className={`grw-pagetree-button btn ${buttonClass}`}
+          className={`grw-pagetree-button btn ${isOpen ? 'grw-pagetree-open' : ''}`}
           onClick={onClickLoadChildren}
         >
           <div className="grw-triangle-icon">
@@ -211,7 +206,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
         </div>
       </div>
 
-      {!isEnableActions && (
+      {isEnableActions && (
         <ClosableTextInput
           isShown={isNewPageInputShown}
           placeholder={t('Input title')}

+ 3 - 5
packages/app/src/components/Sidebar/PageTree/PrivateLegacyPages.tsx

@@ -5,11 +5,9 @@ const PrivateLegacyPages: FC = memo(() => {
   const { t } = useTranslation();
 
   return (
-    <div className="grw-prvt-legacy-pages p-3">
-      <a href="/private-legacy-pages?q=[nq:PrivateLegacyPages]" className="h5">
-        <i className="icon-drawer mr-2"></i> PrivateLegacyPages
-      </a>
-    </div>
+    <a href="/private-legacy-pages?q=[nq:PrivateLegacyPages]" className="h5 grw-private-legacy-pages-anchor text-decoration-none">
+      <i className="icon-drawer mr-2"></i> {t('pagetree.private_legacy_pages')}
+    </a>
   );
 });
 

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

@@ -39,7 +39,7 @@ type TargetAndAncestorsResult = {
 }
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
-  createEmptyPagesByPaths(paths: string[]): Promise<void>
+  createEmptyPagesByPaths(paths: string[], publicOnly?: boolean): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean): Promise<PageDocument[]>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
@@ -141,9 +141,9 @@ const generateChildrenRegExp = (path: string): RegExp => {
 /*
  * Create empty pages if the page in paths didn't exist
  */
-schema.statics.createEmptyPagesByPaths = async function(paths: string[]): Promise<void> {
+schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicOnly = false): Promise<void> {
   // find existing parents
-  const builder = new PageQueryBuilder(this.find({}, { _id: 0, path: 1 }));
+  const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }));
   const existingPages = await builder
     .addConditionToListByPathsArray(paths)
     .query

+ 17 - 15
packages/app/src/server/service/page.js

@@ -866,35 +866,37 @@ class PageService {
   }
 
   async v5InitialMigration(grant) {
-    const socket = this.crowi.socketIoService.getAdminSocket();
-    try {
-      await this._v5RecursiveMigration(grant);
-    }
-    catch (err) {
-      logger.error('V5 initial miration failed.', err);
-      socket.emit('v5InitialMirationFailed', { error: err.message });
-
-      throw err;
-    }
-
+    // const socket = this.crowi.socketIoService.getAdminSocket();
     const Page = this.crowi.model('Page');
     const indexStatus = await Page.aggregate([{ $indexStats: {} }]);
     const pathIndexStatus = indexStatus.filter(status => status.name === 'path_1')?.[0];
     const isPathIndexExists = pathIndexStatus != null;
     const isUnique = isPathIndexExists && pathIndexStatus.spec?.unique === true;
 
+    // drop unique index first
     if (isUnique || !isPathIndexExists) {
       try {
         await this._v5NormalizeIndex(isPathIndexExists);
       }
       catch (err) {
         logger.error('V5 index normalization failed.', err);
-        socket.emit('v5IndexNormalizationFailed', { error: err.message });
+        // socket.emit('v5IndexNormalizationFailed', { error: err.message });
 
         throw err;
       }
     }
 
+    // then migrate
+    try {
+      await this._v5RecursiveMigration(grant, null, true);
+    }
+    catch (err) {
+      logger.error('V5 initial miration failed.', err);
+      // socket.emit('v5InitialMirationFailed', { error: err.message });
+
+      throw err;
+    }
+
     await this._setIsV5CompatibleTrue();
   }
 
@@ -933,7 +935,7 @@ class PageService {
   }
 
   // TODO: use websocket to show progress
-  async _v5RecursiveMigration(grant, regexps) {
+  async _v5RecursiveMigration(grant, regexps, publicOnly = false) {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
     const Page = this.crowi.model('Page');
@@ -996,7 +998,7 @@ class PageService {
         const parentPaths = Array.from(parentPathsSet);
 
         // fill parents with empty pages
-        await Page.createEmptyPagesByPaths(parentPaths);
+        await Page.createEmptyPagesByPaths(parentPaths, publicOnly);
 
         // find parents again
         const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }));
@@ -1071,7 +1073,7 @@ class PageService {
     await streamToPromise(migratePagesStream);
 
     if (await Page.exists(filter) && shouldContinue) {
-      return this._v5RecursiveMigration(grant, regexps);
+      return this._v5RecursiveMigration(grant, regexps, publicOnly);
     }
 
   }

+ 1 - 1
packages/app/src/server/views/admin/users.html

@@ -7,5 +7,5 @@
 {% endblock %}
 
 {% block content_main %}
-<div id ="admin-user-page"></div>
+<div id ="admin-user-page" class="admin-user-page"></div>
 {% endblock content_main %}

+ 5 - 1
packages/app/src/styles/_page-tree.scss

@@ -1,4 +1,9 @@
+$grw-sidebar-content-header-height: 58px;
+$grw-sidebar-content-footer-height: 50px;
+
 .grw-pagetree {
+  min-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
+
   .grw-pagetree-item {
     &:hover {
       opacity: 0.7;
@@ -29,7 +34,6 @@
     .grw-pagetree-title-anchor {
       width: 100%;
       overflow: hidden;
-      color: inherit;
       text-decoration: none;
 
       .grw-pagetree-title {

+ 20 - 7
packages/app/src/styles/_search.scss

@@ -31,8 +31,8 @@
 
   .search-clear {
     position: absolute;
-    top: 5px;
-    right: 4px;
+    top: 4px;
+    right: 26px;
     z-index: 3;
     width: 24px;
     height: 24px;
@@ -63,13 +63,18 @@
   }
 }
 
-// input styles
-.grw-global-search {
-  .search-clear {
-    top: 3px;
-    right: 26px;
+// styles for admin user search
+.admin-user-page {
+  .search-typeahead {
+    .search-clear {
+      top: 7px;
+      right: 4px;
+    }
   }
+}
 
+// input styles
+.grw-global-search {
   .dropdown-toggle {
     min-width: 95px;
     padding-left: 1.5rem;
@@ -166,6 +171,14 @@
 // TODO : keep the selected list in the same positino as other lists
 // TASK : https://redmine.weseek.co.jp/issues/82470
 .search-result {
+  .search-control {
+    padding: 5px 0;
+  }
+  .search-control-include-options {
+    .card-body {
+      padding: 5px 10px;
+    }
+  }
   .search-result-list {
     position: sticky;
     top: 0px;

+ 0 - 7
packages/app/src/styles/_sidebar.scss

@@ -235,13 +235,6 @@
       font-size: 18px;
     }
   }
-
-  .grw-sidebar-content-footer {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-    border-top: solid 1px $border-color;
-  }
 }
 
 // Dock Mode

+ 4 - 0
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -255,6 +255,10 @@ ul.pagination {
   // Pagetree
   .grw-pagetree {
     .grw-pagetree-item {
+      &.grw-pagetree-is-target {
+        background: $bgcolor-list-hover;
+      }
+
       .grw-triangle-icon {
         &:not(:hover) {
           svg {

+ 4 - 0
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -172,6 +172,10 @@ $border-color: $border-color-global;
   // Pagetree
   .grw-pagetree {
     .grw-pagetree-item {
+      &.grw-pagetree-is-target {
+        background: $bgcolor-list-hover;
+      }
+
       .grw-triangle-icon {
         &:not(:hover) {
           svg {

+ 13 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -310,6 +310,19 @@ ul.pagination {
     }
   }
 
+  .grw-pagetree {
+    .grw-pagetree-item {
+      .grw-pagetree-title-anchor {
+        color: inherit;
+      }
+    }
+  }
+  .grw-pagetree-footer {
+    .h5.grw-private-legacy-pages-anchor {
+      color: inherit;
+    }
+  }
+
   .grw-recent-changes {
     .list-group {
       .list-group-item {

+ 78 - 0
packages/app/src/test/integration/service/page.test.js

@@ -871,5 +871,83 @@ describe('PageService', () => {
 
   });
 
+  describe('v5InitialMigration()', () => {
+    test('should migrate all public pages & replace private parents with empty pages', async() => {
+      jest.restoreAllMocks();
+
+      // initialize pages for test
+      const pages = await Page.insertMany([
+        {
+          path: '/publicA',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/publicA/privateB',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/publicA/privateB/publicC',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/parenthesis/(a)[b]{c}d',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/parenthesis/(a)[b]{c}d/public',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+      ]);
+
+      const parent = await Page.find({ path: '/' });
+      await Page.insertMany([
+        {
+          path: '/migratedD',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+          parent: parent._id,
+        },
+      ]);
+
+      // migrate
+      await crowi.pageService.v5InitialMigration(Page.GRANT_PUBLIC);
+
+      const nMigratedPages = await Page.count({
+        path: {
+          $in: ['/publicA', '/publicA/privateB/publicC', '/parenthesis/(a)[b]{c}d', '/parenthesis/(a)[b]{c}d/public', '/migratedD'],
+        },
+        isEmpty: false,
+        parent: { $ne: null },
+      });
+      const nMigratedEmptyPages = await Page.count({
+        path: {
+          $in: ['/publicA/privateB', '/parenthesis'],
+        },
+        isEmpty: true,
+        parent: { $ne: null },
+      });
+      const nNonMigratedPages = await Page.count({
+        path: {
+          $in: ['/publicA/privateB'],
+        },
+        parent: null,
+      });
+
+      expect(nMigratedPages).toBe(5);
+      expect(nMigratedEmptyPages).toBe(2);
+      expect(nNonMigratedPages).toBe(1);
+    });
+  });
 
 });