diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 4ea805f0b2e3..9e80c4f302a6 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -24,8 +24,10 @@ "dependencies": { "@babel/cli": "7.21.0", "@babel/core": "7.21.0", + "@babel/parser": "7.21.2", "@babel/plugin-transform-typescript": "7.21.0", "@babel/runtime-corejs3": "7.21.0", + "@babel/traverse": "7.21.2", "@iarna/toml": "2.2.5", "@vscode/ripgrep": "1.14.2", "@whatwg-node/fetch": "0.8.1", @@ -34,6 +36,7 @@ "execa": "5.1.1", "fast-glob": "3.2.12", "findup-sync": "5.0.0", + "graphql": "16.6.0", "jest": "29.4.3", "jscodeshift": "0.14.0", "prettier": "2.8.4", diff --git a/packages/codemods/src/codemods/list.yargs.ts b/packages/codemods/src/codemods/list.yargs.ts index 00baa968fbce..744c68e27f68 100644 --- a/packages/codemods/src/codemods/list.yargs.ts +++ b/packages/codemods/src/codemods/list.yargs.ts @@ -2,7 +2,6 @@ import fs from 'fs' import path from 'path' import yargs from 'yargs' -// @ts-expect-error is actually exported, just not in types import { decamelize } from 'yargs-parser' export const command = 'list ' diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts index 3d0a4068f56d..afdcc5693ae9 100644 --- a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts @@ -2,8 +2,7 @@ import path from 'path' import task from 'tasuku' -import { findCells } from 'src/lib/cells' - +import { findCells } from '../../../lib/cells' import runTransform from '../../../lib/runTransform' export const command = 'cell-query-result' @@ -13,7 +12,7 @@ export const description = export const handler = () => { task('cellQueryResult', async ({ setOutput }) => { await runTransform({ - transformPath: path.join(__dirname, 'cellQueryResult.ts'), + transformPath: path.join(__dirname, 'cellQueryResult.js'), targetPaths: findCells(), }) diff --git a/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.ts b/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.ts new file mode 100644 index 000000000000..4f2956b3c701 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.ts @@ -0,0 +1,38 @@ +import { + findCells, + fileToAst, + getCellGqlQuery, + parseGqlQueryToAst, +} from '../../../lib/cells' + +async function detectEmptyCells() { + const cellPaths = findCells() + + const susceptibleCells = cellPaths.filter((cellPath) => { + const fileContents = fileToAst(cellPath) + const cellQuery = getCellGqlQuery(fileContents) + + if (!cellQuery) { + return false + } + + const { fields } = parseGqlQueryToAst(cellQuery)[0] + + return fields.length > 1 + }) + + if (susceptibleCells.length > 0) { + console.log( + [ + 'You have Cells that are susceptible to the new isDataEmpty behavior:', + '', + susceptibleCells.map((c) => `• ${c}`).join('\n'), + '', + "The new behavior is documented in detail here. It's most likely what you want, but consider whether it affects you.", + "If you'd like to revert to the old behavior, you can override the `isDataEmpty` function.", + ].join('\n') + ) + } +} + +export default detectEmptyCells diff --git a/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.yargs.ts b/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.yargs.ts new file mode 100644 index 000000000000..4c4fa271397e --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/detectEmptyCells/detectEmptyCells.yargs.ts @@ -0,0 +1,17 @@ +import task, { TaskInnerAPI } from 'tasuku' + +import detectEmptyCells from './detectEmptyCells' + +export const command = 'detect-empty-cells' +export const description = '(v4.x.x->v5.0.0) Detects empty cells and warns' + +export const handler = () => { + task('detectEmptyCells', async ({ setError }: TaskInnerAPI) => { + try { + await detectEmptyCells() + console.log() + } catch (e: any) { + setError('Failed to detect empty cells in your project \n' + e?.message) + } + }) +} diff --git a/packages/web/src/components/createCell.test.tsx b/packages/web/src/components/createCell.test.tsx index 3f2b6ac5473e..2e6d112080f4 100644 --- a/packages/web/src/components/createCell.test.tsx +++ b/packages/web/src/components/createCell.test.tsx @@ -54,6 +54,58 @@ describe('createCell', () => { screen.getByText(/^42$/) }) + test.only('Renders Success if any of the fields have data (i.e. not just the first)', async () => { + const TestCell = createCell({ + // @ts-expect-error - Purposefully using a plain string here. + QUERY: 'query TestQuery { users { name } posts { title } }', + Empty: () => <>No users or posts, + Success: ({ users, posts }) => ( + <> +
+ {users.length > 0 ? ( + + ) : ( + 'no users' + )} +
+
+ {posts.length > 0 ? ( + + ) : ( + 'no posts' + )} +
+ + ), + }) + + const myUseQueryHook = () => { + return { + data: { + users: [], + posts: [{ title: 'bazinga' }, { title: 'kittens' }], + }, + } + } + + render( + + + + ) + + screen.getByText(/bazinga/) + screen.getByText(/kittens/) + }) + test('Renders default Loading when there is no data', async () => { const TestCell = createCell({ // @ts-expect-error - Purposefully using a plain string here. diff --git a/packages/web/src/components/createCell.tsx b/packages/web/src/components/createCell.tsx index 608fa6e6ce17..f2051b731634 100644 --- a/packages/web/src/components/createCell.tsx +++ b/packages/web/src/components/createCell.tsx @@ -185,9 +185,7 @@ export interface CreateCellProps { } /** - * The default `isEmpty` implementation. Checks if the first field is `null` or an empty array. - * - * @remarks + * The default `isEmpty` implementation. Checks if any of the field is `null` or an empty array. * * Consider the following queries. The former returns an object, the latter a list: * @@ -222,37 +220,16 @@ export interface CreateCellProps { * ``` * * Note that the latter can return `null` as well depending on the SDL (`posts: [Post!]`). - * - * @remarks - * - * We only check the first field (in the example below, `users`): - * - * ```js - * export const QUERY = gql` - * users { - * name - * } - * posts { - * title - * } - * ` * ``` */ -const dataField = (data: DataObject) => { - return data[Object.keys(data)[0]] -} - -const isDataNull = (data: DataObject) => { - return dataField(data) === null -} - -const isDataEmptyArray = (data: DataObject) => { - const field = dataField(data) +function isFieldEmptyArray(field: unknown) { return Array.isArray(field) && field.length === 0 } -const isDataEmpty = (data: DataObject) => { - return isDataNull(data) || isDataEmptyArray(data) +function isDataEmpty(data: DataObject) { + return Object.values(data).every((fieldValue) => { + return fieldValue === null || isFieldEmptyArray(fieldValue) + }) } /** diff --git a/yarn.lock b/yarn.lock index a3812215796b..a852437f08c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6785,8 +6785,10 @@ __metadata: dependencies: "@babel/cli": 7.21.0 "@babel/core": 7.21.0 + "@babel/parser": 7.21.2 "@babel/plugin-transform-typescript": 7.21.0 "@babel/runtime-corejs3": 7.21.0 + "@babel/traverse": 7.21.2 "@iarna/toml": 2.2.5 "@types/babel__core": 7.20.0 "@types/findup-sync": 4.0.2 @@ -6803,6 +6805,7 @@ __metadata: fast-glob: 3.2.12 findup-sync: 5.0.0 fs-extra: 11.1.0 + graphql: 16.6.0 jest: 29.4.3 jscodeshift: 0.14.0 prettier: 2.8.4