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

Merge branch 'master' into feat/sync-latest-revision-body-to-yjs-draft

Shun Miyazawa 1 год назад
Родитель
Сommit
a55b657a63
32 измененных файлов с 387 добавлено и 704 удалено
  1. 1 1
      .github/workflows/reusable-app-prod.yml
  2. 4 1
      apps/app/nodemon.json
  3. 129 1
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  4. 10 3
      apps/app/playwright/20-basic-features/presentation.spec.ts
  5. 86 0
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  6. 7 0
      apps/app/public/images/growi-brand-logo-login.svg
  7. 29 31
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  8. 3 3
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  9. 12 4
      apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  10. 1 1
      apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx
  11. 1 1
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx
  12. 3 1
      apps/app/src/client/components/PageComment/DeleteCommentModal.tsx
  13. 11 5
      apps/app/src/client/components/PageCreateModal.tsx
  14. 2 2
      apps/app/src/client/components/PageHeader/PageTitleHeader.tsx
  15. 1 1
      apps/app/src/client/components/PageTags/TagEditModal.tsx
  16. 1 1
      apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx
  17. 5 24
      apps/app/src/client/services/renderer/renderer.tsx
  18. 1 1
      apps/app/src/components/Layout/NoLoginLayout.module.scss
  19. 3 1
      apps/app/src/components/Layout/NoLoginLayout.tsx
  20. 11 1
      apps/app/src/features/rate-limiter/config/index.ts
  21. 2 1
      apps/app/src/features/rate-limiter/utils/config-generator.ts
  22. 21 2
      apps/app/src/pages/_document.page.tsx
  23. 11 6
      apps/app/src/pages/utils/commons.ts
  24. 1 19
      apps/app/src/server/models/activity.ts
  25. 0 9
      apps/app/src/server/models/obsolete-page.js
  26. 6 5
      apps/app/src/server/routes/apiv3/staffs.js
  27. 1 1
      apps/app/src/server/service/import.js
  28. 2 1
      apps/app/src/server/service/slack-command-handler/keep.js
  29. 3 2
      apps/app/src/server/service/slack-command-handler/togetter.js
  30. 19 17
      apps/app/src/services/renderer/renderer.tsx
  31. 0 339
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  32. 0 219
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -213,7 +213,7 @@ jobs:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['20', '21', '23', '30', '50']
+        spec-group: ['21', '23', '30', '50']
 
     services:
       mongodb:

+ 4 - 1
apps/app/nodemon.json

@@ -4,6 +4,9 @@
     ".next",
     "public/static",
     "package.json",
-    "playwright"
+    "playwright",
+    "test",
+    "test-with-vite",
+    "tmp"
   ]
 }

+ 129 - 1
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -1,4 +1,9 @@
-import { test, expect } from '@playwright/test';
+import { test, expect, type Page } from '@playwright/test';
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
 
 test('has title', async({ page }) => {
   await page.goto('/Sandbox');
@@ -14,9 +19,132 @@ test('get h1', async({ page }) => {
   await expect(page.getByRole('heading').filter({ hasText: /\/Sandbox/ })).toBeVisible();
 });
 
+test('/Sandbox/Math is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox/Math');
+
+  // Expect the Math-specific elements to be present
+  await expect(page.locator('.math').first()).toBeVisible();
+});
+
+test('Sandbox with edit is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox#edit');
+
+  // Expect the Editor-specific elements to be present
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await expect(page.getByTestId('save-page-btn')).toBeVisible();
+  await expect(page.getByTestId('grw-grant-selector')).toBeVisible();
+});
+
+test.describe.serial('PageEditor', () => {
+  const body1 = 'hello';
+  const body2 = ' world!';
+  const targetPath = '/Sandbox/testForUseEditingMarkdown';
+
+  test('Edit and save with save-page-btn', async({ page }) => {
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+    await appendTextToEditorUntilContains(page, body1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1);
+  });
+
+  test('Edit and save with shortcut key', async({ page }) => {
+    const savePageShortcutKey = 'Control+s';
+
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(body1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(body1);
+
+    await appendTextToEditorUntilContains(page, body1 + body2);
+    await page.keyboard.press(savePageShortcutKey);
+    await page.getByTestId('view-button').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1 + body2);
+  });
+});
+
 test('Access to /me page', async({ page }) => {
   await page.goto('/me');
 
   // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
+
+test('All In-App Notification list is successfully loaded', async({ page }) => {
+  await page.goto('/me/all-in-app-notifications');
+
+  // Expect the In-App Notification-specific elements to be present when accessing /me/all-in-app-notifications
+  await expect(page.getByTestId('grw-in-app-notification-page')).toBeVisible();
+});
+
+test('/trash is successfully loaded', async({ page }) => {
+  await page.goto('/trash');
+
+  await expect(page.getByTestId('trash-page-list')).toContainText('There are no pages under this page.');
+});
+
+test('/tags is successfully loaded', async({ page }) => {
+  await page.goto('/tags');
+
+  await expect(page.getByTestId('grw-tags-list')).toContainText('You have no tag, You can set tags on pages');
+});
+
+test.describe.serial('Access to Template Editing Mode', () => {
+  const templateBody1 = 'Template for children';
+  const templateBody2 = 'Template for descendants';
+
+  test('Successfully created template for children', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-children').click();
+
+    await appendTextToEditorUntilContains(page, templateBody1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody1);
+  });
+
+  test('Template is applied to pages created (template for children)', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody1);
+  });
+
+  test('Successfully created template for descendants', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-descendants').click();
+
+    await appendTextToEditorUntilContains(page, templateBody2);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody2);
+  });
+
+  test('Template is applied to pages created (template for descendants)', async({ page }) => {
+    await page.goto('/Sandbox/Bootstrap5');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody2);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody2);
+  });
+});

+ 10 - 3
apps/app/playwright/20-basic-features/presentation.spec.ts

@@ -11,11 +11,18 @@ test('Presentation', async({ page }) => {
   await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
     .toHaveText(/Welcome to GROWI/);
 
-  // forward the slide
-  await page.keyboard.press('ArrowRight');
+  // forward the slide with keyboard
   await page.keyboard.press('ArrowRight');
 
   // check the content of the h1
   await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
-    .toHaveText(/For administrator/);
+    .toHaveText(/What can you do with GROWI?/);
+
+  // forward the slide with button
+  await page.getByRole('application').getByLabel('next slide').click();
+
+  // check the content of the h2
+  await expect(page.getByRole('application').getByRole('heading', { level: 2 }))
+    .toHaveText(/1. Knowledge Management: Create pages to store information and knowledge/);
+
 });

+ 86 - 0
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -0,0 +1,86 @@
+import { test, expect, type Page } from '@playwright/test';
+
+const openPageItemControl = async(page: Page): Promise<void> => {
+  await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+};
+
+test('Page Deletion and PutBack is executed successfully', async({ page }) => {
+  await page.goto('/Sandbox/Bootstrap5');
+
+  // Delete
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-delete-modal-btn').click();
+  await expect(page.getByTestId('page-delete-modal')).toBeVisible();
+  await page.getByTestId('delete-page-button').click();
+
+  // PutBack
+  await expect(page.getByTestId('trash-page-alert')).toBeVisible();
+  await page.getByTestId('put-back-button').click();
+  await expect(page.getByTestId('put-back-page-modal')).toBeVisible();
+  await page.getByTestId('put-back-execution-button').click();
+  await expect(page.getByTestId('trash-page-alert')).not.toBeVisible();
+});
+
+test('PageDuplicateModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-duplicate-modal-btn').click();
+
+  await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
+});
+
+test('PageMoveRenameModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('rename-page-btn').click();
+
+  await expect(page.getByTestId('page-rename-modal')).toBeVisible();
+});
+
+// TODO: Uncomment after https://redmine.weseek.co.jp/issues/149786
+// test('PresentationModal for "/" is shown successfully', async({ page }) => {
+//   await page.goto('/');
+
+//   await openPageItemControl(page);
+//   await page.getByTestId('open-presentation-modal-btn').click();
+
+//   expect(page.getByTestId('page-presentation-modal')).toBeVisible();
+// });
+
+test.describe('Page Accessories Modal', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+    await openPageItemControl(page);
+  });
+
+  test('Page History is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-history-tab').click();
+    await expect(page.getByTestId(('page-history'))).toBeVisible();
+  });
+
+  test('Page Attachment Data is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-attachment-data-tab').click();
+    await expect(page.getByTestId('page-attachment')).toBeVisible();
+  });
+
+  test('Share Link Management is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+    await expect(page.getByTestId('share-link-management')).toBeVisible();
+  });
+});
+
+test('Successfully add new tag', async({ page }) => {
+  const tag = 'we';
+  await page.goto('/Sandbox/Bootstrap5');
+
+  await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
+  await expect(page.locator('#edit-tag-modal')).toBeVisible();
+  await page.locator('.rbt-input-main').fill(tag);
+  await expect(page.locator('#tag-typeahead-asynctypeahead-item-0')).toBeVisible();
+  await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
+  await page.getByTestId('tag-edit-done-btn').click();
+  await expect(page.getByTestId('grw-tag-labels')).toContainText(tag);
+});

+ 7 - 0
apps/app/public/images/growi-brand-logo-login.svg

@@ -0,0 +1,7 @@
+<svg viewBox="0 0 353 78" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M352.62 2.32007H338V75.4701H352.62V2.32007Z" fill="white"/>
+<path d="M293.71 75.4701L279.67 28.4501L265.56 75.4701H251.85L230.46 2.32007H245.76L258.94 50.6901L272.85 5.33007H286.81L300.39 50.4801L313.26 2.32007H328.81L308.26 75.4701H293.71Z" fill="white"/>
+<path d="M186.671 77.28C181.341 77.28 176.351 76.33 171.831 74.45C167.301 72.57 163.331 69.89 160.041 66.5C156.741 63.11 154.131 59.01 152.271 54.31C150.411 49.62 149.471 44.37 149.471 38.7C149.471 33.03 150.411 27.58 152.271 22.89C154.131 18.19 156.751 14.14 160.051 10.84C163.351 7.55001 167.321 4.97001 171.851 3.19001C176.371 1.41001 181.361 0.51001 186.681 0.51001C192.001 0.51001 197.101 1.41001 201.651 3.19001C206.221 4.97001 210.221 7.55001 213.541 10.84C216.881 14.14 219.511 18.19 221.371 22.89C223.231 27.58 224.171 32.9 224.171 38.7C224.171 44.5 223.231 49.62 221.371 54.31C219.511 59 216.881 63.1 213.551 66.5C210.221 69.89 206.221 72.57 201.661 74.45C197.101 76.33 192.061 77.28 186.681 77.28H186.671ZM186.761 13.62C183.401 13.62 180.321 14.27 177.621 15.54C174.911 16.82 172.541 18.61 170.601 20.84C168.651 23.09 167.121 25.75 166.051 28.77C164.981 31.8 164.431 35.13 164.431 38.69C164.431 42.25 164.971 45.8 166.051 48.86C167.121 51.91 168.661 54.59 170.651 56.83C172.631 59.07 175.001 60.83 177.711 62.08C180.421 63.33 183.461 63.96 186.761 63.96C190.061 63.96 193.131 63.33 195.871 62.08C198.611 60.83 201.001 59.07 202.981 56.83C204.961 54.59 206.511 51.91 207.571 48.86C208.641 45.79 209.191 42.37 209.191 38.69C209.191 35.01 208.641 31.81 207.571 28.77C206.501 25.75 204.971 23.09 203.021 20.84C201.081 18.61 198.701 16.82 195.961 15.54C193.221 14.26 190.131 13.61 186.771 13.61L186.761 13.62Z" fill="white"/>
+<path d="M123 75.4701L107.33 48.3101H98.8104V75.4701H84.1904V2.32007H109.9C113.31 2.32007 116.66 2.68007 119.87 3.40007C123.12 4.13007 126.05 5.34007 128.59 7.01007C131.16 8.70007 133.24 10.9401 134.79 13.6801C136.34 16.4301 137.13 21.5601 137.13 25.4701C137.13 32.6801 132.83 41.7601 122.21 46.3201L139.04 75.4801H123.01L123 75.4701ZM108.2 36.0101C109.96 36.0101 111.74 35.8801 113.49 35.6201C115.18 35.3701 116.75 34.9401 118.07 34.2101C120.83 32.6801 122.52 28.4301 122.52 25.4801C122.52 22.5301 121.06 19.0801 118.76 17.2001C117.09 15.8301 114.48 14.9401 109.31 14.9401H98.8204V36.0101H108.2Z" fill="white"/>
+<path d="M40.0404 77.28C34.1804 77.28 28.7504 76.33 23.9004 74.45C19.0404 72.57 14.8004 69.89 11.3104 66.5C7.81039 63.11 5.05039 59 3.09039 54.31C1.14039 49.62 0.150391 44.37 0.150391 38.7C0.150391 33.03 1.17039 27.57 3.20039 22.88C5.22039 18.18 8.04039 14.13 11.5704 10.84C15.1004 7.55002 19.3204 4.98002 24.1104 3.20002C28.8904 1.42002 34.1504 0.52002 39.7304 0.52002C45.3104 0.52002 50.8804 1.37002 55.6904 3.04002C60.5204 4.72002 64.4804 6.97002 67.4504 9.73002L68.1204 10.35L60.4204 23.7C58.7404 21 53.1704 17.01 50.0604 15.66C46.9504 14.31 43.5104 13.63 39.8404 13.63C36.1704 13.63 32.9404 14.28 30.0304 15.56C27.1204 16.84 24.6004 18.62 22.5604 20.86C20.5104 23.1 18.9004 25.77 17.7804 28.79C16.6504 31.82 16.0804 35.15 16.0804 38.71C16.0804 42.27 16.6504 45.82 17.7804 48.88C18.9004 51.93 20.5304 54.61 22.6104 56.85C24.6804 59.08 27.2404 60.85 30.2204 62.09C33.2004 63.34 36.6404 63.98 40.4504 63.98C45.4604 63.98 49.7604 63.29 53.5104 61.83V48.33H46.1004L38.9304 36.02H68.1204V71.4C64.4504 73.17 59.5604 74.83 55.1804 75.81C50.8004 76.79 45.7004 77.29 40.0304 77.29L40.0404 77.28Z" fill="white"/>
+</svg>

+ 29 - 31
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx

@@ -14,37 +14,35 @@ const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
   const { t } = useTranslation();
 
   return (
-    <div className="table-responsive">
-      <table className="table table-bordered">
-        <thead>
-          <tr>
-            <th>{t('admin:export_management.file')}</th>
-            <th>{t('admin:export_management.growi_version')}</th>
-            <th>{t('admin:export_management.collections')}</th>
-            <th>{t('admin:export_management.exported_at')}</th>
-            <th></th>
-          </tr>
-        </thead>
-        <tbody>
-          {props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
-            return (
-              <tr key={fileName}>
-                <th>{fileName}</th>
-                <td>{meta.version}</td>
-                <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
-                <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
-                <td>
-                  <ArchiveFilesTableMenu
-                    fileName={fileName}
-                    onZipFileStatRemove={props.onZipFileStatRemove}
-                  />
-                </td>
-              </tr>
-            );
-          })}
-        </tbody>
-      </table>
-    </div>
+    <table className="table table-bordered">
+      <thead>
+        <tr>
+          <th>{t('admin:export_management.file')}</th>
+          <th>{t('admin:export_management.growi_version')}</th>
+          <th>{t('admin:export_management.collections')}</th>
+          <th>{t('admin:export_management.exported_at')}</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        {props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
+          return (
+            <tr key={fileName}>
+              <th>{fileName}</th>
+              <td>{meta.version}</td>
+              <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
+              <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
+              <td>
+                <ArchiveFilesTableMenu
+                  fileName={fileName}
+                  onZipFileStatRemove={props.onZipFileStatRemove}
+                />
+              </td>
+            </tr>
+          );
+        })}
+      </tbody>
+    </table>
   );
 };
 

+ 3 - 3
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -13,11 +13,11 @@ const ArchiveFilesTableMenu = (props: ArchiveFilesTableMenuProps):JSX.Element =>
   const { t } = useTranslation();
 
   return (
-    <div className="btn-group admin-user-menu dropdown">
-      <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
+    <div className="dropdown">
+      <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
         <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
       </button>
-      <ul className="dropdown-menu" role="menu">
+      <ul className="dropdown-menu dropdown-menu-end">
         <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
         <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${props.fileName}` }}>
           <span className="material-symbols-outlined">cloud_download</span> {t('admin:export_management.download')}

+ 12 - 4
apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -14,11 +14,13 @@ const GROUPS_PAGE = [
 ];
 const GROUPS_USER = [
   'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-  'useruisettings', 'editorsettings', 'bookmarks', 'subscriptions',
+  'externalusergroups', 'externalusergrouprelations',
+  'useruisettings', 'editorsettings', 'bookmarks', 'bookmarkfolders', 'subscriptions',
   'inappnotificationsettings',
 ];
 const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
+  'configs', 'migrations', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
+  'growiplugins',
 ];
 const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
@@ -102,8 +104,14 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
 
     const html = t('admin:export_management.desc_password_seed');
 
-    // eslint-disable-next-line react/no-danger
-    return <div className="card custom-card bg-body-tertiary" dangerouslySetInnerHTML={{ __html: html }}></div>;
+    return (
+      <div className="card">
+        <div className="card-body">
+          {/* eslint-disable-next-line react/no-danger */}
+          <p className="card-text" dangerouslySetInnerHTML={{ __html: html }} />
+        </div>
+      </div>
+    );
   }, [selectedCollections, t]);
 
   const renderCheckboxes = useCallback((collectionNames, color?) => {

+ 1 - 1
apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx

@@ -14,7 +14,7 @@ import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 
 
 const IGNORED_COLLECTION_NAMES = [
-  'sessions', 'rlflx', 'activities',
+  'sessions', 'rlflx', 'activities', 'yjs-writings', 'transferkeys',
 ];
 
 const ExportArchiveDataPage = (): JSX.Element => {

+ 1 - 1
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -21,7 +21,7 @@ const DropdownItemContents = ({
 }) => (
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
-    <div className={`card custom-card mb-1 p-2 ${className}`} style={style}>{contents}</div>
+    <div className={`card mb-1 p-2 ${className}`} style={style}>{contents}</div>
   </>
 );
 /* eslint-enable react/prop-types */

+ 3 - 1
apps/app/src/client/components/PageComment/DeleteCommentModal.tsx

@@ -62,7 +62,9 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     return (
       <>
         <UserPicture user={creator} size="xs" /> <strong className="me-2"><Username user={creator}></Username></strong>{commentDate}:
-        <p className="card custom-card comment-body mt-2 p-2">{commentBodyElement}</p>
+        <div className="card mt-2">
+          <div className="card-body comment-body px-3 py-2">{commentBodyElement}</div>
+        </div>
       </>
     );
   };

+ 11 - 5
apps/app/src/client/components/PageCreateModal.tsx

@@ -2,8 +2,12 @@ import React, {
   useEffect, useState, useMemo, useCallback,
 } from 'react';
 
+import path from 'path';
+
+
 import { Origin } from '@growi/core';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
+import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import {
@@ -33,14 +37,13 @@ const PageCreateModal: React.FC = () => {
 
   const { data: pageCreateModalData, close: closeCreateModal } = usePageCreateModal();
 
-  const path = pageCreateModalData?.path;
   const isOpened = pageCreateModalData?.isOpened ?? false;
 
   const { create } = useCreatePage();
   const { createTemplate } = useCreateTemplatePage();
 
   const { data: isReachable } = useIsSearchServiceReachable();
-  const pathname = path || '';
+  const pathname = pageCreateModalData?.path ?? '';
   const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
   const isCreatable = isCreatablePage(pathname) || isUsersHomepage(pathname);
   const pageNameInputInitialValue = isCreatable ? pathUtils.addTrailingSlash(pathname) : '/';
@@ -106,16 +109,19 @@ const PageCreateModal: React.FC = () => {
    * access input page
    */
   const createInputPage = useCallback(async() => {
+    const targetPath = normalizePath(pageNameInput);
+    const parentPath = path.dirname(targetPath);
+
     return create(
       {
-        path: pageNameInput,
-        parentPath: pathname,
+        path: targetPath,
+        parentPath,
         wip: true,
         origin: Origin.View,
       },
       { onTerminated: closeCreateModal },
     );
-  }, [closeCreateModal, create, pageNameInput, pathname]);
+  }, [closeCreateModal, create, pageNameInput]);
 
   /**
    * access template page

+ 2 - 2
apps/app/src/client/components/PageHeader/PageTitleHeader.tsx

@@ -94,8 +94,8 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
 
   useEffect(() => {
     setEditedPagePath(currentPagePath);
-    if (isUntitledPage && editorMode === EditorMode.Editor) {
-      setRenameInputShown(true);
+    if (isUntitledPage != null) {
+      setRenameInputShown(isUntitledPage && editorMode === EditorMode.Editor);
     }
   }, [currentPage._id, currentPagePath, editorMode, isUntitledPage]);
 

+ 1 - 1
apps/app/src/client/components/PageTags/TagEditModal.tsx

@@ -59,7 +59,7 @@ const TagEditModalSubstance: React.FC<TagEditModalSubstanceProps> = (props: TagE
         <TagsInput tags={initTags} onTagsUpdated={tags => setTags(tags)} autoFocus />
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-primary" onClick={handleSubmit}>
+        <button type="button" data-testid="tag-edit-done-btn" className="btn btn-primary" onClick={handleSubmit}>
           {t('tag_edit_modal.done')}
         </button>
       </ModalFooter>

+ 1 - 1
apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx

@@ -17,7 +17,7 @@ const { encodeSpaces } = pagePathUtils;
 const DropdownItemContents = ({ title, contents }) => (
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
-    <div className="card custom-card mb-1 p-2">{contents}</div>
+    <div className="card mb-1 p-2">{contents}</div>
   </>
 );
 

+ 5 - 24
apps/app/src/client/services/renderer/renderer.tsx

@@ -21,7 +21,6 @@ import { RichAttachment } from '~/client/components/ReactMarkdownComponents/Rich
 import { TableWithEditButton } from '~/client/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
 import type { RendererOptions } from '~/interfaces/renderer-options';
-import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
@@ -30,7 +29,7 @@ import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
-  commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
+  getCommonSanitizeOption, generateCommonOptions, verifySanitizePlugin,
 } from '~/services/renderer/renderer';
 import loggerFactory from '~/utils/logger';
 
@@ -72,13 +71,9 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
-      commonSanitizeOption,
+      getCommonSanitizeOption(config),
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
@@ -134,14 +129,9 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   // add remark plugins
   // remarkPlugins.push();
 
-  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
-      commonSanitizeOption,
+      getCommonSanitizeOption(config),
     )]
     : () => {};
 
@@ -186,14 +176,9 @@ export const generateSimpleViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
-      commonSanitizeOption,
+      getCommonSanitizeOption(config),
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
@@ -280,13 +265,9 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     remarkPlugins.push(breaks);
   }
 
-  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
-      commonSanitizeOption,
+      getCommonSanitizeOption(config),
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,

+ 1 - 1
apps/app/src/components/Layout/NoLoginLayout.module.scss

@@ -25,7 +25,7 @@
     }
 
     .growi-logo-type {
-      letter-spacing: .5rem;
+      margin-left: 7px;
     }
 
   }

+ 3 - 1
apps/app/src/components/Layout/NoLoginLayout.tsx

@@ -1,6 +1,8 @@
 import type { ReactNode } from 'react';
 import React from 'react';
 
+import Image from 'next/image';
+
 import { useAppTitle } from '~/stores-universal/context';
 
 import GrowiLogo from '../Common/GrowiLogo';
@@ -37,7 +39,7 @@ export const NoLoginLayout = ({
               <div className="nologin-header mx-auto rounded-4 rounded-bottom-0">
                 <div className="d-flex justify-content-center align-items-center">
                   <GrowiLogo />
-                  <h1 className="growi-logo-type text-white fs-3 my-3 ms-3">GROWI</h1>
+                  <Image width={128.48} height={28} src="/images/growi-brand-logo-login.svg" alt="GROWI" className="growi-logo-type my-3" />
                 </div>
                 {appTitle !== 'GROWI' ? (
                   <h2 className="fs-4 text-center text-white">{ appTitle }</h2>

+ 11 - 1
apps/app/src/features/rate-limiter/config/index.ts

@@ -58,8 +58,18 @@ export const defaultConfig: IApiRateLimitEndpointMap = {
   },
 };
 
+const isDev = process.env.NODE_ENV === 'development';
+const defaultConfigWithRegExpForDev: IApiRateLimitEndpointMap = isDev ? {
+  '/__nextjs_original-stack-frame': {
+    method: 'GET',
+    maxRequests: Infinity,
+  },
+} : {};
+
 // default config with reg exp
-export const defaultConfigWithRegExp = {
+export const defaultConfigWithRegExp: IApiRateLimitEndpointMap = {
+  ...defaultConfigWithRegExpForDev,
+
   '/forgot-password/.*': {
     method: 'ALL',
     maxRequests: MAX_REQUESTS_TIER_1,

+ 2 - 1
apps/app/src/features/rate-limiter/utils/config-generator.ts

@@ -1,5 +1,6 @@
+import type { IApiRateLimitEndpointMap } from '../config';
 import {
-  defaultConfig, defaultConfigWithRegExp, IApiRateLimitEndpointMap,
+  defaultConfig, defaultConfigWithRegExp,
 } from '../config';
 
 const envVar = process.env;

+ 21 - 2
apps/app/src/pages/_document.page.tsx

@@ -1,6 +1,7 @@
 /* eslint-disable @next/next/google-font-display */
 import React from 'react';
 
+import { Lang } from '@growi/core';
 import type { DocumentContext, DocumentInitialProps } from 'next/document';
 import Document, {
   Html, Head, Main, NextScript,
@@ -8,8 +9,12 @@ import Document, {
 
 import type { GrowiPluginResourceEntries } from '~/features/growi-plugin/server/services';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import { configManager } from '~/server/service/config-manager';
+import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-utils';
 import loggerFactory from '~/utils/logger';
 
+import { getLocateAtServerSide } from './utils/commons';
+
 const logger = loggerFactory('growi:page:_document');
 
 type HeadersForGrowiPluginProps = {
@@ -41,14 +46,24 @@ interface GrowiDocumentProps {
   customCss: string | null,
   customNoscript: string | null,
   pluginResourceEntries: GrowiPluginResourceEntries;
+  locale: string;
 }
 declare type GrowiDocumentInitialProps = DocumentInitialProps & GrowiDocumentProps;
 
 class GrowiDocument extends Document<GrowiDocumentInitialProps> {
 
   static override async getInitialProps(ctx: DocumentContext): Promise<GrowiDocumentInitialProps> {
+
+    const langMap = {
+      [Lang.ja_JP]: 'ja-jp',
+      [Lang.en_US]: 'en-us',
+      [Lang.zh_CN]: 'zh-cn',
+      [Lang.fr_FR]: 'fr-fr',
+    } as const;
+
     const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
-    const { crowi } = ctx.req as CrowiRequest;
+    const req = ctx.req as CrowiRequest;
+    const { crowi } = req;
     const { customizeService } = crowi;
 
     const { themeHref } = customizeService;
@@ -60,6 +75,8 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const growiPluginService = await import('~/features/growi-plugin/server/services').then(mod => mod.growiPluginService);
     const pluginResourceEntries = await growiPluginService.retrieveAllPluginResourceEntries();
 
+    const locale = langMap[getLocateAtServerSide(req)];
+
     return {
       ...initialProps,
       themeHref,
@@ -67,6 +84,7 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
       customCss,
       customNoscript,
       pluginResourceEntries,
+      locale,
     };
   }
 
@@ -95,10 +113,11 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const {
       customCss, customScript, customNoscript,
       themeHref, pluginResourceEntries,
+      locale,
     } = this.props;
 
     return (
-      <Html>
+      <Html lang={locale}>
         <Head>
           {this.renderCustomScript(customScript)}
           <link rel="stylesheet" key="link-theme" href={themeHref} />

+ 11 - 6
apps/app/src/pages/utils/commons.ts

@@ -106,6 +106,15 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   return { props };
 };
 
+
+export const getLocateAtServerSide = (req: CrowiRequest): Lang => {
+  const { user, headers } = req;
+  const { configManager } = req.crowi;
+
+  return user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
+    : (user.lang ?? configManager.getConfig('crowi', 'app:globalLang') as Lang ?? Lang.en_US);
+};
+
 export const getNextI18NextConfig = async(
     // 'serverSideTranslations' method should be given from Next.js Page
     //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
@@ -115,13 +124,9 @@ export const getNextI18NextConfig = async(
     context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined, preloadAllLang = false,
 ): Promise<SSRConfig> => {
 
-  const req: CrowiRequest = context.req as CrowiRequest;
-  const { crowi, user, headers } = req;
-  const { configManager } = crowi;
-
   // determine language
-  const locale = user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
-    : (user.lang ?? configManager.getConfig('crowi', 'app:globalLang') as Lang ?? Lang.en_US);
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const locale = getLocateAtServerSide(req);
 
   const namespaces = ['commons'];
   if (namespacesRequired != null) {

+ 1 - 19
apps/app/src/server/models/activity.ts

@@ -1,4 +1,3 @@
-import type { Ref, IPage } from '@growi/core';
 import type {
   Types, Document, Model, SortOrder,
 } from 'mongoose';
@@ -15,10 +14,7 @@ import {
 } from '~/interfaces/activity';
 
 import loggerFactory from '../../utils/logger';
-import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
-
-
-import Subscription from './subscription';
+import { getOrCreateModel } from '../util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:models:activity');
@@ -34,8 +30,6 @@ export interface ActivityDocument extends Document {
   event: Types.ObjectId
   action: SupportedActionType
   snapshot: ISnapshot
-
-  getNotificationTargetUsers(): Promise<any[]>
 }
 
 export interface ActivityModel extends Model<ActivityDocument> {
@@ -98,18 +92,6 @@ activitySchema.post('save', function() {
   logger.debug('activity has been created', this);
 });
 
-activitySchema.methods.getNotificationTargetUsers = async function() {
-  const User = getModelSafely('User') || require('~/server/models/user')();
-  const { user: actionUser, target } = this;
-  const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);
-  const notificationUsers = subscribedUsers.filter(item => (item.toString() !== actionUser._id.toString()));
-  const activeNotificationUsers = await User.find({
-    _id: { $in: notificationUsers },
-    status: User.STATUS_ACTIVE,
-  }).distinct('_id');
-  return activeNotificationUsers;
-};
-
 activitySchema.statics.createByParameters = async function(parameters): Promise<IActivity> {
   const activity = await this.create(parameters) as unknown as IActivity;
 

+ 0 - 9
apps/app/src/server/models/obsolete-page.js

@@ -668,15 +668,6 @@ export const getPageSchema = (crowi) => {
     await this.updateMany({ _id: { $in: pages.map(p => p._id) } }, { grantedGroups: [transferToUserGroup] });
   };
 
-  pageSchema.methods.getNotificationTargetUsers = async function() {
-    const Revision = mongoose.model('Revision');
-
-    const [commentCreators, revisionAuthors] = await Promise.all([Comment.findCreatorsByPage(this), Revision.findAuthorsByPage(this)]);
-
-    const targetUsers = new Set([this.creator].concat(commentCreators, revisionAuthors));
-    return Array.from(targetUsers);
-  };
-
   pageSchema.statics.getHistories = function() {
     // TODO
 

+ 6 - 5
apps/app/src/server/routes/apiv3/staffs.js

@@ -1,13 +1,14 @@
+import axios from 'axios';
+import { addHours } from 'date-fns/addHours';
+import { isAfter } from 'date-fns/isAfter';
+import { Router } from 'express';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:staffs'); // eslint-disable-line no-unused-vars
 
-const express = require('express');
-
-const axios = require('axios');
 
-const router = express.Router();
-const { isAfter, addHours } = require('date-fns');
+const router = Router();
 
 const contributors = require('^/resource/Contributor');
 

+ 1 - 1
apps/app/src/server/service/import.js

@@ -3,6 +3,7 @@
  * @typedef {import("@types/unzip-stream").Entry} Entry
  */
 
+import { parseISO } from 'date-fns/parseISO';
 import gc from 'expose-gc/function';
 
 import loggerFactory from '~/utils/logger';
@@ -12,7 +13,6 @@ const path = require('path');
 const { Writable, Transform } = require('stream');
 
 const JSONStream = require('JSONStream');
-const parseISO = require('date-fns/parseISO');
 const isIsoDate = require('is-iso-date');
 const mongoose = require('mongoose');
 const streamToPromise = require('stream-to-promise');

+ 2 - 1
apps/app/src/server/service/slack-command-handler/keep.js

@@ -1,11 +1,12 @@
 import {
   inputBlock, actionsBlock, buttonElement, markdownSectionBlock,
 } from '@growi/slack/dist/utils/block-kit-builder';
+import { format } from 'date-fns/format';
+import { parse } from 'date-fns/parse';
 
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:service:SlackBotService:keep');
-const { parse, format } = require('date-fns');
 
 const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 

+ 3 - 2
apps/app/src/server/service/slack-command-handler/togetter.js

@@ -2,13 +2,14 @@ import {
   inputBlock, actionsBlock, buttonElement, markdownSectionBlock, divider,
 } from '@growi/slack/dist/utils/block-kit-builder';
 import { respond, deleteOriginal } from '@growi/slack/dist/utils/response-url';
+import { format, formatDate } from 'date-fns/format';
+import { parse } from 'date-fns/parse';
 
 import loggerFactory from '~/utils/logger';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:SlackBotService:togetter');
 
-const { parse, format } = require('date-fns');
 
 const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
@@ -163,7 +164,7 @@ module.exports = (crowi) => {
         // include header
         else {
           const ts = (parseInt(message.ts) - grwTzoffset) * 1000;
-          const time = format(new Date(ts), 'h:mm a');
+          const time = formatDate(new Date(ts), 'h:mm a');
           cleanedContents.push(`${message.user}  ${time}\n${message.text}\n`);
           lastMessage = message;
         }

+ 19 - 17
apps/app/src/services/renderer/renderer.tsx

@@ -38,22 +38,28 @@ const logger = loggerFactory('growi:services:renderer');
 
 type SanitizePlugin = PluginTuple<[SanitizeOption]>;
 
-export const commonSanitizeOption: SanitizeOption = {
-  tagNames: recommendedTagNames,
-  attributes: recommendedAttributes,
-  clobberPrefix: '', // remove clobber prefix
-};
-
-let isInjectedCustomSanitaizeOption = false;
+let currentInitializedSanitizeType: RehypeSanitizeType = RehypeSanitizeType.RECOMMENDED;
+let commonSanitizeOption: SanitizeOption;
+export const getCommonSanitizeOption = (config:RendererConfig): SanitizeOption => {
+  if (commonSanitizeOption == null || config.sanitizeType !== currentInitializedSanitizeType) {
+    // initialize
+    commonSanitizeOption = {
+      tagNames: config.sanitizeType === RehypeSanitizeType.RECOMMENDED
+        ? recommendedTagNames
+        : config.customTagWhitelist ?? recommendedTagNames,
+      attributes: config.sanitizeType === RehypeSanitizeType.RECOMMENDED
+        ? recommendedAttributes
+        : config.customAttrWhitelist ?? recommendedAttributes,
+      clobberPrefix: '', // remove clobber prefix
+    };
 
-export const injectCustomSanitizeOption = (config: RendererConfig): void => {
-  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    commonSanitizeOption.tagNames = config.customTagWhitelist ?? recommendedTagNames;
-    commonSanitizeOption.attributes = config.customAttrWhitelist ?? recommendedAttributes;
-    isInjectedCustomSanitaizeOption = true;
+    currentInitializedSanitizeType = config.sanitizeType;
   }
+
+  return commonSanitizeOption;
 };
 
+
 const isSanitizePlugin = (pluggable: Pluggable): pluggable is SanitizePlugin => {
   if (!Array.isArray(pluggable) || pluggable.length < 2) {
     return false;
@@ -131,13 +137,9 @@ export const generateSSRViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
-      commonSanitizeOption,
+      getCommonSanitizeOption(config),
     )]
     : () => {};
 

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

@@ -1,339 +0,0 @@
-function openEditor() {
-  cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
-  cy.waitUntil(() => {
-    // do
-    cy.get('@pageEditorModeManager').within(() => {
-      cy.get('button:nth-child(2)').click();
-    });
-    // until
-    return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
-  });
-  cy.get('.CodeMirror').should('be.visible');
-}
-
-context('Access to page', () => {
-  const ssPrefix = 'access-to-page-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  // TODO: https://redmine.weseek.co.jp/issues/109939
-  it('/Sandbox with anchor hash is successfully loaded', () => {
-    cy.visit('/Sandbox#headers');
-    cy.collapseSidebar(true);
-
-    // for check download toc data
-    // https://redmine.weseek.co.jp/issues/111384
-    // cy.get('.toc-link').should('be.visible');
-
-    // assert the element is in viewport
-    cy.get('#headers').should('be.inViewport');
-
-    // remove animation for screenshot
-    // remove 'blink' class because ::after element cannot be operated
-    // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
-    cy.get('#headers').invoke('removeClass', 'blink');
-
-    cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-sandbox-headers`);
-  });
-
-  it('/Sandbox/Math is successfully loaded', () => {
-    cy.visit('/Sandbox/Math');
-    cy.collapseSidebar(true);
-
-    // for check download toc data
-    // https://redmine.weseek.co.jp/issues/111384
-    // cy.get('.toc-link').should('be.visible');
-
-    cy.get('.math').should('be.visible');
-
-    cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-sandbox-math`);
-  });
-
-  it('/Sandbox with edit is successfully loaded', () => {
-    cy.visit('/Sandbox#edit');
-    cy.collapseSidebar(true);
-
-    cy.getByTestid('navbar-editor').should('be.visible');
-    cy.get('.grw-editor-navbar-bottom').should('be.visible');
-    cy.getByTestid('save-page-btn').should('be.visible');
-    cy.get('.grw-grant-selector').should('be.visible');
-
-    cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
-  })
-
-  const body1 = 'hello';
-  const body2 = ' world!';
-  it('Edit and save with save-page-btn', () => {
-    cy.visit('/Sandbox/testForUseEditingMarkdown');
-
-    openEditor();
-
-    // check edited contents after save
-    cy.appendTextToEditorUntilContains(body1);
-    cy.get('.page-editor-preview-body').should('contain.text', body1);
-    cy.getByTestid('page-editor').should('be.visible');
-    cy.getByTestid('save-page-btn').click();
-    cy.get('.wiki').should('be.visible');
-    cy.get('.wiki').children().first().should('have.text', body1);
-    cy.screenshot(`${ssPrefix}-edit-and-save-with-save-page-btn`);
-  })
-
-  it('Edit and save with shortcut key', () => {
-    const savePageShortcutKey = '{ctrl+s}';
-
-    cy.visit('/Sandbox/testForUseEditingMarkdown');
-
-    openEditor();
-
-    // check editing contents with shortcut key
-    cy.appendTextToEditorUntilContains(body2);
-    cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
-    cy.get('.CodeMirror').click().type(savePageShortcutKey);
-    cy.get('.CodeMirror-code').should('contain.text', body1+body2);
-    cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
-    cy.screenshot(`${ssPrefix}-edit-and-save-with-shortcut-key`);
-  })
-
-  it('/user/admin is successfully loaded', () => {
-    cy.visit('/user/admin');
-    cy.collapseSidebar(true);
-
-    // for check download toc data
-    // https://redmine.weseek.co.jp/issues/111384
-    // cy.get('.toc-link').should('be.visible');
-
-    // eslint-disable-next-line cypress/no-unnecessary-waiting
-    cy.wait(2000); // wait for calcViewHeight and rendering
-    cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-user-admin`);
-  });
-
-});
-
-
-context('Access to special pages', () => {
-  const ssPrefix = 'access-to-special-pages-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('/trash is successfully loaded', () => {
-    cy.visit('/trash');
-
-    cy.getByTestid('trash-page-list').should('contain.text', 'There are no pages under this page.');
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-trash`);
-  });
-
-  it('/tags is successfully loaded', { scrollBehavior: false } ,() => {
-    // open sidebar
-    // cy.collapseSidebar(false);
-
-    cy.visit('/tags');
-
-    // cy.getByTestid('grw-sidebar-content-tags').within(() => {
-    //   cy.getByTestid('grw-tags-list').should('be.visible');
-    //   cy.getByTestid('grw-tags-list').contains('You have no tag, You can set tags on pages');
-    // })
-
-    cy.getByTestid('tags-page').within(() => {
-      cy.getByTestid('grw-tags-list').should('be.visible');
-      cy.getByTestid('grw-tags-list').should('contain.text', 'You have no tag, You can set tags on pages');
-    });
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-tags`);
-  });
-
-});
-
-context('Access to Template Editing Mode', () => {
-  const ssPrefix = 'access-to-template-page-';
-  const templateBody1 = 'Template for children';
-  const templateBody2 = 'Template for descendants';
-
-  const createPageFromPageTreeTest = (newPagePath: string, parentPagePath: string, expectedBody: string) => {
-    cy.visit('/');
-    cy.waitUntilSkeletonDisappear();
-
-    // Open sidebar
-    cy.collapseSidebar(false);
-    cy.getByTestid('grw-sidebar-contents').should('be.visible');
-    cy.waitUntilSkeletonDisappear();
-
-    // If PageTree is not active when the sidebar is opened, make it active
-    cy.getByTestid('grw-sidebar-nav-primary-page-tree').should('be.visible')
-      .then($elem => {
-        if (!$elem.hasClass('active')) {
-          cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
-        }
-      });
-
-    // Create page (/{parentPath}}/{newPagePath}) from PageTree
-    cy.getByTestid('grw-sidebar-contents').within(() => {
-      cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
-        cy.get('#page-create-button-in-page-tree').first().click({force: true})
-      });
-    });
-    cy.get('@pagetreeItem').within(() => {
-      cy.getByTestid('autosize-submittable-input').type(newPagePath).type('{enter}');
-    })
-
-    cy.visit(`/${parentPagePath}/${newPagePath}`);
-    cy.collapseSidebar(true);
-
-    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
-    cy.waitUntilSkeletonDisappear();
-
-    // Check if the template is applied
-    cy.getByTestid('search-result-base').within(() => {
-      cy.get('.wiki').should('be.visible');
-      cy.get('.wiki').children().first().should('have.text', expectedBody);
-    })
-
-    cy.screenshot(`${ssPrefix}-page(${newPagePath})-to-which-template-is-applied`)
-  }
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it("Successfully created template for children", () => {
-    cy.visit('/Sandbox');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-template-modal-btn').filter(':visible').click({force: true});
-    cy.getByTestid('page-template-modal').should('be.visible');
-    cy.screenshot(`${ssPrefix}-open-page-template-modal`);
-
-    cy.getByTestid('template-button-children').click(({force: true}))
-    cy.waitUntilSkeletonDisappear();
-
-    cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
-      cy.url().should('include', '/_template#edit');
-      cy.screenshot(`${ssPrefix}-open-template-page-for-children-in-editor-mode`);
-    });
-
-    cy.appendTextToEditorUntilContains(templateBody1);
-    cy.get('.page-editor-preview-body').should('contain.text', templateBody1);
-    cy.getByTestid('page-editor').should('be.visible');
-    cy.getByTestid('save-page-btn').click();
-  });
-
-  it('Template is applied to pages created from PageTree (template for children 1)', () => {
-    createPageFromPageTreeTest('template-test-page1', '/Sandbox' ,templateBody1);
-  });
-
-  it('Successfully created template for descendants', () => {
-    cy.visit('/Sandbox');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // Wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-template-modal-btn').filter(':visible').click({force: true});
-    cy.getByTestid('page-template-modal').should('be.visible');
-
-    cy.getByTestid('template-button-descendants').click(({force: true}))
-    cy.waitUntilSkeletonDisappear();
-
-    cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
-      cy.url().should('include', '/__template#edit');
-      cy.screenshot(`${ssPrefix}-open-template-page-for-descendants-in-editor-mode`);
-    })
-
-    cy.appendTextToEditorUntilContains(templateBody2);
-    cy.get('.page-editor-preview-body').should('contain.text', templateBody2);
-    cy.getByTestid('page-editor').should('be.visible');
-    cy.getByTestid('save-page-btn').click();
-  });
-
-  it('Template is applied to pages created from PageTree (template for children 2)', () => {
-    createPageFromPageTreeTest('template-test-page2','Sandbox',templateBody1);
-  });
-
-  it('Template is applied to pages created from PageTree (template for descendants)', () => {
-    // delete /Sandbox/_template
-    cy.visit('/Sandbox/_template');
-
-    cy.waitUntil(() => {
-      //do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-delete-modal-btn').filter(':visible').click({force: true});
-
-    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-      cy.intercept('POST', '/_api/pages.remove').as('remove');
-      cy.getByTestid('delete-page-button').click();
-      cy.wait('@remove')
-    });
-
-    createPageFromPageTreeTest('template-test-page3','Sandbox',`${templateBody1}\n${templateBody2}`);
-  })
-});
-
-context('Access to /me/all-in-app-notifications', () => {
-  const ssPrefix = 'in-app-notifications-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('All In-App Notification list is successfully loaded', { scrollBehavior: false },() => {
-    cy.visit('/');
-    cy.get('.notification-wrapper').click();
-    cy.get('.notification-wrapper > .dropdown-menu > a').click();
-
-    cy.getByTestid('grw-in-app-notification-page').should('be.visible');
-    cy.getByTestid('grw-in-app-notification-page-spinner').should('not.exist');
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-see-all`);
-
-    cy.get('.grw-custom-nav-tab > div > ul > li:nth-child(2) > a').click();
-    cy.getByTestid('grw-in-app-notification-page-spinner').should('not.exist');
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-see-unread`);
-   });
-
-})

+ 0 - 219
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

@@ -1,219 +0,0 @@
-context('Modal for page operation', () => {
-
-  const ssPrefix = 'modal-for-page-operation-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('Page Deletion and PutBack is executed successfully', { scrollBehavior: false }, () => {
-    cy.visit('/Sandbox/Bootstrap5');
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      //wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-delete-modal-btn').filter(':visible').click({force: true});
-
-    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}-delete-modal`);
-      cy.getByTestid('delete-page-button').click();
-    });
-
-    cy.getByTestid('trash-page-alert').should('be.visible');
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-bootstrap5-is-in-garbage-box`);
-
-    cy.getByTestid('put-back-button').click();
-    cy.getByTestid('put-back-page-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}-put-back-modal`);
-      cy.getByTestid('put-back-execution-button').should('be.visible').click();
-    });
-
-    cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-put-backed-bootstrap5-page`);
-  });
-
-  it('PageDuplicateModal is shown successfully', () => {
-    cy.visit('/Sandbox/5');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('open-page-duplicate-modal-btn').filter(':visible').click({force: true});
-
-    cy.getByTestid('page-duplicate-modal').should('be.visible').screenshot(`${ssPrefix}-duplicate-bootstrap5`);
-  });
-
-  it('PageMoveRenameModal is shown successfully', () => {
-    cy.visit('/Sandbox/Bootstrap5');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      // wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-    cy.getByTestid('rename-page-btn').filter(':visible').click({force: true});
-    cy.getByTestid('grw-page-rename-button').should('be.disabled');
-
-    cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap5`);
-  });
-
-});
-
-
-// TODO: Uncomment after https://redmine.weseek.co.jp/issues/103121 is resolved
-// context('Open presentation modal', () => {
-
-//   const ssPrefix = 'access-to-presentation-modal-';
-
-//   beforeEach(() => {
-//     // login
-//     cy.fixture("user-admin.json").then(user => {
-//       cy.login(user.username, user.password);
-//     });
-//     cy.collapseSidebar(true);
-//   });
-
-//   it('PresentationModal for "/" is shown successfully', () => {
-//     cy.visit('/');
-
-//     cy.getByTestid('grw-contextual-sub-nav').within(() => {
-//       cy.getByTestid('open-page-item-control-btn').click({force: true});
-//       cy.getByTestid('open-presentation-modal-btn').click({force: true});
-//     });
-
-//     // eslint-disable-next-line cypress/no-unnecessary-waiting
-//     cy.wait(1500);
-//     cy.screenshot(`${ssPrefix}-open-top`);
-//   });
-
-// });
-
-context('Page Accessories Modal', () => {
-
-  const ssPrefix = 'access-to-page-accessories-modal';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-
-    cy.visit('/');
-    cy.collapseSidebar(true, true);
-    cy.waitUntilSkeletonDisappear();
-
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('grw-contextual-sub-nav').within(() => {
-        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
-      });
-      //wait until
-      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
-    });
-
-
-  });
-
-  it('Page History is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-history-tab').click({force: true});
-    });
-
-    cy.getByTestid('page-history').should('be.visible');
-
-    cy.waitUntilSpinnerDisappear();
-    cy.screenshot(`${ssPrefix}-open-page-history-bootstrap5`);
-  });
-
-  it('Page Attachment Data is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-attachment-data-tab').click({force: true});
-    });
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
-
-    cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap5`);
-  });
-
-  it('Share Link Management is shown successfully', () => {
-    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
-      cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').click({force: true});
-    });
-
-    cy.waitUntilSpinnerDisappear();
-    cy.getByTestid('page-accessories-modal').should('be.visible');
-    cy.getByTestid('share-link-management').should('be.visible');
-
-    cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap5`);
-  });
-});
-
-context('Tag Oprations', { scrollBehavior: false }, () =>{
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-  });
-
-  it('Successfully add new tag', () => {
-    const ssPrefix = 'tag-operations-add-new-tag-'
-    const tag = 'we';
-
-    cy.visit('/Sandbox/Bootstrap5');
-    cy.collapseSidebar(true);
-
-    // Add tag
-    cy.get('#edit-tags-btn-wrapper-for-tooltip').as('edit-tag-tooltip').should('be.visible');
-
-    // open Edit Tags Modal
-    cy.waitUntil(() => {
-      // do
-      cy.get('@edit-tag-tooltip').find('a').click({force: true});
-      // wait until
-      return cy.get('#edit-tag-modal').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#edit-tag-modal').should('be.visible').screenshot(`${ssPrefix}1-edit-tag-input`);
-
-    cy.get('#edit-tag-modal').should('be.visible').within(() => {
-      cy.get('.rbt-input-main').type(tag);
-      cy.get('#tag-typeahead-asynctypeahead').should('be.visible');
-      cy.get('#tag-typeahead-asynctypeahead-item-0').should('be.visible');
-      // select
-      cy.get('a#tag-typeahead-asynctypeahead-item-0').click();
-      // save
-      cy.get('div.modal-footer > button').click();
-    });
-
-    cy.get('.Toastify__toast').should('be.visible').trigger('mouseover');
-    cy.getByTestid('grw-tag-labels').contains(tag).should('exist');
-
-    cy.screenshot(`${ssPrefix}2-click-done`);
-  });
-
-});