mirror of
https://github.com/open-metadata/OpenMetadata
synced 2026-05-24 09:39:11 +00:00
Fixed the chinese character enoding issue (#25294)
* Fixed the chinese character enoding issue * Added playwright test * Fixed the chinese encoding issue * fixed unit test * fix code smell
This commit is contained in:
parent
ffabae1908
commit
b5b07093ba
5 changed files with 266 additions and 17 deletions
|
|
@ -10,14 +10,16 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, Page, test as base } from '@playwright/test';
|
||||
import { test as base, expect, Page } from '@playwright/test';
|
||||
import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass';
|
||||
import { DatabaseClass } from '../../support/entity/DatabaseClass';
|
||||
import { EntityDataClass } from '../../support/entity/EntityDataClass';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import { PersonaClass } from '../../support/persona/PersonaClass';
|
||||
import { UserClass } from '../../support/user/UserClass';
|
||||
import { REACTION_EMOJIS, reactOnFeed } from '../../utils/activityFeed';
|
||||
import { performAdminLogin } from '../../utils/admin';
|
||||
import { redirectToHomePage } from '../../utils/common';
|
||||
import { redirectToHomePage, uuid } from '../../utils/common';
|
||||
import {
|
||||
navigateToCustomizeLandingPage,
|
||||
setUserDefaultPersona,
|
||||
|
|
@ -593,3 +595,200 @@ test.describe('Mention notifications in Notification Box', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mentions: Chinese character encoding in activity feed', () => {
|
||||
const database = new DatabaseClass();
|
||||
const endpointName = `测试Endpoint-${uuid()}`;
|
||||
const apiEndpoint = new ApiEndpointClass(undefined, endpointName);
|
||||
let schemaFqn: string;
|
||||
const userName = `测试-${uuid()}`;
|
||||
|
||||
test.beforeAll('Create database, schema, and user with Chinese name', async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await performAdminLogin(browser);
|
||||
|
||||
await database.create(apiContext);
|
||||
await apiEndpoint.create(apiContext);
|
||||
await adminUser.create(apiContext);
|
||||
schemaFqn = database.schemaResponseData.fullyQualifiedName;
|
||||
const user = new UserClass({
|
||||
firstName: userName,
|
||||
lastName: '',
|
||||
email: `${userName}@example.com`,
|
||||
password: 'User@OMD123',
|
||||
});
|
||||
|
||||
await user.create(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await adminUser.login(page);
|
||||
await redirectToHomePage(page);
|
||||
});
|
||||
|
||||
test('Should allow mentioning a user with Chinese characters in the activity feed', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.slow();
|
||||
const feedPromise = page.waitForResponse((response) => {
|
||||
const url = response.url();
|
||||
return (
|
||||
url.includes('/api/v1/feed') &&
|
||||
url.includes('entityLink=') &&
|
||||
url.includes('type=Conversation') &&
|
||||
response.request().method() === 'GET'
|
||||
);
|
||||
});
|
||||
await page.goto(`/databaseSchema/${schemaFqn}/activity_feed/all`);
|
||||
const feedResponse = await feedPromise;
|
||||
expect(feedResponse.status()).toBe(200);
|
||||
await waitForAllLoadersToDisappear(page);
|
||||
|
||||
await page.getByTestId('comments-input-field').click();
|
||||
|
||||
const editorLocator = page.locator(
|
||||
'[data-testid="editor-wrapper"] [contenteditable="true"].ql-editor'
|
||||
);
|
||||
|
||||
await editorLocator.fill('Hey ');
|
||||
|
||||
await editorLocator.click();
|
||||
|
||||
await page.keyboard.press('@');
|
||||
const userSuggestionsResponse = page.waitForResponse((response) => {
|
||||
const url = response.url();
|
||||
|
||||
return (
|
||||
url.includes('/api/v1/search/query') &&
|
||||
url.includes(encodeURIComponent(userName))
|
||||
);
|
||||
});
|
||||
await editorLocator.pressSequentially(userName);
|
||||
await userSuggestionsResponse;
|
||||
|
||||
await page.locator(`[data-value="@${userName}"]`).first().click();
|
||||
|
||||
await expect(page.locator('[data-testid="send-button"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="send-button"]')
|
||||
).not.toBeDisabled();
|
||||
|
||||
const postMentionResponse = page.waitForResponse('/api/v1/feed/*/posts');
|
||||
await page.locator('[data-testid="send-button"]').click();
|
||||
await postMentionResponse;
|
||||
const replyCard = page
|
||||
.getByTestId('feed-reply-card')
|
||||
.filter({ hasText: `Hey @${userName}` });
|
||||
await expect(replyCard).toBeVisible();
|
||||
await expect(replyCard.getByTestId('viewer-container')).toHaveText(
|
||||
`Hey @${userName}`
|
||||
);
|
||||
const userMentionLink = replyCard.getByRole('link', {
|
||||
name: `@${userName}`,
|
||||
});
|
||||
await expect(userMentionLink).toBeVisible();
|
||||
await expect(userMentionLink).toHaveAttribute(
|
||||
'href',
|
||||
new RegExp(`/users/${userName}$`)
|
||||
);
|
||||
|
||||
const [newPage] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
userMentionLink.click(),
|
||||
]);
|
||||
|
||||
await newPage.waitForResponse((response) =>
|
||||
response.url().includes(`/api/v1/users/name/${encodeURIComponent(userName)}`)
|
||||
);
|
||||
|
||||
await waitForAllLoadersToDisappear(newPage);
|
||||
expect(newPage.getByTestId('user-display-name')).toHaveText(userName);
|
||||
});
|
||||
|
||||
|
||||
test('Should encode the chinese character while mentioning api endpoint', async ({ page }) => {
|
||||
test.slow();
|
||||
const feedPromise = page.waitForResponse((response) => {
|
||||
const url = response.url();
|
||||
return (
|
||||
url.includes('/api/v1/feed') &&
|
||||
url.includes('entityLink=') &&
|
||||
url.includes('type=Conversation') &&
|
||||
response.request().method() === 'GET'
|
||||
);
|
||||
});
|
||||
|
||||
await page.goto(`/databaseSchema/${schemaFqn}/activity_feed/all`);
|
||||
const feedResponse = await feedPromise;
|
||||
expect(feedResponse.status()).toBe(200);
|
||||
await waitForAllLoadersToDisappear(page);
|
||||
|
||||
await page.getByTestId('comments-input-field').click();
|
||||
|
||||
const editorLocator = page.locator(
|
||||
'[data-testid="editor-wrapper"] [contenteditable="true"].ql-editor'
|
||||
);
|
||||
|
||||
await editorLocator.fill('Check ');
|
||||
|
||||
await editorLocator.click();
|
||||
|
||||
await page.keyboard.press('#');
|
||||
const endpointSuggestionsResponse = page.waitForResponse((response) => {
|
||||
const url = response.url();
|
||||
return (
|
||||
url.includes('/api/v1/search/query') &&
|
||||
url.includes(encodeURIComponent(endpointName))
|
||||
);
|
||||
});
|
||||
|
||||
await editorLocator.pressSequentially(endpointName);
|
||||
await endpointSuggestionsResponse;
|
||||
|
||||
await page.locator(`[data-value="#apiEndpoint/${endpointName}"]`).first().click();
|
||||
|
||||
await expect(page.locator('[data-testid="send-button"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="send-button"]')
|
||||
).not.toBeDisabled();
|
||||
|
||||
const postMentionResponse = page.waitForResponse('/api/v1/feed/*/posts');
|
||||
await page.locator('[data-testid="send-button"]').click();
|
||||
await postMentionResponse;
|
||||
|
||||
const endpointFqn = apiEndpoint.entityResponseData.fullyQualifiedName;
|
||||
|
||||
const replyCard = page
|
||||
.getByTestId('feed-reply-card')
|
||||
.filter({ hasText: `Check #${endpointFqn}` });
|
||||
await expect(replyCard).toBeVisible();
|
||||
|
||||
await expect(replyCard.getByTestId('viewer-container')).toHaveText(
|
||||
`Check #${endpointFqn}`
|
||||
);
|
||||
|
||||
const endpointMentionLink = replyCard.getByRole('link', {
|
||||
name: endpointFqn,
|
||||
});
|
||||
await expect(endpointMentionLink).toBeVisible();
|
||||
await expect(endpointMentionLink).toHaveAttribute(
|
||||
'href',
|
||||
new RegExp(`/apiEndpoint/${endpointFqn}$`)
|
||||
);
|
||||
const [newPage] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
endpointMentionLink.click(),
|
||||
]);
|
||||
|
||||
await newPage.waitForResponse((response) =>
|
||||
response.url().includes('/api/v1/apiEndpoints/name/') &&
|
||||
response.request().method() === 'GET'
|
||||
);
|
||||
|
||||
await waitForAllLoadersToDisappear(newPage);
|
||||
|
||||
await expect(newPage.getByTestId('entity-header-display-name')).toHaveText(
|
||||
endpointName
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export class ApiEndpointClass extends EntityClass {
|
|||
entityResponseData: ResponseDataWithServiceType =
|
||||
{} as ResponseDataWithServiceType;
|
||||
|
||||
constructor(name?: string) {
|
||||
constructor(name?: string, apiEndpointName?: string) {
|
||||
super(EntityTypeEndpoint.API_ENDPOINT);
|
||||
|
||||
this.serviceName = name ?? `pw-api-service-${uuid()}`;
|
||||
|
|
@ -115,7 +115,7 @@ export class ApiEndpointClass extends EntityClass {
|
|||
service: this.service.name,
|
||||
};
|
||||
|
||||
this.apiEndpointName = `pw-api-endpoint-${uuid()}`;
|
||||
this.apiEndpointName = apiEndpointName ?? `pw-api-endpoint-${uuid()}`;
|
||||
this.fqn = `${this.service.name}.${this.apiCollection.name}.${this.apiEndpointName}.requestSchema`;
|
||||
|
||||
this.children = [
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const confirmStateInitialValue = {
|
|||
isThread: false,
|
||||
};
|
||||
|
||||
export const MENTION_ALLOWED_CHARS = /^[A-Za-z0-9_.-]*$/;
|
||||
export const MENTION_ALLOWED_CHARS = /^[^\s]*$/;
|
||||
export const MENTION_DENOTATION_CHARS = ['@', '#'];
|
||||
|
||||
export const TOOLBAR_ITEMS = [
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
getEntityType,
|
||||
getFeedHeaderTextFromCardStyle,
|
||||
getFieldOperationIcon,
|
||||
getFrontEndFormat,
|
||||
suggestions,
|
||||
} from './FeedUtils';
|
||||
|
||||
|
|
@ -45,8 +46,8 @@ jest.mock('../rest/searchAPI', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('./StringsUtils', () => ({
|
||||
getEncodedFqn: jest.fn().mockImplementation((fqn) => fqn),
|
||||
getDecodedFqn: jest.fn().mockImplementation((fqn) => fqn),
|
||||
getEncodedFqn: jest.fn().mockImplementation((fqn) => encodeURIComponent(fqn)),
|
||||
getDecodedFqn: jest.fn().mockImplementation((fqn) => decodeURIComponent(fqn)),
|
||||
}));
|
||||
|
||||
jest.mock('./FeedUtils', () => ({
|
||||
|
|
@ -108,8 +109,7 @@ describe('Feed Utils', () => {
|
|||
const message = `<#E::user::"admin.test"|[@admin.test](http://localhost:3000/users/%22admin.test%22)> test`;
|
||||
const result = getBackendFormat(message);
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const expectedResult = `<#E::user::\"admin.test\"|<#E::user::%22admin.test%22|[@admin.test](http://localhost:3000/users/%22admin.test%22)>> test`;
|
||||
const expectedResult = `<#E::user::"admin.test"|<#E::user::"admin.test"|[@admin.test](http://localhost:3000/users/%22admin.test%22)>> test`;
|
||||
|
||||
expect(result).toStrictEqual(expectedResult);
|
||||
});
|
||||
|
|
@ -262,3 +262,43 @@ describe('getFieldOperationIcon', () => {
|
|||
expect(stringResult).toContain(FieldOperation.Deleted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFrontEndFormat', () => {
|
||||
it('should return correct frontend format for user mention', () => {
|
||||
const message =
|
||||
'<#E::user::admin|[@admin](http://localhost:3000/users/admin)>';
|
||||
const result = getFrontEndFormat(message);
|
||||
|
||||
expect(result).toBe('[@admin](http://localhost:3000/users/admin)');
|
||||
});
|
||||
|
||||
it('should return correct frontend format for api endpoint with chinese characters', () => {
|
||||
const encodedFqn = 'pw-api-service.collection.%E6%B5%8B%E8%AF%95';
|
||||
const decodedFqn = 'pw-api-service.collection.测试';
|
||||
const message = `<#E::apiEndpoint::${encodedFqn}|[#apiEndpoint/${decodedFqn}](http://localhost:3000/apiEndpoint/${encodedFqn})>`;
|
||||
|
||||
const result = getFrontEndFormat(message);
|
||||
|
||||
expect(result).toBe(
|
||||
`[#apiEndpoint/${decodedFqn}](http://localhost:3000/apiEndpoint/${decodedFqn})`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct frontend format for user mention with encoded characters in URL', () => {
|
||||
const userName = '测试';
|
||||
const encodedName = encodeURIComponent(userName);
|
||||
const message = `<#E::user::${userName}|[@${userName}](http://localhost:3000/users/${encodedName})>`;
|
||||
|
||||
const result = getFrontEndFormat(message);
|
||||
|
||||
expect(result).toBe(
|
||||
`[@${userName}](http://localhost:3000/users/${userName})`
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle message without mentions', () => {
|
||||
const message = 'Hello world';
|
||||
|
||||
expect(getFrontEndFormat(message)).toBe('Hello world');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export const userMentionItemWithAvatar = (
|
|||
/>
|
||||
) : (
|
||||
<div
|
||||
className="flex-center flex-shrink align-middle mention-avatar"
|
||||
className="flex-center shrink align-middle mention-avatar"
|
||||
data-testid="avatar"
|
||||
style={{ backgroundColor: color }}>
|
||||
<span>{character}</span>
|
||||
|
|
@ -344,7 +344,18 @@ export const getFrontEndFormat = (message: string) => {
|
|||
(m) => getEntityLinkDetail(m) ?? []
|
||||
);
|
||||
entityLinkList.forEach((m, i) => {
|
||||
const markdownLink = entityLinkDetails[i][3];
|
||||
let markdownLink = entityLinkDetails[i]?.[3];
|
||||
const entityType = entityLinkDetails[i]?.[1];
|
||||
const entityFqn = entityLinkDetails[i]?.[2];
|
||||
const linkText = entityLinkDetails[i]?.[4];
|
||||
const entityUrl = entityLinkDetails[i]?.[5];
|
||||
|
||||
if (entityType && entityFqn && entityUrl) {
|
||||
const decodedUrl = getDecodedFqn(entityUrl);
|
||||
|
||||
markdownLink = `[${linkText}](${decodedUrl})`;
|
||||
}
|
||||
|
||||
updatedMessage = updatedMessage.replaceAll(m, markdownLink);
|
||||
});
|
||||
|
||||
|
|
@ -386,8 +397,7 @@ export const deletePost = async (
|
|||
if (isThread) {
|
||||
try {
|
||||
const data = await deleteThread(threadId);
|
||||
callback &&
|
||||
callback((prev) => prev.filter((thread) => thread.id !== data.id));
|
||||
callback?.((prev) => prev.filter((thread) => thread.id !== data.id));
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
|
|
@ -515,12 +525,12 @@ export const prepareFeedLink = (
|
|||
|
||||
const entityLink = entityUtilClassBase.getEntityLink(entityType, entityFQN);
|
||||
|
||||
if (!withoutFeedEntities.includes(entityType as EntityType)) {
|
||||
if (withoutFeedEntities.includes(entityType as EntityType)) {
|
||||
return entityLink;
|
||||
} else {
|
||||
const activityFeedLink = `${entityLink}/${TabSpecificField.ACTIVITY_FEED}`;
|
||||
|
||||
return subTab ? `${activityFeedLink}/${subTab}` : activityFeedLink;
|
||||
} else {
|
||||
return entityLink;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -580,7 +590,7 @@ export const entityDisplayName = (entityType: string, entityFQN: string) => {
|
|||
|
||||
// Remove quotes if the name is wrapped in quotes
|
||||
if (displayName) {
|
||||
displayName = displayName.replace(/(?:^"+)|(?:"+$)/g, '');
|
||||
displayName = displayName.replaceAll(/(?:^"+)|(?:"+$)/g, '');
|
||||
}
|
||||
|
||||
return displayName;
|
||||
|
|
|
|||
Loading…
Reference in a new issue