Skip to content

Commit d560fbe

Browse files
authored
Merge pull request #1713 from quadratichq/qa
QA: Aug 13th
2 parents b0ac880 + 978df35 commit d560fbe

File tree

288 files changed

+15210
-2383
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

288 files changed

+15210
-2383
lines changed

Cargo.lock

+106-93
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

+378-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quadratic-api/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@types/node": "^18.11.9",
4343
"@types/supertest": "^2.0.12",
4444
"auth0": "^3.6.1",
45-
"axios": "^1.5.0",
45+
"axios": "^1.7.4",
4646
"cors": "^2.8.5",
4747
"express": "^4.19.2",
4848
"express-async-errors": "^3.1.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Team" ADD COLUMN "client_data_kv" JSONB NOT NULL DEFAULT '{}';

quadratic-api/prisma/schema.prisma

+5
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ model Team {
118118
stripeCurrentPeriodEnd DateTime? @map("stripe_current_period_end")
119119
stripeSubscriptionLastUpdated DateTime? @map("stripe_subscription_last_updated")
120120
121+
// Key/value storage used by Quadratic client on the Team.
122+
// It remembers things like closing the onboarding banner.
123+
// Use for client-specific data that is not useful to the server or other services
124+
clientDataKv Json @default("{}") @map("client_data_kv")
125+
121126
@@index([uuid])
122127
}
123128

quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts

+51-42
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,35 @@ import { User } from 'auth0';
22
import request from 'supertest';
33
import { app } from '../../app';
44
import dbClient from '../../dbClient';
5-
import { clearDb } from '../../tests/testDataGenerator';
5+
import { clearDb, createFile, createTeam, createUser } from '../../tests/testDataGenerator';
66

77
beforeEach(async () => {
8-
// Create some users & team
9-
const user_1 = await dbClient.user.create({
10-
data: {
11-
auth0Id: 'team_1_owner',
12-
},
13-
});
14-
const user_2 = await dbClient.user.create({
15-
data: {
16-
auth0Id: 'team_1_editor',
17-
},
18-
});
19-
const user_3 = await dbClient.user.create({
20-
data: {
21-
auth0Id: 'team_1_viewer',
22-
},
23-
});
24-
await dbClient.user.create({
25-
data: {
26-
auth0Id: 'user_without_team',
27-
},
28-
});
29-
await dbClient.team.create({
30-
data: {
8+
const user_1 = await createUser({ auth0Id: 'team_1_owner' });
9+
const user_2 = await createUser({ auth0Id: 'team_1_editor' });
10+
const user_3 = await createUser({ auth0Id: 'team_1_viewer' });
11+
await createUser({ auth0Id: 'user_without_team' });
12+
13+
const team = await createTeam({
14+
team: {
3115
name: 'Test Team 1',
3216
uuid: '00000000-0000-4000-8000-000000000001',
33-
UserTeamRole: {
34-
create: [
35-
{
36-
userId: user_1.id,
37-
role: 'OWNER',
38-
},
39-
{ userId: user_2.id, role: 'EDITOR' },
40-
{ userId: user_3.id, role: 'VIEWER' },
41-
],
17+
},
18+
users: [
19+
{
20+
userId: user_1.id,
21+
role: 'OWNER',
4222
},
23+
{ userId: user_2.id, role: 'EDITOR' },
24+
{ userId: user_3.id, role: 'VIEWER' },
25+
],
26+
connections: [{ type: 'POSTGRES' }],
27+
});
28+
29+
await createFile({
30+
data: {
31+
name: 'Test File 1',
32+
ownerTeamId: team.id,
33+
creatorUserId: user_1.id,
4334
},
4435
});
4536
});
@@ -95,14 +86,6 @@ jest.mock('auth0', () => {
9586
});
9687

9788
describe('GET /v0/teams/:uuid', () => {
98-
// TODO the auth/team middleware should handle all this...?
99-
describe('sending a bad request', () => {
100-
it.todo('responds with a 401 without authentication');
101-
it.todo('responds with a 404 for requesting a team that doesn’t exist');
102-
it.todo('responds with a 400 for failing schema validation on the team UUID');
103-
it.todo('responds with a 400 for failing schema validation on the payload');
104-
});
105-
10689
describe('get a team you belong to', () => {
10790
// TODO different responses for OWNER, EDITOR, VIEWER?
10891
it('responds with a team', async () => {
@@ -113,7 +96,11 @@ describe('GET /v0/teams/:uuid', () => {
11396
.expect((res) => {
11497
expect(res.body).toHaveProperty('team');
11598
expect(res.body.team.uuid).toBe('00000000-0000-4000-8000-000000000001');
116-
expect(res.body.team.name).toBe('Test Team 1');
99+
100+
expect(res.body.clientDataKv).toStrictEqual({});
101+
expect(res.body.connections).toHaveLength(1);
102+
expect(res.body.files).toHaveLength(1);
103+
expect(typeof res.body.files[0].file.creatorId).toBe('number');
117104

118105
expect(res.body.users[0].email).toBe('[email protected]');
119106
expect(res.body.users[0].role).toBe('OWNER');
@@ -128,5 +115,27 @@ describe('GET /v0/teams/:uuid', () => {
128115
expect(res.body.users[2].name).toBe('Test User 3');
129116
});
130117
});
118+
119+
it('does not return archived connections', async () => {
120+
// delete all connections in a team
121+
const team = await dbClient.team.findUniqueOrThrow({
122+
where: {
123+
uuid: '00000000-0000-4000-8000-000000000001',
124+
},
125+
});
126+
await dbClient.connection.deleteMany({
127+
where: {
128+
teamId: team.id,
129+
},
130+
});
131+
132+
await request(app)
133+
.get(`/v0/teams/00000000-0000-4000-8000-000000000001`)
134+
.set('Authorization', `Bearer ValidToken team_1_owner`)
135+
.expect(200)
136+
.expect((res) => {
137+
expect(res.body.connections).toHaveLength(0);
138+
});
139+
});
131140
});
132141
});

quadratic-api/src/routes/v0/teams.$uuid.GET.ts

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
3535
},
3636
include: {
3737
Connection: {
38+
where: {
39+
archived: null,
40+
},
3841
orderBy: {
3942
createdDate: 'desc',
4043
},
@@ -135,6 +138,7 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
135138
updatedDate: file.updatedDate.toISOString(),
136139
publicLinkAccess: file.publicLinkAccess,
137140
thumbnail: file.thumbnail,
141+
creatorId: file.creatorUserId,
138142
},
139143
userMakingRequest: {
140144
filePermissions: getFilePermissions({
@@ -168,7 +172,18 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
168172
}),
169173
},
170174
})),
175+
connections: dbTeam.Connection.map((connection) => ({
176+
uuid: connection.uuid,
177+
name: connection.name,
178+
createdDate: connection.createdDate.toISOString(),
179+
type: connection.type,
180+
})),
181+
clientDataKv: isObject(dbTeam.clientDataKv) ? dbTeam.clientDataKv : {},
171182
};
172183

173184
return res.status(200).json(response);
174185
}
186+
187+
function isObject(x: any): x is Record<string, any> {
188+
return typeof x === 'object' && !Array.isArray(x) && x !== null;
189+
}

quadratic-api/src/routes/v0/teams.$uuid.PATCH.test.ts

+14-19
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('PATCH /v0/teams/:uuid', () => {
7272
});
7373

7474
describe('update a team', () => {
75-
it('accepts change from OWNER', async () => {
75+
it('accepts name change', async () => {
7676
await request(app)
7777
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
7878
.send({ name: 'Foobar' })
@@ -82,32 +82,27 @@ describe('PATCH /v0/teams/:uuid', () => {
8282
expect(res.body.name).toBe('Foobar');
8383
});
8484
});
85-
86-
it('accepts change from EDITOR', async () => {
85+
it('accepst key/value pair updates', async () => {
86+
// Create value
8787
await request(app)
8888
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
89-
.send({ name: 'Foobar' })
90-
.set('Authorization', `Bearer ValidToken team_1_editor`)
89+
.send({ clientDataKv: { foo: 'bar' } })
90+
.set('Authorization', `Bearer ValidToken team_1_owner`)
9191
.expect(200)
9292
.expect((res) => {
93-
expect(res.body.name).toBe('Foobar');
93+
expect(res.body.clientDataKv.foo).toBe('bar');
9494
});
95-
});
9695

97-
it('rejects change from VIEWER', async () => {
96+
// Update value
9897
await request(app)
9998
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
100-
.send({ name: 'Foobar' })
101-
.set('Authorization', `Bearer ValidToken team_1_viewer`)
102-
.expect(403);
103-
});
104-
105-
it('rejects change from someone who isn’t a team member', async () => {
106-
await request(app)
107-
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
108-
.send({ name: 'Foobar' })
109-
.set('Authorization', `Bearer ValidToken no_team`)
110-
.expect(403);
99+
.send({ clientDataKv: { anotherValue: 'hello' } })
100+
.set('Authorization', `Bearer ValidToken team_1_owner`)
101+
.expect(200)
102+
.expect((res) => {
103+
expect(res.body.clientDataKv.foo).toBe('bar');
104+
expect(res.body.clientDataKv.anotherValue).toBe('hello');
105+
});
111106
});
112107
});
113108
});
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Response } from 'express';
2-
import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas';
2+
import { ApiSchemas, ApiTypes, TeamClientDataKvSchema } from 'quadratic-shared/typesAndSchemas';
33
import z from 'zod';
44
import dbClient from '../../dbClient';
55
import { getTeam } from '../../middleware/getTeam';
66
import { userMiddleware } from '../../middleware/user';
77
import { validateAccessToken } from '../../middleware/validateAccessToken';
88
import { parseRequest } from '../../middleware/validateRequestSchema';
9-
import { updateCustomer } from '../../stripe/stripe';
109
import { RequestWithUser } from '../../types/Request';
1110
import { ApiError } from '../../utils/ApiError';
1211

@@ -21,35 +20,53 @@ const schema = z.object({
2120

2221
async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:uuid.PATCH.response']>) {
2322
const {
24-
body: { name },
23+
body: { name, clientDataKv },
2524
params: { uuid },
2625
} = parseRequest(req, schema);
2726
const {
2827
user: { id: userId },
2928
} = req;
3029
const {
3130
userMakingRequest: { permissions },
32-
team: { stripeCustomerId },
31+
team: { clientDataKv: exisitingClientDataKv },
3332
} = await getTeam({ uuid, userId });
3433

3534
// Can the user even edit this team?
3635
if (!permissions.includes('TEAM_EDIT')) {
3736
throw new ApiError(403, 'User does not have permission to edit this team.');
3837
}
3938

40-
// Update Customer name in Stripe
41-
if (stripeCustomerId) {
42-
await updateCustomer(stripeCustomerId, name);
39+
// Have they supplied _something_?
40+
if (!name && !clientDataKv) {
41+
throw new ApiError(400, '`name` or `clientDataKv` are required');
4342
}
4443

45-
// Update the team name
44+
// Validate exisiting data in the db
45+
const validatedExisitingClientDataKv = validateClientDataKv(exisitingClientDataKv);
46+
47+
// Update the team with supplied data
4648
const newTeam = await dbClient.team.update({
4749
where: {
4850
uuid,
4951
},
5052
data: {
51-
name,
53+
...(name ? { name } : {}),
54+
...(clientDataKv ? { clientDataKv: { ...validatedExisitingClientDataKv, ...clientDataKv } } : {}),
5255
},
5356
});
54-
return res.status(200).json({ name: newTeam.name });
57+
58+
// Return the new data
59+
const newClientDataKv = validateClientDataKv(newTeam.clientDataKv);
60+
return res.status(200).json({
61+
name: newTeam.name,
62+
clientDataKv: newClientDataKv,
63+
});
64+
}
65+
66+
function validateClientDataKv(clientDataKv: unknown) {
67+
const parseResult = TeamClientDataKvSchema.safeParse(clientDataKv);
68+
if (!parseResult.success) {
69+
throw new ApiError(500, '`clientDataKv` must be a valid JSON object');
70+
}
71+
return parseResult.data;
5572
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"shadcn"
4+
]
5+
}

quadratic-client/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@monaco-editor/react": "^4.3.1",
2323
"@mui/icons-material": "^5.2.0",
2424
"@mui/material": "^5.2.2",
25+
"@radix-ui/react-accordion": "^1.2.0",
2526
"@radix-ui/react-alert-dialog": "^1.0.5",
2627
"@radix-ui/react-avatar": "^1.0.4",
2728
"@radix-ui/react-checkbox": "^1.0.4",
Loading
1.13 KB
Loading
2.11 KB
Loading

quadratic-client/scripts/fonts.js

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const fontDirectory = path.join('public', 'fonts');
88
const fontFamilies = ['opensans'];
99
const fontFiles = ['OpenSans', 'OpenSans-Bold', 'OpenSans-Italic', 'OpenSans-BoldItalic'];
1010

11-
const charSet = [''];
12-
1311
async function cleanOldFiles() {
1412
console.log('Removing old files...');
1513
for (const family of fontFamilies) {

quadratic-client/src/app/actions.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider';
99
import { ROUTES } from '@/shared/constants/routes';
1010
import { DOCUMENTATION_URL } from '@/shared/constants/urls';
1111
import { ApiTypes, FilePermission, FilePermissionSchema, TeamPermission } from 'quadratic-shared/typesAndSchemas';
12-
import { NavigateFunction, SubmitFunction } from 'react-router-dom';
12+
import { SubmitFunction } from 'react-router-dom';
1313
import { SetterOrUpdater } from 'recoil';
1414
const { FILE_EDIT, FILE_DELETE } = FilePermissionSchema.enum;
1515

@@ -70,10 +70,10 @@ export const isAvailableBecauseFileLocationIsAccessibleAndWriteable = ({
7070
}: IsAvailableArgs) => Boolean(fileTeamPrivacy) && Boolean(teamPermissions?.includes('TEAM_EDIT'));
7171

7272
export const createNewFileAction = {
73-
label: 'Create',
73+
label: 'New',
7474
isAvailable: isAvailableBecauseFileLocationIsAccessibleAndWriteable,
75-
run({ navigate, teamUuid }: { navigate: NavigateFunction; teamUuid: string }) {
76-
navigate(ROUTES.CREATE_FILE_PRIVATE(teamUuid));
75+
run({ setEditorInteractionState }: { setEditorInteractionState: SetterOrUpdater<EditorInteractionState> }) {
76+
setEditorInteractionState((prevState) => ({ ...prevState, showNewFileMenu: true }));
7777
},
7878
};
7979

@@ -186,6 +186,11 @@ export const downloadSelectionAsCsvAction = {
186186
},
187187
};
188188

189+
export const dataValidations = {
190+
label: 'Data Validations',
191+
isAvailable: isAvailableBecauseCanEditFile,
192+
};
193+
189194
export const findInSheet = {
190195
label: 'Find in current sheet',
191196
};
@@ -197,3 +202,8 @@ export const resizeColumnAction = {
197202
label: 'Resize column to fit data',
198203
isAvailable: isAvailableBecauseCanEditFile,
199204
};
205+
206+
export const validationAction = {
207+
label: 'Data Validations',
208+
isAvailable: isAvailableBecauseCanEditFile,
209+
};

0 commit comments

Comments
 (0)