Skip to content

Commit

Permalink
feat: add state editing in-context (#3286)
Browse files Browse the repository at this point in the history
  • Loading branch information
stepan662 authored Dec 13, 2023
1 parent bda8d85 commit a5fb570
Show file tree
Hide file tree
Showing 29 changed files with 1,407 additions and 297 deletions.
11 changes: 11 additions & 0 deletions e2e/cypress/common/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@ import { getDevUi } from './devUiTools';
export const getByAriaLabel = (label: string) => {
return getDevUi().find(`*[aria-label="${label}"]`);
};

export const gcyWithCustom = (
{ value, ...other }: { value: string; [key: string]: string },
options?: Parameters<typeof cy.get>[1]
) =>
cy.get(
`[data-cy="${value}"]${Object.entries(other)
.map(([key, value]) => `[data-cy-${key}="${value}"]`)
.join('')}`,
options
);
43 changes: 43 additions & 0 deletions e2e/cypress/common/simulateReqAndResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { components } from '../../../packages/web/src/ui/client/apiSchema.generated';
import { getDevUi } from './devUiTools';
import { openUI, visitWithApiKey } from './nextInternalCommon';
import testLanguages from './testLanguages';

export type ApiKeyPermissionsModel =
components['schemas']['ApiKeyPermissionsModel'];
export type ComplexEditKeyDto = components['schemas']['ComplexEditKeyDto'];

type Props = {
permissions: ApiKeyPermissionsModel;
inForm: () => void;
checkRequest?: (data: ComplexEditKeyDto) => void;
};

export const simulateReqAndResponse = ({
permissions,
inForm,
checkRequest,
}: Props) => {
const randomId = String(Math.random());
// simulating restricted languages in api key info (english only)
cy.intercept(
{ path: '/v2/api-keys/current-permissions', method: 'get' },
(req) => {
req.reply(permissions);
}
);
cy.intercept({ path: '/v2/projects/*/languages**', method: 'get' }, (req) => {
req.reply(testLanguages);
});
visitWithApiKey(permissions.scopes as any);
openUI();
inForm();
cy.intercept({ path: '/v2/projects/*/keys/**', method: 'put' }, (req) => {
checkRequest?.(req.body);
// response is not checked by the client
req.reply({});
}).as(randomId);

getDevUi().contains('Update').click();
return cy.wait(`@${randomId}`);
};
10 changes: 0 additions & 10 deletions e2e/cypress/common/testApiKey.ts

This file was deleted.

34 changes: 34 additions & 0 deletions e2e/cypress/common/testApiKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiKeyPermissionsModel } from './simulateReqAndResponse';

export const fullPermissions: ApiKeyPermissionsModel = {
projectId: 1,
viewLanguageIds: null,
translateLanguageIds: null,
stateChangeLanguageIds: null,
scopes: [
'keys.create',
'keys.edit',
'translations.view',
'translations.edit',
'translations.state-edit',
'screenshots.view',
'screenshots.delete',
'screenshots.upload',
],
};

export const translateEnglish: ApiKeyPermissionsModel = {
projectId: 1,
viewLanguageIds: null,
translateLanguageIds: [1000000001],
stateChangeLanguageIds: null,
scopes: ['translations.view', 'translations.edit', 'screenshots.view'],
};

export const changeStateEnglish: ApiKeyPermissionsModel = {
projectId: 1,
viewLanguageIds: null,
translateLanguageIds: null,
stateChangeLanguageIds: [1000000001],
scopes: ['translations.view', 'screenshots.view', 'translations.state-edit'],
};
185 changes: 121 additions & 64 deletions e2e/cypress/e2e/next-internal/ui.cy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { login } from '../../common/apiCalls';
import { getByAriaLabel } from '../../common/selectors';
import testApiKey from '../../common/testApiKey';
import testLanguages from '../../common/testLanguages';
import {
translateEnglish,
changeStateEnglish,
fullPermissions,
} from '../../common/testApiKeys';
import { openUI, visitWithApiKey } from '../../common/nextInternalCommon';
import { getDevUi } from '../../common/devUiTools';
import { simulateReqAndResponse } from '../../common/simulateReqAndResponse';

context('UI Dialog', () => {
beforeEach(() => {
Expand All @@ -27,75 +31,128 @@ context('UI Dialog', () => {
.should('contain', '/projects/1/translations/single?key=app-title');
});

it('updates translation properly', () => {
visitWithApiKey([
'translations.view',
'keys.edit',
'translations.edit',
'screenshots.view',
'screenshots.upload',
'screenshots.delete',
]);
it('disabled when view only', () => {
visitWithApiKey(['translations.view', 'screenshots.view']);
openUI();
assertCanEditEnglish();
getDevUi().contains('There are no screenshots.').should('be.visible');
getByAriaLabel('Take screenshot').should('not.exist');
getDevUi().contains('Update').should('be.disabled');
});

it('updates translation properly', () => {
simulateReqAndResponse({
permissions: fullPermissions,
inForm() {
getDevUi()
.find('textarea')
.contains('What To Pack')
.should('not.be.disabled');
getDevUi()
.find('textarea')
.contains('What To Pack')
.type('{selectAll}{del}Hello world', { force: true });
},
checkRequest(data) {
assert(data.translations['en'] === 'Hello world');
},
});
cy.contains('Hello world').should('be.visible');
});

it('updates translation properly when languages restricted', () => {
// simulating restricted languages in api key info (english only)
cy.intercept({ path: '/v2/api-keys/current**', method: 'get' }, (req) => {
req.reply(testApiKey);
simulateReqAndResponse({
permissions: translateEnglish,
inForm() {
getDevUi()
.find('textarea')
.contains('Was mitnehmen')
.should('be.disabled');
getDevUi()
.find('textarea')
.contains('What To Pack')
.should('not.be.disabled');
getDevUi()
.find('textarea')
.contains('What To Pack')
.type('{selectAll}{del}Hello world', { force: true });
},
checkRequest(data) {
assert(data.translations['en'] === 'Hello world');
},
});
cy.intercept(
{ path: '/v2/projects/*/languages**', method: 'get' },
(req) => {
req.reply(testLanguages);
}
);
visitWithApiKey([
'translations.view',
'translations.edit',
'screenshots.view',
]);
openUI();
getDevUi().find('textarea').contains('Was mitnehmen').should('be.disabled');
assertCanEditEnglish();
});

it('disabled when view only', () => {
visitWithApiKey(['translations.view', 'screenshots.view']);
openUI();
getDevUi().contains('There are no screenshots.').should('be.visible');
getByAriaLabel('Take screenshot').should('not.exist');
getDevUi().contains('Update').should('be.disabled');
it('updates state properly when languages restricted', () => {
simulateReqAndResponse({
permissions: changeStateEnglish,
inForm() {
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'de',
})
.should('be.disabled');
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'en',
})
.should('not.be.disabled');
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'en',
})
.click();
},
checkRequest(data) {
assert(data.states.en === 'REVIEWED', 'State changed correctly');
assert(
Object.values(data.states).length === 1,
'No other states touched'
);
assert(
Object.values(data.translations).length === 0,
'No translation changes'
);
},
});
});

function assertCanEditEnglish() {
getDevUi()
.find('textarea')
.contains('What To Pack')
.should('not.be.disabled');
getDevUi()
.find('textarea')
.contains('What To Pack')
.type('{selectAll}{del}Hello world', { force: true });
cy.intercept({ path: '/v2/projects/*/keys/**', method: 'put' }, (req) => {
req.reply({
body: {
id: 1000000706,
name: 'app-title',
translations: {
de: { id: 1000000806, text: 'Was mitnehmen', state: 'TRANSLATED' },
en: { id: 1000000817, text: 'Hello world', state: 'TRANSLATED' },
fr: { id: 1000000828, text: 'Quoi emballer', state: 'TRANSLATED' },
cs: { id: 1000000839, text: 'Co sbalit', state: 'TRANSLATED' },
},
tags: [],
screenshots: [],
},
});
}).as('updateTranslation');
getDevUi().contains('Update').click();
cy.wait('@updateTranslation');
getDevUi().contains('Hello world').should('be.visible');
}
it('updates state properly when languages restricted', () => {
simulateReqAndResponse({
permissions: changeStateEnglish,
inForm() {
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'de',
})
.should('be.disabled');
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'en',
})
.should('not.be.disabled');
getDevUi()
.findDcyWithCustom({
value: 'translation-state-button',
language: 'en',
})
.click();
},
checkRequest(data) {
assert(data.states.en === 'REVIEWED', 'State changed correctly');
assert(
Object.values(data.states).length === 1,
'No other states touched'
);
assert(
Object.values(data.translations).length === 0,
'No translation changes'
);
},
});
});
});
12 changes: 12 additions & 0 deletions e2e/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,15 @@ Cypress.Commands.add('gcy', (dataCy) => {
Cypress.Commands.add('findDcy', { prevSubject: true }, (subject, dataCy) => {
return subject.find('[data-cy="' + dataCy + '"]');
});

Cypress.Commands.add(
'findDcyWithCustom',
{ prevSubject: true },
(subject, { value, ...other }, options) =>
subject.find(
`[data-cy="${value}"]${Object.entries(other)
.map(([key, value]) => `[data-cy-${key}="${value}"]`)
.join('')}`,
options
)
);
5 changes: 5 additions & 0 deletions e2e/cypress/support/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ declare namespace Cypress {
gcy(dataCy: string): Chainable;

findDcy(dataCy: string): Chainable;

findDcyWithCustom(
params: { value: string; [key: string]: string },
options?: Parameters<typeof cy.get>[1]
): Chainable;
}
}
13 changes: 6 additions & 7 deletions packages/web/src/ui/KeyDialog/KeyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useDialogContext, useDialogActions } from './dialogContext';
import { NsSelect } from './NsSelect';
import { TOLGEE_RESTRICT_ATTRIBUTE } from '../../constants';
import { Tags } from './Tags/Tags';
import { usePermissions } from './dialogContext/usePermissions';

const ScContainer = styled('div')`
font-family: Rubik, Roboto, Arial;
Expand Down Expand Up @@ -78,6 +77,7 @@ const ScRestriction = styled('div')`
`;

const ScError = styled('div')`
padding-top: 16px;
color: red;
`;

Expand All @@ -98,10 +98,9 @@ export const KeyForm = () => {
const keyExists = useDialogContext((c) => c.keyExists);
const fallbackNamespaces = useDialogContext((c) => c.fallbackNamespaces);
const selectedNs = useDialogContext((c) => c.selectedNs);
const isAuthorizedTo = usePermissions();

const screenshotsView = isAuthorizedTo('screenshots.view');
const permissions = useDialogContext((c) => c.permissions);

const screenshotsView = permissions.canViewScreenshots;
const ready = !loading && !error;

return (
Expand Down Expand Up @@ -177,9 +176,9 @@ export const KeyForm = () => {
)}

{formDisabled && ready && (
<ScRestriction>{`Modification is restricted due to missing ${
keyData?.keyId !== undefined ? 'translations.edit' : 'keys.edit'
} scope in current api key settings.`}</ScRestriction>
<ScRestriction>
Modification is restricted due to missing permissions.
</ScRestriction>
)}

{error && <ScError>{error}</ScError>}
Expand Down
Loading

0 comments on commit a5fb570

Please sign in to comment.