Compare commits

...
Author SHA1 Message Date
Kirby Cofino bed277b8ea add custom multi-select that auto-submits when user selects next page 2022-09-23 09:44:01 -07:00
Kirby Cofino 81d56f6ebe [eas-cli] add paginated multi-select 2022-09-21 16:04:10 -07:00
Szymon Dziedzic 9f94ddb606 [eas-json] add Xcode 14 image (#1365)
* [eas-json] add XCode 14 image

* bump eas-build-job version

* fix eas.schema.json

* update default and latest images

* update eas-build-job package

* update CHANGELOG.md
2022-09-21 13:43:02 +02:00
Will Schurman 6b101614ba [eas-cli] Update changelog for recent series of stacked commits (#1377) 2022-09-19 15:54:15 -07:00
9 changed files with 255 additions and 25 deletions

No files matched your search

+2
View File
@@ -23,6 +23,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373), by [@wschurman](https://github.com/wschurman))
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
+2 -1
View File
@@ -13,7 +13,7 @@
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.90",
"@expo/eas-build-job": "0.2.94",
"@expo/eas-json": "2.1.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
@@ -37,6 +37,7 @@
"cli-progress": "3.11.2",
"cli-table3": "0.6.2",
"dateformat": "4.6.3",
"enquirer": "2.3.6",
"env-paths": "2.2.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
+57
View File
@@ -1,6 +1,10 @@
// @ts-ignore
import { MultiSelect } from 'enquirer';
import { constants } from 'os';
import prompts, { Answers, Choice, Options, PromptType, PromptObject as Question } from 'prompts';
import { PAGINATION_FETCH_MORE_VALUE } from './utils/queryConstants';
export { PromptType, Question, Choice };
export interface ExpoChoice<T> extends Choice {
@@ -93,3 +97,56 @@ export async function pressAnyKeyToContinueAsync(): Promise<void> {
});
});
}
class ExpoMultiSelect extends MultiSelect {
state: any;
selected: any;
styles: any;
run!: () => Promise<any>;
submit!: () => Promise<any>;
constructor(options = {}) {
super({ ...options, multiple: true });
}
toggle(choice: ExpoChoice<any>, enabled: boolean): ExpoChoice<any> | void {
super.toggle(choice, enabled);
if (choice?.value === PAGINATION_FETCH_MORE_VALUE) {
this.submit();
return;
}
return choice;
}
format(): string {
if (!this.state.submitted || this.state.cancelled) {
return '';
}
if (Array.isArray(this.selected)) {
return this.selected
.filter(choice => choice.value !== PAGINATION_FETCH_MORE_VALUE)
.map(choice => this.styles.primary(choice.name))
.join(', ');
}
return this.styles.primary(this.selected.name);
}
}
export async function multiselectAsync<T>(message: string, choices: ExpoChoice<T>[]): Promise<T[]> {
const result = await new ExpoMultiSelect({
message,
// prompt will mutate input
choices,
initial: choices.filter(x => x.selected),
}).run();
if (result && typeof choices === 'object' && choices.length > 0) {
return choices.filter(choice => result.includes(choice.title)).map(choice => choice.value);
}
return result ?? null;
}
@@ -1,11 +1,14 @@
import { confirmAsync, selectAsync } from '../../prompts';
import { confirmAsync, multiselectAsync, selectAsync } from '../../prompts';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithMultiSelectPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../queries';
import { PAGINATION_FETCH_MORE_VALUE } from '../queryConstants';
jest.mock('../../prompts', () => ({
selectAsync: jest.fn(),
multiselectAsync: jest.fn(),
confirmAsync: jest.fn(),
}));
@@ -19,7 +22,7 @@ const mockRenderMethod = jest.fn();
const selectMoreObject = {
title: 'Next page...',
value: '_fetchMore',
value: PAGINATION_FETCH_MORE_VALUE,
};
function createPaginatedQueryResponse(
@@ -118,8 +121,8 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
);
jest
.mocked(selectAsync)
.mockResolvedValueOnce(selectMoreObject.value)
.mockResolvedValueOnce(selectMoreObject.value)
.mockResolvedValueOnce(PAGINATION_FETCH_MORE_VALUE)
.mockResolvedValueOnce(PAGINATION_FETCH_MORE_VALUE)
.mockResolvedValueOnce(selectedItem.value);
const selectedQueryItem = await paginatedQueryWithSelectPromptAsync({
@@ -129,7 +132,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => 'item: ' + item.value,
createDisplayTextForSelectionPromptListItem: jest.fn(),
},
});
@@ -186,3 +189,96 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
expect(result).toBeFalsy();
});
});
describe(paginatedQueryWithMultiSelectPromptAsync.name, () => {
const mockHandleEmptySelectList = jest.fn();
beforeEach(() => {
mockQuery.mockClear();
mockRenderMethod.mockClear();
mockHandleEmptySelectList.mockClear();
jest.mocked(multiselectAsync).mockClear();
});
it('carries over user selections to new pages', async () => {
const selectionStates = [
['first-2', 'first-3', PAGINATION_FETCH_MORE_VALUE],
['first-2', 'second-1', PAGINATION_FETCH_MORE_VALUE],
['first-2', 'second-1', 'third-3'],
];
const limit = 5;
const offset = 0;
mockQuery
.mockResolvedValueOnce(createPaginatedQueryResponse(limit + 1, 'first'))
// we need to insert the last item from the previous query
// as the first item in the following query.
// the id is used for grouping items, so it must match an id in the previous query
.mockResolvedValueOnce(
createPaginatedQueryResponse(limit + 1, 'second', {
id: `first-${limit}`,
value: `first-${limit}`,
})
)
.mockResolvedValueOnce(
createPaginatedQueryResponse(4, 'third', {
id: `second-${limit}`,
value: `second-${limit}`,
})
);
jest
.mocked(multiselectAsync)
.mockResolvedValueOnce(selectionStates[0])
.mockResolvedValueOnce(selectionStates[1])
.mockResolvedValueOnce(selectionStates[2]);
const selectedQueryItems = await paginatedQueryWithMultiSelectPromptAsync({
limit,
offset,
queryToPerform: mockQuery,
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: jest.fn(),
},
});
expect(selectedQueryItems).toEqual(
selectionStates[2].map(selection => ({ id: selection, value: selection }))
);
});
it('does not fetch additional pages when "Next page..." is not selected', async () => {
const limit = 5;
const offset = 0;
mockQuery
.mockResolvedValueOnce(createPaginatedQueryResponse(limit + 1, 'first'))
// we need to insert the last item from the previous query
// as the first item in the following query.
// the id is used for grouping items, so it must match an id in the previous query
.mockResolvedValueOnce(
createPaginatedQueryResponse(limit + 1, 'second', {
id: `first-${limit}`,
value: `first-${limit}`,
})
);
jest
.mocked(multiselectAsync)
.mockResolvedValueOnce([`first-0`])
.mockResolvedValueOnce([`Fail this test`]);
const selectedQueryItems = await paginatedQueryWithMultiSelectPromptAsync({
limit,
offset,
queryToPerform: mockQuery,
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: jest.fn(),
},
});
expect(selectedQueryItems).toEqual([{ id: `first-0`, value: `first-0` }]);
});
});
+73 -11
View File
@@ -1,7 +1,6 @@
import { confirmAsync, selectAsync } from '../prompts';
import { ExpoChoice, confirmAsync, multiselectAsync, selectAsync } from '../prompts';
import uniqBy from './expodash/uniqBy';
const fetchMoreValue = '_fetchMore';
import { PAGINATION_FETCH_MORE_VALUE } from './queryConstants';
type BasePaginatedQueryArgs<QueryReturnType extends Record<string, any>> = {
limit: number;
@@ -69,10 +68,6 @@ async function paginatedQueryWithConfirmPromptInternalAsync<
}
}
/**
* Returns an array of item(s) where the id is equal to the id of the user's selected item
* If no items are available for a user to select, this will return an empty array.
*/
export async function paginatedQueryWithSelectPromptAsync<
QueryReturnType extends Record<string, any>
>(queryArgs: PaginatedQueryWithSelectPromptArgs<QueryReturnType>): Promise<QueryReturnType | void> {
@@ -99,23 +94,23 @@ async function paginatedQueryWithSelectPromptInternalAsync<
const selectionPromptListItems = uniqBy(newAccumulator, queryItem =>
promptOptions.getIdentifierForQueryItem(queryItem)
).map(queryItem => ({
).map<ExpoChoice<string>>(queryItem => ({
title: promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
value: promptOptions.getIdentifierForQueryItem(queryItem),
}));
if (areMorePagesAvailable) {
selectionPromptListItems.push({ title: 'Next page...', value: fetchMoreValue });
selectionPromptListItems.push({ title: 'Next page...', value: PAGINATION_FETCH_MORE_VALUE });
}
if (selectionPromptListItems.length === 0) {
return;
}
const valueOfUserSelectedListItem = await selectAsync<string>(
const valueOfUserSelectedListItem = await selectAsync(
promptOptions.title,
selectionPromptListItems
);
if (valueOfUserSelectedListItem === fetchMoreValue) {
if (valueOfUserSelectedListItem === PAGINATION_FETCH_MORE_VALUE) {
return await paginatedQueryWithSelectPromptInternalAsync(
{
limit,
@@ -131,3 +126,70 @@ async function paginatedQueryWithSelectPromptInternalAsync<
items => promptOptions.getIdentifierForQueryItem(items) === valueOfUserSelectedListItem
);
}
export async function paginatedQueryWithMultiSelectPromptAsync<
QueryReturnType extends Record<string, any>
>(
queryArgs: PaginatedQueryWithSelectPromptArgs<QueryReturnType>
): Promise<QueryReturnType[] | void> {
return await paginatedQueryWithMultiSelectPromptInternalAsync(queryArgs, [], []);
}
async function paginatedQueryWithMultiSelectPromptInternalAsync<
QueryReturnType extends Record<string, any>
>(
{
limit,
offset,
queryToPerform,
promptOptions,
}: PaginatedQueryWithSelectPromptArgs<QueryReturnType>,
queryItemAccumulator: QueryReturnType[],
selectedIdsAccumulator: string[]
): Promise<QueryReturnType[] | void> {
// query an extra item to determine if there are more pages left
const paginatedItems = await queryToPerform(limit + 1, offset);
const areMorePagesAvailable = paginatedItems.length > limit;
// drop that extra item used for pagination from our render logic
const currentPage = paginatedItems.slice(0, limit);
const newQueryItemAccumulator = [...queryItemAccumulator, ...currentPage];
const selectionPromptListItems = uniqBy(newQueryItemAccumulator, queryItem =>
promptOptions.getIdentifierForQueryItem(queryItem)
).map<ExpoChoice<string>>(queryItem => ({
title: promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
value: promptOptions.getIdentifierForQueryItem(queryItem),
selected: selectedIdsAccumulator.includes(promptOptions.getIdentifierForQueryItem(queryItem)),
}));
if (areMorePagesAvailable) {
selectionPromptListItems.push({ title: 'Next page...', value: PAGINATION_FETCH_MORE_VALUE });
}
if (selectionPromptListItems.length === 0) {
return;
}
const valueOfUserSelectedListItems = await multiselectAsync(
promptOptions.title,
selectionPromptListItems
);
const newSelectedIdsAccumulator = valueOfUserSelectedListItems.filter(
id => id !== PAGINATION_FETCH_MORE_VALUE
);
if (valueOfUserSelectedListItems.includes(PAGINATION_FETCH_MORE_VALUE)) {
return await paginatedQueryWithMultiSelectPromptInternalAsync(
{
limit,
offset: offset + limit,
queryToPerform,
promptOptions,
},
newQueryItemAccumulator,
newSelectedIdsAccumulator
);
}
return newQueryItemAccumulator.filter(queryItem =>
newSelectedIdsAccumulator.includes(promptOptions.getIdentifierForQueryItem(queryItem))
);
}
@@ -0,0 +1 @@
export const PAGINATION_FETCH_MORE_VALUE = '_fetchMore';
+1 -1
View File
@@ -6,7 +6,7 @@
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "0.2.90",
"@expo/eas-build-job": "0.2.94",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
+2 -3
View File
@@ -303,7 +303,7 @@
"enum": [
"default",
"latest",
"stable",
"macos-monterey-12.6-xcode-14.0",
"macos-monterey-12.4-xcode-13.4",
"macos-monterey-12.3-xcode-13.3",
"macos-monterey-12.1-xcode-13.2",
@@ -311,9 +311,8 @@
"macos-big-sur-11.4-xcode-12.5"
],
"markdownEnumDescriptions": [
"- Expo SDK `>=45` - `macos-monterey-12.3-xcode-13.3`\n- Expo SDK `<45` - `macos-big-sur-11.4-xcode-13.0`",
"`macos-monterey-12.4-xcode-13.4`",
"`macos-monterey-12.3-xcode-13.3`"
"`macos-monterey-12.6-xcode-14.0`"
]
},
{
Generated
+16 -4
View File
@@ -1343,10 +1343,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected]0":
version "0.2.90"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.90.tgz#9d2b25a79d44cb9532b08a67caa13cb9529a9deb"
integrity sha512-gCWcfSsg4O+Ua1xt6JA2Himzlj1LBes8mEhbcm1vu1kgg15QP62Aer4ck/B2vxiox9ZqYxK2ZvEdyZ7SI+GNGQ==
"@expo/[email protected]4":
version "0.2.94"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.94.tgz#05111ebeaf9a841c3eb778023dcde272af3fa48c"
integrity sha512-+tig8SczdGfjk2Tma5cVmESkx0bPBmR6dan2Bp5ITai3nhl8JN9mNZfEJPaBPCDlUe37sHvJwosh3GMfcq7QYg==
dependencies:
joi "^17.4.2"
semver "^7.3.5"
@@ -4191,6 +4191,11 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-colors@^4.1.1:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
ansi-escapes@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
@@ -5961,6 +5966,13 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
[email protected]:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
dependencies:
ansi-colors "^4.1.1"
[email protected], env-paths@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"