Przeglądaj źródła

Merge branch 'master' into feat/156884-retrieve-the-source-of-the-knowledge-assistant's-response-results

Shun Miyazawa 1 rok temu
rodzic
commit
fab10b6c19

+ 37 - 0
.github/workflows/reusable-app-prod.yml

@@ -225,6 +225,7 @@ jobs:
       run: |
         pnpm playwright test --project=chromium/installer
       env:
+        DEBUG: pw:api
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright-installer
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
@@ -239,6 +240,7 @@ jobs:
       run: |
         pnpm playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
       env:
+        DEBUG: pw:api
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
@@ -253,10 +255,19 @@ jobs:
       run: |
         pnpm playwright test --project=${{ matrix.browser }}/guest-mode --shard=${{ matrix.shard }}
       env:
+        DEBUG: pw:api
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright-guest-mode
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
+    - name: Upload test results
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: blob-report-${{ matrix.shard }}
+        path: blob-report
+        retention-days: 30
+
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       if: failure()
@@ -266,3 +277,29 @@ jobs:
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  report-playwright:
+    needs: [run-playwright]
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'pnpm'
+
+    - name: Merge into HTML Report
+      run: pnpm playwright merge-reports --reporter html ./all-blob-reports
+
+    - name: Upload HTML report
+      uses: actions/upload-artifact@v4
+      with:
+        name: html-report
+        path: playwright-report
+        retention-days: 30

+ 6 - 1
apps/app/playwright.config.ts

@@ -48,7 +48,12 @@ export default defineConfig({
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: process.env.CI ? 'github' : 'list',
+  reporter: process.env.CI
+    ? [
+      ['github'],
+      ['blob'],
+    ]
+    : 'list',
 
   webServer: {
     command: 'pnpm run server',

+ 11 - 0
apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -80,6 +80,16 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
     router.push(link);
   }, [router]);
 
+  const itemSelectedByWheelClickHandler = useCallback((page: IPageForItem) => {
+    if (page.path == null || page._id == null) {
+      return;
+    }
+
+    const url = pathUtils.returnPathForURL(page.path, page._id);
+
+    window.open(url, '_blank');
+  }, []);
+
   const [, drag] = useDrag({
     type: 'PAGE_TREE',
     item: { page },
@@ -186,6 +196,7 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
       onClick={itemSelectedHandler}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}
+      onWheelClick={itemSelectedByWheelClickHandler}
       onRenamed={props.onRenamed}
       itemRef={itemRef}
       itemClass={PageTreeItem}

+ 16 - 1
apps/app/src/client/components/TreeItem/TreeItemLayout.tsx

@@ -28,7 +28,8 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
     indentSize = 10,
     itemLevel: baseItemLevel = 1,
     itemNode, targetPathOrId, isOpen: _isOpen = false,
-    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, isWipPageShown = true,
+    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, onWheelClick,
+    isEnableActions, isReadOnlyUser, isWipPageShown = true,
     itemRef, itemClass,
     showAlternativeContent,
   } = props;
@@ -51,6 +52,19 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
 
   }, [onClick, page]);
 
+  const itemMouseupHandler = useCallback((e: MouseEvent) => {
+    // DO NOT handle the event when e.currentTarget and e.target is different
+    if (e.target !== e.currentTarget) {
+      return;
+    }
+
+    if (e.button === 1) {
+      e.preventDefault();
+      onWheelClick?.(page);
+    }
+
+  }, [onWheelClick, page]);
+
 
   // descendantCount
   const { getDescCount } = usePageTreeDescCountMap();
@@ -141,6 +155,7 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
           border-0 py-0 ps-0 d-flex align-items-center rounded-1`}
         id={`grw-pagetree-list-${page._id}`}
         onClick={itemClickHandler}
+        onMouseUp={itemMouseupHandler}
         aria-current={isSelected ? true : undefined}
       >
 

+ 1 - 0
apps/app/src/client/components/TreeItem/interfaces/index.ts

@@ -34,4 +34,5 @@ export type TreeItemProps = TreeItemBaseProps & {
   showAlternativeContent?: boolean,
   customAlternativeComponents?: Array<React.FunctionComponent<TreeItemToolProps>>,
   onClick?(page: IPageForItem): void,
+  onWheelClick?(page: IPageForItem): void,
 };

+ 2 - 2
apps/app/src/server/routes/apiv3/app-settings.js

@@ -4,8 +4,8 @@ import { body } from 'express-validator';
 import { i18n } from '^/config/next-i18next.config';
 
 import { SupportedAction } from '~/interfaces/activity';
-import { getTranslation } from '~/server/service/i18next';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -569,7 +569,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to send test mail for smtp
    */
   router.post('/smtp-test', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
-    const { t } = await getTranslation();
+    const { t } = await getTranslation(req.user.lang);
 
     try {
       await sendTestEmail(req.user.email);

+ 1 - 1
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -1,7 +1,7 @@
 import { allOrigin } from '@growi/core';
 import type {
   IPage, IUser, IUserHasId,
-} from '@growi/core';
+} from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { isCreatablePage, isUserPage, isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { attachTitleHeader, normalizePath } from '@growi/core/dist/utils/path-utils';

+ 4 - 3
apps/app/src/server/routes/apiv3/page/index.ts

@@ -125,6 +125,7 @@ module.exports = (crowi) => {
       query('path').optional().isString(),
       query('findAll').optional().isBoolean(),
       query('shareLinkId').optional().isMongoId(),
+      query('includeEmpty').optional().isBoolean(),
     ],
     likes: [
       body('pageId').isString(),
@@ -209,7 +210,7 @@ module.exports = (crowi) => {
   router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
     const { user, isSharedPage } = req;
     const {
-      pageId, path, findAll, revisionId, shareLinkId,
+      pageId, path, findAll, revisionId, shareLinkId, includeEmpty,
     } = req.query;
 
     const isValid = (shareLinkId != null && pageId != null && path == null) || (shareLinkId == null && (pageId != null || path != null));
@@ -231,10 +232,10 @@ module.exports = (crowi) => {
         page = await Page.findByIdAndViewer(pageId, user);
       }
       else if (!findAll) {
-        page = await Page.findByPathAndViewer(path, user, null, true);
+        page = await Page.findByPathAndViewer(path, user, null, true, false);
       }
       else {
-        pages = await Page.findByPathAndViewer(path, user, null, false);
+        pages = await Page.findByPathAndViewer(path, user, null, false, includeEmpty);
       }
     }
     catch (err) {

+ 4 - 4
apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -932,7 +932,7 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/SamlAuthSetting'
    */
   router.put('/saml', loginRequiredStrictly, adminRequired, addActivity, validator.samlAuth, apiV3FormValidator, async(req, res) => {
-    const { t } = await getTranslation();
+    const { t } = await getTranslation(req.user.lang);
 
     //  For the value of each mandatory items,
     //  check whether it from the environment variables is empty and form value to update it is empty
@@ -943,11 +943,11 @@ module.exports = (crowi) => {
       const formValue = req.body[key];
       if (configManager.getConfigFromEnvVars('crowi', configKey) === null && formValue == null) {
         const formItemName = t(`security_setting.form_item_name.${key}`);
-        invalidValues.push(t('form_validation.required', formItemName));
+        invalidValues.push(t('input_validation.message.required', formItemName));
       }
     }
     if (invalidValues.length !== 0) {
-      return res.apiv3Err(t('form_validation.error_message'), 400, invalidValues);
+      return res.apiv3Err(t('input_validation.message.error_message'), 400, invalidValues);
     }
 
     const rule = req.body.ABLCRule;
@@ -958,7 +958,7 @@ module.exports = (crowi) => {
         crowi.passportService.parseABLCRule(rule);
       }
       catch (err) {
-        return res.apiv3Err(t('form_validation.invalid_syntax', t('security_settings.form_item_name.ABLCRule')), 400);
+        return res.apiv3Err(t('input_validation.message.invalid_syntax', t('security_settings.form_item_name.ABLCRule')), 400);
       }
     }
 

+ 1 - 1
apps/app/src/server/routes/login-passport.js

@@ -241,7 +241,7 @@ module.exports = function(crowi, app) {
    * @param {*} res
    */
   const testLdapCredentials = async(req, res) => {
-    const { t } = await getTranslation();
+    const { t } = await getTranslation(req.user.lang);
 
     if (!passportService.isLdapStrategySetup) {
       logger.debug('LdapStrategy has not been set up');

+ 5 - 4
apps/app/src/server/service/i18next.ts

@@ -14,7 +14,7 @@ import { configManager } from './config-manager';
 
 const relativePathToLocalesRoot = path.relative(__dirname, resolveFromRoot('public/static/locales'));
 
-const initI18next = async(lang: Lang = defaultLang) => {
+const initI18next = async(fallbackLng: Lang[] = [defaultLang]) => {
   const i18nInstance = createInstance();
   await i18nInstance
     .use(
@@ -26,7 +26,7 @@ const initI18next = async(lang: Lang = defaultLang) => {
     )
     .init({
       ...initOptions,
-      lng: lang,
+      fallbackLng,
     });
   return i18nInstance;
 };
@@ -38,10 +38,11 @@ type Translation = {
 
 export async function getTranslation(lang?: Lang): Promise<Translation> {
   const globalLang = configManager.getConfig('crowi', 'app:globalLang') as Lang;
-  const i18nextInstance = await initI18next(globalLang);
+  const fixedLang = lang ?? globalLang;
+  const i18nextInstance = await initI18next([fixedLang, defaultLang]);
 
   return {
-    t: i18nextInstance.getFixedT(lang ?? globalLang),
+    t: i18nextInstance.getFixedT(fixedLang),
     i18n: i18nextInstance,
   };
 }

+ 3 - 2
apps/app/src/stores/page-listing.tsx

@@ -22,9 +22,10 @@ import type {
 
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
   const findAll = true;
+  const includeEmpty = true;
   return useSWR(
-    path != null ? ['/page', path, findAll] : null,
-    ([endpoint, path, findAll]) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
+    path != null ? ['/page', path, findAll, includeEmpty] : null,
+    ([endpoint, path, findAll, includeEmpty]) => apiv3Get(endpoint, { path, findAll, includeEmpty }).then(result => result.data.pages),
   );
 };