Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] App request phase 2 #27797

Merged
merged 61 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
daba9be
refactor: :recycle: Allow end-users to access the marketplace and see…
rique223 Dec 12, 2022
040c6bf
refactor: :recycle: Refactor apps rest.js and rest-typings apps index.js
rique223 Dec 12, 2022
308570f
feat: :sparkles: Finish request flow first phase
rique223 Dec 13, 2022
ea3e440
Fix some tests
rique223 Dec 13, 2022
356a8d6
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Dec 13, 2022
923f7a8
Re-add necessary permission to deprecated apps endpoint
rique223 Dec 15, 2022
b848bd6
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Dec 15, 2022
1257edb
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Dec 18, 2022
f2854e3
Hide view logs button from end user
rique223 Jan 9, 2023
5c93160
fix: :bug: Hide app menu from end user
rique223 Jan 9, 2023
f091635
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
dougfabris Jan 9, 2023
ce295f7
Remove unnecessary of installed flag
rique223 Jan 9, 2023
c9a0748
Fix AppsModelList unit test
rique223 Jan 9, 2023
629088b
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 10, 2023
c43e880
Fix AppsModelList unit test
rique223 Jan 10, 2023
8b419e9
Merge branch 'feat/new-marketplace-request' of github.com:RocketChat/…
rique223 Jan 10, 2023
c7ac21d
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 10, 2023
7c66e31
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 11, 2023
2a84d8c
fix: :bug: Fix faulty boolean logic on AppsPageContent
rique223 Jan 12, 2023
e0da344
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 13, 2023
2a10f8b
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 17, 2023
bae5e4d
Reviews
rique223 Jan 17, 2023
6181ebf
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request
rique223 Jan 18, 2023
44ddc1b
refactor: :recycle: Remove unnecessary isMarkeptlace flag
rique223 Jan 20, 2023
0d32584
feat: :sparkles: Create App Requests stats endpoint and improve App r…
rique223 Jan 20, 2023
f12e385
feat: :sparkles: Retrive appRequestStats from /apps/marketplace/
rique223 Jan 20, 2023
3e4fde3
Merge remote-tracking branch 'origin' into feat/new-marketplace-reque…
rique223 Jan 20, 2023
42b27d0
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 20, 2023
6dcc02e
feat: :sparkles: Create new app request page and new app request badg…
rique223 Jan 23, 2023
abc14ea
Typecheck
rique223 Jan 24, 2023
9c53b38
feat: :sparkles: Create Requested status in app details page
rique223 Jan 24, 2023
eadc0d5
Refactor ListItem component to take loading in consideration
rique223 Jan 24, 2023
87d2c64
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 24, 2023
9c52562
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 24, 2023
072135c
fix: :bug: Fix useFilteredApps filtering wrong list of apps
rique223 Jan 25, 2023
f7e11c3
Merge branch 'feat/new-marketplace-request-phase-2' of github.com:Roc…
rique223 Jan 25, 2023
64c6ca7
feat: :construction: WIP: Create new app requests tab on app details …
rique223 Jan 26, 2023
c41567d
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 26, 2023
7372808
feat: :sparkles: Finish implementing app request flow
rique223 Jan 26, 2023
1386aff
Fix AppsModelList unit tests
rique223 Jan 26, 2023
77efd45
Merge branch 'feat/new-marketplace-request-phase-2' of github.com:Roc…
rique223 Jan 26, 2023
26764c2
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 26, 2023
7fb6688
Fix AdministrationList test
rique223 Jan 26, 2023
5919ce1
test: :white_check_mark: Add useAppRequestStats mock to AppsModelList…
rique223 Jan 27, 2023
d130ed9
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 27, 2023
8385e26
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 27, 2023
8e34326
feat: :construction: WIP: App request flow finishing touches
rique223 Jan 27, 2023
58c1545
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Jan 30, 2023
fd0303d
feat: :sparkles: Disable request buttons when current user already re…
rique223 Jan 30, 2023
ce8ddda
Merge branch 'feat/new-marketplace-request-phase-2' of github.com:Roc…
rique223 Jan 30, 2023
3464236
fix: :bug: Fix some loose ends of the request flow
rique223 Jan 31, 2023
6bc9781
feat: :construction: Create possible flow to mark app requests as seen
rique223 Feb 1, 2023
3640673
Fix AppsModelList test
rique223 Feb 2, 2023
6fb6a89
feat: :sparkles: Implement mark app request as seen flow
rique223 Feb 2, 2023
8ce8b3f
test: :white_check_mark: Create new tests for new AppsModelList reque…
rique223 Feb 3, 2023
7e680af
Fix AdministrationList tests
rique223 Feb 3, 2023
d603f67
Fix App request flow
rique223 Feb 3, 2023
4e56835
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Feb 3, 2023
40b1bda
Merge branch 'feat/new-marketplace' into feat/new-marketplace-request…
rique223 Feb 6, 2023
9c04bd6
fix: :bug: Fix infinity loop on mark app requests as seen call
rique223 Feb 6, 2023
e6f4755
Forbid non-admin users to see admin pages
rique223 Feb 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions apps/meteor/app/apps/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import type { AppScreenshot, AppRequestFilter, Pagination, IRestResponse, Serialized, AppRequest } from '@rocket.chat/core-typings';
import type { AppScreenshot, AppRequestFilter, Serialized, AppRequestsStats, PaginatedAppRequests } from '@rocket.chat/core-typings';

import type { App } from '../../../client/views/marketplace/types';
import { dispatchToastMessage } from '../../../client/lib/toast';
Expand Down Expand Up @@ -98,24 +98,26 @@ class AppClientOrchestrator {
throw new Error('Invalid response from API');
}

public async getAppsFromMarketplace(): Promise<App[]> {
const result = await APIClient.get('/apps/marketplace');
public async getAppsFromMarketplace(isAdminUser?: string): Promise<App[]> {
const result = await APIClient.get('/apps/marketplace', { isAdminUser });

if (!Array.isArray(result)) {
// TODO: chapter day: multiple results are returned, but we only need one
throw new Error('Invalid response from API');
}

return (result as App[]).map((app: App) => {
const { latest, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt, bundledIn } = app;
const { latest, appRequestStats, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt, bundledIn, requestedEndUser } = app;
return {
...latest,
appRequestStats,
price,
pricingPlans,
purchaseType,
isEnterpriseOnly,
modifiedAt,
bundledIn,
requestedEndUser,
};
});
}
Expand Down Expand Up @@ -246,26 +248,30 @@ class AppClientOrchestrator {

public async appRequests(
appId: string,
filter: AppRequestFilter,
sort: string,
pagination: Pagination,
): Promise<IRestResponse<AppRequest>> {
filter?: AppRequestFilter,
sort?: string,
limit?: number,
offset?: number,
): Promise<PaginatedAppRequests> {
try {
const response: IRestResponse<AppRequest> = await APIClient.get(
`/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`,
);
const response = await APIClient.get(`/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${limit}&offset=${offset}`);

const restResponse = {
data: response.data,
meta: response.meta,
};

return restResponse;
return response;
} catch (e: unknown) {
throw new Error('Could not get the list of app requests');
}
}

public async getAppRequestsStats(): Promise<AppRequestsStats> {
try {
const response = await APIClient.get('/apps/app-request/stats');

return response;
} catch (e: unknown) {
throw new Error('Could not get the app requests stats');
}
}

public async getCategories(): Promise<Serialized<ICategory[]>> {
const result = await APIClient.get('/apps/categories');

Expand Down
66 changes: 63 additions & 3 deletions apps/meteor/app/apps/server/communication/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,15 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${token}`;
}

const customQueryParams = new URLSearchParams();

if (this.queryParams.isAdminUser === 'false') {
customQueryParams.set('endUserID', this.user._id);
}

let result;
try {
result = HTTP.get(`${baseUrl}/v1/apps`, {
result = HTTP.get(`${baseUrl}/v1/apps?${customQueryParams.toString()}`, {
headers,
});
} catch (e) {
Expand Down Expand Up @@ -1087,11 +1093,11 @@ export class AppsRestApi {
}

try {
const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, {
const result = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, {
headers,
});

return API.v1.success({ data });
return API.v1.success(result.data);
} catch (e) {
orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message);

Expand All @@ -1100,5 +1106,59 @@ export class AppsRestApi {
},
},
);

this.api.addRoute(
'app-request/stats',
{ authRequired: true },
{
async get() {
const baseUrl = orchestrator.getMarketplaceUrl();
const headers = getDefaultHeaders();

const token = await getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

try {
const result = HTTP.get(`${baseUrl}/v1/app-request/stats`, { headers });

return API.v1.success(result.data);
} catch (e) {
orchestrator.getRocketChatLogger().error('Error getting the app requests stats from marketplace', e.message);

return API.v1.failure(e.message);
}
},
},
);

this.api.addRoute(
'app-request/markAsSeen',
{ authRequired: true },
{
async post() {
const baseUrl = orchestrator.getMarketplaceUrl();
const headers = getDefaultHeaders();

const token = await getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

const { unseenRequests } = this.bodyParams;

try {
const result = HTTP.post(`${baseUrl}/v1/app-request/markAsSeen`, { headers, data: { ids: unseenRequests } });

return API.v1.success(result.data);
} catch (e) {
orchestrator.getRocketChatLogger().error('Error marking app requests as seen', e.message);

return API.v1.failure(e.message);
}
},
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('AdministrationList', () => {
accountBoxItems={[{} as any]}
hasAuditPermission
hasAuditLogPermission
hasManageApps
hasAdminPermission
hasAuditLicense={false}
onDismiss={() => null}
Expand All @@ -50,6 +51,7 @@ describe('AdministrationList', () => {
hasAuditLicense={false}
hasAuditLogPermission={false}
hasAuditPermission={false}
hasManageApps={false}
accountBoxItems={[{} as any]}
onDismiss={() => null}
/>,
Expand All @@ -74,6 +76,7 @@ describe('AdministrationList', () => {
hasAuditLicense={false}
hasAuditLogPermission={false}
hasAuditPermission={false}
hasManageApps={false}
accountBoxItems={[{} as any]}
onDismiss={() => null}
/>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OptionDivider } from '@rocket.chat/fuselage';
import type { FC } from 'react';
import type { ReactElement } from 'react';
import React, { Fragment } from 'react';

import type { AccountBoxItem, IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox';
Expand All @@ -15,15 +15,17 @@ type AdministrationListProps = {
hasAuditLicense: boolean;
hasAuditPermission: boolean;
hasAuditLogPermission: boolean;
hasManageApps: boolean;
};

const AdministrationList: FC<AdministrationListProps> = ({
const AdministrationList = ({
accountBoxItems,
hasAuditPermission,
hasAuditLogPermission,
hasAdminPermission,
hasManageApps,
onDismiss,
}) => {
}: AdministrationListProps): ReactElement => {
const appBoxItems = accountBoxItems.filter((item): item is IAppAccountBoxItem => isAppAccountBoxItem(item));
const adminBoxItems = accountBoxItems.filter((item): item is AccountBoxItem => !isAppAccountBoxItem(item));
const showAudit = hasAuditPermission || hasAuditLogPermission;
Expand All @@ -32,7 +34,7 @@ const AdministrationList: FC<AdministrationListProps> = ({

const list = [
showAdmin && <AdministrationModelList showWorkspace={showWorkspace} accountBoxItems={adminBoxItems} onDismiss={onDismiss} />,
<AppsModelList appBoxItems={appBoxItems} onDismiss={onDismiss} />,
<AppsModelList appBoxItems={appBoxItems} onDismiss={onDismiss} appsManagementAllowed={hasManageApps} />,
showAudit && <AuditModelList showAudit={hasAuditPermission} showAuditLog={hasAuditLogPermission} onDismiss={onDismiss} />,
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,47 @@ describe('AppsModelList', () => {
'../../../app/ui-message/client/ActionManager': {
triggerActionButtonAction: {},
},
'../../views/marketplace/hooks/useAppRequestStats': {
useAppRequestStats: () => {
return {
isLoading: false,
data: {
data: {
totalUnseen: 5,
},
},
};
},
},
...stubs,
}).default;
};

it('should render apps', async () => {
it('should render all apps options when a user has manage apps permission', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={() => null} appBoxItems={[]} />);
render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed />);

expect(screen.getByText('Apps')).to.exist;
expect(screen.getByText('Marketplace')).to.exist;
expect(screen.getByText('Installed')).to.exist;
expect(screen.getByText('Requested')).to.exist;
});

it('should render only marketplace and installed options when a user does not have manage apps permission', async () => {
const AppsModelList = loadMock({
'@rocket.chat/ui-contexts': {
'useAtLeastOnePermission': (): boolean => false,
'@noCallThru': false,
},
});

render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed={false} />);

expect(screen.getByText('Apps')).to.exist;
expect(screen.getByText('Marketplace')).to.exist;
expect(screen.getByText('Installed')).to.exist;
expect(screen.queryByText('Requested')).to.not.exist;
});

context('when clicked', () => {
Expand All @@ -36,7 +65,7 @@ describe('AppsModelList', () => {
return <RouterContextMock pushRoute={pushRoute}>{children}</RouterContextMock>;
};

it('should go to admin marketplace', async () => {
it('should go to marketplace', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} />, { wrapper: ProvidersMock });
Expand All @@ -59,6 +88,18 @@ describe('AppsModelList', () => {
await waitFor(() => expect(handleDismiss).to.have.been.called());
});

it('should go to requested if user has manage apps permission', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} appsManagementAllowed />, { wrapper: ProvidersMock });

const button = screen.getByText('Requested');

userEvent.click(button);
await waitFor(() => expect(pushRoute).to.have.been.called.with('marketplace', { context: 'requested', page: 'list' }));
await waitFor(() => expect(handleDismiss).to.have.been.called());
});

it('should render apps and trigger action', async () => {
const triggerActionButtonAction = spy();

Expand All @@ -69,7 +110,7 @@ describe('AppsModelList', () => {
},
});

render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[{ name: 'Custom App' } as any]} />, {
render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[{ name: 'Custom App' } as any]} appsManagementAllowed />, {
wrapper: ProvidersMock,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import React from 'react';

import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager';
import type { IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox';
import { useAppRequestStats } from '../../views/marketplace/hooks/useAppRequestStats';
import ListItem from '../Sidebar/ListItem';

type AppsModelListProps = {
appBoxItems: IAppAccountBoxItem[];
appsManagementAllowed?: boolean;
onDismiss: () => void;
};

const AppsModelList = ({ appBoxItems, onDismiss }: AppsModelListProps): ReactElement => {
const AppsModelList = ({ appBoxItems, appsManagementAllowed, onDismiss }: AppsModelListProps): ReactElement => {
const t = useTranslation();
const marketplaceRoute = useRoute('marketplace');
const page = 'list';

const { data: appRequestStats, isLoading } = useAppRequestStats();

return (
<>
<OptionTitle>{t('Apps')}</OptionTitle>
Expand All @@ -38,6 +42,19 @@ const AppsModelList = ({ appBoxItems, onDismiss }: AppsModelListProps): ReactEle
onDismiss();
}}
/>

{appsManagementAllowed && (
<ListItem
icon='cube'
text={t('Requested')}
action={(): void => {
marketplaceRoute.push({ context: 'requested', page });
onDismiss();
}}
loading={isLoading}
notifications={appRequestStats?.data.totalUnseen ? appRequestStats?.data.totalUnseen : null}
/>
)}
</>
{appBoxItems.length > 0 && (
<>
Expand Down
Loading