feat: batch commit delete/move (#1016)

* feat: batch delete/move commits

* fix: ts linter issue
This commit is contained in:
Kristaps Fabians Geikins
2022-09-22 16:49:18 +03:00
committed by GitHub
parent 1a0a72f73b
commit 05f11a26da
35 changed files with 1740 additions and 156 deletions
+1
View File
@@ -16,6 +16,7 @@
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"circleci:check": "circleci config validate ./.circleci/config.yml",
"dev:docker": "docker-compose -f ./docker-compose-deps.yml",
"dev:docker:up": "docker-compose -f ./docker-compose-deps.yml up -d",
"dev:docker:down": "docker-compose -f ./docker-compose-deps.yml down",
"dev": "yarn workspaces foreach -piv -j unlimited run dev",
+1
View File
@@ -2,6 +2,7 @@ query StreamWithBranch($streamId: String!, $branchName: String!, $cursor: String
stream(id: $streamId) {
id
name
role
branch(name: $branchName) {
id
name
+26
View File
@@ -0,0 +1,26 @@
import { gql } from '@apollo/client/core'
export const streamBranchesSelectorQuery = gql`
query StreamBranchesSelector($streamId: String!) {
stream(id: $streamId) {
id
branches(limit: 100) {
items {
name
}
}
}
}
`
export const moveCommitsMutation = gql`
mutation MoveCommits($input: CommitsMoveInput!) {
commitsMove(input: $input)
}
`
export const deleteCommitsMutation = gql`
mutation DeleteCommits($input: CommitsDeleteInput!) {
commitsDelete(input: $input)
}
`
@@ -166,7 +166,7 @@ export type BranchCommitsArgs = {
export type BranchCollection = {
__typename?: 'BranchCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Branch>>>;
items?: Maybe<Array<Branch>>;
totalCount: Scalars['Int'];
};
@@ -346,8 +346,6 @@ export type CommitCreateInput = {
message?: InputMaybe<Scalars['String']>;
objectId: Scalars['String'];
parents?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
/** **DEPRECATED** Use the `parents` field. */
previousCommitIds?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
sourceApplication?: InputMaybe<Scalars['String']>;
streamId: Scalars['String'];
totalChildrenCount?: InputMaybe<Scalars['Int']>;
@@ -388,6 +386,15 @@ export type CommitUpdateInput = {
streamId: Scalars['String'];
};
export type CommitsDeleteInput = {
commitIds: Array<Scalars['String']>;
};
export type CommitsMoveInput = {
commitIds: Array<Scalars['String']>;
targetBranch: Scalars['String'];
};
export enum DiscoverableStreamsSortType {
CreatedDate = 'CREATED_DATE',
FavoritesCount = 'FAVORITES_COUNT'
@@ -467,6 +474,10 @@ export type Mutation = {
commitDelete: Scalars['Boolean'];
commitReceive: Scalars['Boolean'];
commitUpdate: Scalars['Boolean'];
/** Delete a batch of commits */
commitsDelete: Scalars['Boolean'];
/** Move a batch of commits to a new branch */
commitsMove: Scalars['Boolean'];
/** Delete a pending invite */
inviteDelete: Scalars['Boolean'];
/** Re-send a pending invite */
@@ -621,6 +632,16 @@ export type MutationCommitUpdateArgs = {
};
export type MutationCommitsDeleteArgs = {
input: CommitsDeleteInput;
};
export type MutationCommitsMoveArgs = {
input: CommitsMoveInput;
};
export type MutationInviteDeleteArgs = {
inviteId: Scalars['String'];
};
@@ -1678,7 +1699,7 @@ export type StreamWithBranchQueryVariables = Exact<{
}>;
export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null };
export type StreamWithBranchQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null };
export type BranchCreatedSubscriptionVariables = Exact<{
streamId: Scalars['String'];
@@ -1697,6 +1718,27 @@ export type StreamCommitQueryQueryVariables = Exact<{
export type StreamCommitQueryQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, commit?: { __typename?: 'Commit', id: string, message?: string | null, referencedObject: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, createdAt?: string | null, branchName?: string | null, sourceApplication?: string | null } | null } | null };
export type StreamBranchesSelectorQueryVariables = Exact<{
streamId: Scalars['String'];
}>;
export type StreamBranchesSelectorQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branches?: { __typename?: 'BranchCollection', items?: Array<{ __typename?: 'Branch', name: string }> | null } | null } | null };
export type MoveCommitsMutationVariables = Exact<{
input: CommitsMoveInput;
}>;
export type MoveCommitsMutation = { __typename?: 'Mutation', commitsMove: boolean };
export type DeleteCommitsMutationVariables = Exact<{
input: CommitsDeleteInput;
}>;
export type DeleteCommitsMutation = { __typename?: 'Mutation', commitsDelete: boolean };
export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string };
export type FullStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, streamId: string, createdAt: string, requester: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } };
@@ -2214,6 +2256,7 @@ export const StreamWithBranch = gql`
stream(id: $streamId) {
id
name
role
branch(name: $branchName) {
id
name
@@ -2262,6 +2305,28 @@ export const StreamCommitQuery = gql`
}
}
`;
export const StreamBranchesSelector = gql`
query StreamBranchesSelector($streamId: String!) {
stream(id: $streamId) {
id
branches(limit: 100) {
items {
name
}
}
}
}
`;
export const MoveCommits = gql`
mutation MoveCommits($input: CommitsMoveInput!) {
commitsMove(input: $input)
}
`;
export const DeleteCommits = gql`
mutation DeleteCommits($input: CommitsDeleteInput!) {
commitsDelete(input: $input)
}
`;
export const StreamInvite = gql`
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
@@ -2765,9 +2830,12 @@ export const CommonUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"k
export const GetStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},...BasicStreamAccessRequestFieldsFragmentDoc.definitions]} as unknown as DocumentNode<GetStreamAccessRequestQuery, GetStreamAccessRequestQueryVariables>;
export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},...BasicStreamAccessRequestFieldsFragmentDoc.definitions]} as unknown as DocumentNode<CreateStreamAccessRequestMutation, CreateStreamAccessRequestMutationVariables>;
export const UseStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UseStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accept"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamRole"}},"defaultValue":{"kind":"EnumValue","value":"STREAM_CONTRIBUTOR"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestUse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"requestId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"requestId"}}},{"kind":"Argument","name":{"kind":"Name","value":"accept"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accept"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}]}]}}]} as unknown as DocumentNode<UseStreamAccessRequestMutation, UseStreamAccessRequestMutationVariables>;
export const StreamWithBranchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithBranch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamWithBranchQuery, StreamWithBranchQueryVariables>;
export const StreamWithBranchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithBranch"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branchName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commentCount"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamWithBranchQuery, StreamWithBranchQueryVariables>;
export const BranchCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"BranchCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"branchCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode<BranchCreatedSubscription, BranchCreatedSubscriptionVariables>;
export const StreamCommitQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamCommitQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"commit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"authorAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"branchName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}}]}}]}}]}}]} as unknown as DocumentNode<StreamCommitQueryQuery, StreamCommitQueryQueryVariables>;
export const StreamBranchesSelectorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamBranchesSelector"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamBranchesSelectorQuery, StreamBranchesSelectorQueryVariables>;
export const MoveCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MoveCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CommitsMoveInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commitsMove"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<MoveCommitsMutation, MoveCommitsMutationVariables>;
export const DeleteCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CommitsDeleteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commitsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<DeleteCommitsMutation, DeleteCommitsMutationVariables>;
export const StreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<StreamInviteQuery, StreamInviteQueryVariables>;
export const UserStreamInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserStreamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInvites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UsersOwnInviteFields"}}]}}]}},...UsersOwnInviteFieldsFragmentDoc.definitions,...LimitedUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<UserStreamInvitesQuery, UserStreamInvitesQueryVariables>;
export const UseStreamInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UseStreamInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accept"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamInviteUse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accept"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accept"}}},{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<UseStreamInviteMutation, UseStreamInviteMutationVariables>;
@@ -3,9 +3,7 @@
<v-card
:class="`rounded-lg overflow-hidden`"
:elevation="hover ? 10 : 1"
:style="`transition: all 0.2s ease-in-out; ${
highlight ? 'outline: 0.2rem solid #047EFB;' : ''
}`"
:style="`${highlighted ? 'outline: 0.2rem solid #047EFB;' : ''}`"
>
<router-link :to="`/streams/${streamId}/commits/${commit.id}`">
<preview-image
@@ -15,14 +13,22 @@
></preview-image>
</router-link>
<v-toolbar class="transparent elevation-0" dense>
<v-toolbar-title>
<router-link
class="text-decoration-none"
:to="`/streams/${streamId}/commits/${commit.id}`"
>
<v-icon small>mdi-source-commit</v-icon>
{{ commit.message }}
</router-link>
<v-toolbar-title class="d-flex" style="overflow: visible; width: 100%">
<v-checkbox
v-if="allowSelect"
v-model="selectedState"
dense
@change="onSelect"
/>
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
<router-link
class="text-decoration-none"
:to="`/streams/${streamId}/commits/${commit.id}`"
>
<v-icon v-if="!allowSelect" small>mdi-source-commit</v-icon>
{{ commit.message }}
</router-link>
</div>
</v-toolbar-title>
</v-toolbar>
<div class="mx-1 mb-2">
@@ -90,13 +96,37 @@ export default {
commit: { type: Object, default: () => null },
previewHeight: { type: Number, default: () => 180 },
showStreamAndBranch: { type: Boolean, default: true },
highlight: { type: Boolean, default: false }
highlight: { type: Boolean, default: false },
allowSelect: {
type: Boolean,
default: false
},
selected: {
type: Boolean,
default: false
}
},
computed: {
highlighted() {
return this.highlight || this.selected
},
streamId() {
return (
this.commit.streamId ?? this.$route.params.streamId ?? this.$route.query.stream
)
},
selectedState: {
get() {
return this.selected
},
set(val) {
this.$emit('update:selected', !!val)
}
}
},
methods: {
onSelect() {
this.$emit('select', { value: this.selected })
}
}
}
@@ -0,0 +1,70 @@
<template>
<v-dialog
v-model="dialogModel"
:fullscreen="$vuetify.breakpoint.xsOnly"
:max-width="maxWidth"
>
<v-card>
<!-- Header -->
<v-toolbar color="primary" dark flat>
<v-app-bar-nav-icon style="pointer-events: none">
<slot name="icon">
<v-icon>mdi-information</v-icon>
</slot>
</v-app-bar-nav-icon>
<v-toolbar-title>
<slot name="title">Title</slot>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="close"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<!-- Body -->
<v-card-text class="pt-5">
<slot name="content">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
in culpa qui officia deserunt mollit anim id est laborum.
</slot>
</v-card-text>
<!-- Actions -->
<v-card-actions class="px-6 pb-5 pt-0">
<slot name="actions">
<v-spacer></v-spacer>
<v-btn text @click="close">Cancel</v-btn>
</slot>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
export default defineComponent({
name: 'BaseDialog',
props: {
show: {
type: Boolean,
required: true
},
maxWidth: {
type: Number,
default: 500
}
},
setup(props, { emit }) {
const dialogModel = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const close = () => (dialogModel.value = false)
return { dialogModel, close }
}
})
</script>
@@ -0,0 +1,53 @@
<template>
<v-select
v-model="model"
filled
rounded
dense
hide-details
:items="items"
prepend-icon="mdi-source-branch"
:disabled="loading"
/>
</template>
<script lang="ts">
import { StreamBranchesSelectorDocument } from '@/graphql/generated/graphql'
import { Nullable } from '@/helpers/typeHelpers'
import { useQuery } from '@vue/apollo-composable'
import { computed, defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'BranchSelect',
props: {
streamId: {
type: String,
required: true
},
value: {
type: String as PropType<Nullable<string>>,
default: null
},
excludedNames: {
type: Array as PropType<string[]>,
default: () => []
}
},
setup(props, { emit }) {
const { result, loading } = useQuery(StreamBranchesSelectorDocument, () => ({
streamId: props.streamId
}))
const items = computed(() =>
(result.value?.stream?.branches?.items || [])
.filter((i) => !props.excludedNames.includes(i.name))
.map((i) => i.name)
)
const model = computed({
get: () => props.value,
set: (newVal) => emit('input', newVal)
})
return { items, loading, model }
}
})
</script>
@@ -0,0 +1,74 @@
<template>
<div>
<prioritized-portal to="toolbar" identity="commits-multi-select" :priority="2">
<div class="font-weight-bold">{{ count }} commits selected</div>
</prioritized-portal>
<prioritized-portal to="actions" identity="commits-multi-select" :priority="2">
<div class="d-flex align-center">
<v-btn small @click="clear">Clear selection</v-btn>
<v-btn small class="ml-2" color="primary" @click="initMove">Move To</v-btn>
<v-btn small class="mx-2" color="red" @click="initDelete">Delete</v-btn>
</div>
</prioritized-portal>
<commits-batch-actions-dialog
:show.sync="showDialog"
:stream-id="streamId"
:branch-name="branchName"
:selected-commit-ids="selectedCommitIds"
:type="dialogType"
@finish="onFinish"
/>
</div>
</template>
<script lang="ts">
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
import CommitsBatchActionsDialog from '@/main/dialogs/commit/CommitsBatchActionsDialog.vue'
import { BatchActionType } from '@/main/lib/stream/composables/commitMultiActions'
import { computed, defineComponent, PropType, ref } from 'vue'
export default defineComponent({
name: 'CommitMultiSelectToolbar',
components: {
PrioritizedPortal,
CommitsBatchActionsDialog
},
props: {
streamId: {
type: String,
required: true
},
selectedCommitIds: {
type: Array as PropType<string[]>,
required: true
},
branchName: {
type: String,
required: true
}
},
setup(props, { emit }) {
const showDialog = ref(false)
const dialogType = ref(BatchActionType.Move)
const count = computed(() => props.selectedCommitIds?.length || 0)
const clear = () => emit('clear')
const initMove = () => {
showDialog.value = true
dialogType.value = BatchActionType.Move
}
const initDelete = () => {
showDialog.value = true
dialogType.value = BatchActionType.Delete
}
const onFinish = () => {
clear()
emit('finish')
}
return { clear, count, showDialog, initMove, initDelete, onFinish, dialogType }
}
})
</script>
@@ -85,6 +85,7 @@ import { gql } from '@apollo/client/core'
import isNull from 'lodash/isNull'
import isUndefined from 'lodash/isUndefined'
import clone from 'lodash/clone'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
export default {
props: {
@@ -190,7 +191,7 @@ export default {
this.$eventHub.$emit('notification', { text: 'Branch deleted' })
this.$router.push(`/streams/` + this.$route.params.streamId)
this.$emit('close')
this.$eventHub.$emit('branch-refresh')
this.$eventHub.$emit(StreamEvents.RefetchBranches)
},
async updateBranch() {
if (!this.$refs.form.validate()) return
@@ -227,7 +228,7 @@ export default {
}
this.loading = false
this.$eventHub.$emit('branch-refresh')
this.$eventHub.$emit(StreamEvents.RefetchBranches)
this.$eventHub.$emit('notification', {
text: 'Branch updated',
action: {
@@ -0,0 +1,200 @@
<template>
<base-dialog :show.sync="realShow">
<template #title>
{{ titleText }}
</template>
<template #content>
<template v-if="type === BatchActionType.Delete">
Deleting commits is an irrevocable action! If you are sure about wanting to
delete the selected commits, please click on the button below!
</template>
<template v-else-if="type === BatchActionType.Move">
<div class="mb-4">
Please select the target branch to move all of the selected commits to:
</div>
<branch-select
v-model="targetBranch"
:stream-id="streamId"
:excluded-names="[branchName]"
/>
</template>
</template>
<template #actions>
<v-spacer></v-spacer>
<v-btn @click="close">Cancel</v-btn>
<template v-if="type === BatchActionType.Delete">
<v-btn color="red" :disabled="deleteDisabled" @click="deleteCommits">
Delete
</v-btn>
</template>
<template v-else-if="type === BatchActionType.Move">
<v-btn color="primary" :disabled="moveDisabled" @click="moveCommits">
Move
</v-btn>
</template>
</template>
</base-dialog>
</template>
<script lang="ts">
import BaseDialog from '@/main/components/common/layout/BaseDialog.vue'
import BranchSelect from '@/main/components/stream/branch/BranchSelect.vue'
import { BatchActionType } from '@/main/lib/stream/composables/commitMultiActions'
import { useApolloClient } from '@vue/apollo-composable'
import { computed, defineComponent, PropType, ref } from 'vue'
import {
MoveCommitsDocument,
DeleteCommitsDocument,
MoveCommitsMutation,
MoveCommitsMutationVariables,
DeleteCommitsMutation,
DeleteCommitsMutationVariables
} from '@/graphql/generated/graphql'
import { Nullable } from '@/helpers/typeHelpers'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage
} from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
import { useGlobalToast } from '@/main/lib/core/composables/notifications'
import { DocumentNode } from 'graphql'
export default defineComponent({
name: 'CommitsBatchActionsDialog',
components: {
BaseDialog,
BranchSelect
},
props: {
streamId: {
type: String,
required: true
},
branchName: {
type: String,
required: true
},
selectedCommitIds: {
type: Array as PropType<string[]>,
required: true
},
show: {
type: Boolean,
required: true
},
type: {
type: String as PropType<BatchActionType>,
default: BatchActionType.Delete
}
},
setup(props, { emit }) {
const apollo = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const targetBranch = ref(null as Nullable<string>)
const loading = ref(false)
const realShow = computed({
get: () => props.show,
set: (newShow) => emit('update:show', newShow)
})
const count = computed(() => props.selectedCommitIds.length)
const titleText = computed(() => {
switch (props.type) {
case BatchActionType.Delete:
return `Delete ${count.value} commits`
case BatchActionType.Move:
return `Move ${count.value} commits`
default:
return ''
}
})
const moveDisabled = computed(() => !targetBranch.value || loading.value)
const deleteDisabled = computed(() => loading.value)
const close = () => (realShow.value = false)
const invokeAction = async <
D = Record<string, unknown>,
V = Record<string, unknown>
>(params: {
shouldQuit: () => boolean
getResult: (data: D | undefined) => boolean | null | undefined
document: DocumentNode
variables: V
successMessage: string
}) => {
const { shouldQuit, document, variables, getResult, successMessage } = params
if (shouldQuit()) return
loading.value = true
const { data, errors } = await apollo
.mutate({
mutation: document,
variables
})
.catch(convertThrowIntoFetchResult)
const result = getResult(data)
if (result) {
triggerNotification({
text: successMessage,
type: 'success'
})
// finished
close()
emit('finish')
} else {
const msg = getFirstErrorMessage(errors)
triggerNotification({
text: msg,
type: 'error'
})
}
loading.value = false
}
const moveCommits = async () => {
await invokeAction<MoveCommitsMutation, MoveCommitsMutationVariables>({
shouldQuit: () => moveDisabled.value,
getResult: (data) => data?.commitsMove,
document: MoveCommitsDocument,
variables: {
input: {
commitIds: props.selectedCommitIds.slice(),
targetBranch: targetBranch.value!
}
},
successMessage: 'Selected commits successfully moved'
})
}
const deleteCommits = async () => {
await invokeAction<DeleteCommitsMutation, DeleteCommitsMutationVariables>({
shouldQuit: () => deleteDisabled.value,
getResult: (data) => data?.commitsDelete,
document: DeleteCommitsDocument,
variables: {
input: {
commitIds: props.selectedCommitIds.slice()
}
},
successMessage: 'Selected commits successfully deleted'
})
}
return {
realShow,
count,
titleText,
BatchActionType,
close,
moveCommits,
deleteCommits,
targetBranch,
moveDisabled,
deleteDisabled
}
}
})
</script>
@@ -25,12 +25,12 @@ export function useMixpanel(): OverridedMixpanel {
}
/**
* Composable that resolves whether the user is logged in through an Apollo query
* Composable that resolves user auth information through an Apollo query
*/
export function useIsLoggedIn() {
const { result } = useQuery(IsLoggedInDocument)
const isLoggedIn = computed(() => !!result.value?.user?.id)
const userId = computed(() => result.value?.user?.id)
const isLoggedIn = computed(() => !!userId.value)
return { isLoggedIn, userId }
}
@@ -29,12 +29,17 @@ export const GlobalEvents = Object.freeze({
*/
export const StreamEvents = Object.freeze({
/**
* For triggering a refetch of stream data
* For triggering a refetch of main stream data
*/
Refetch: 'stream:refetch',
/**
* For triggering a refetch of stream collaborator data
*/
RefetchCollaborators: 'stream:refetch:collaborators'
RefetchCollaborators: 'stream:refetch:collaborators',
/**
* For triggering a refetch of stream branch data
*/
RefetchBranches: 'stream:refetch:branches'
})
@@ -0,0 +1,52 @@
import { reduce } from 'lodash'
import { ref, computed } from 'vue'
export enum BatchActionType {
Move = 'move',
Delete = 'delete'
}
/**
* Composable for setting up commit multi-select & actions like delete, move etc.
*/
export function useCommitMultiActions() {
const selectedCommitsState = ref({} as Record<string, boolean>)
const selectedCommitIds = computed(() =>
reduce(
selectedCommitsState.value,
(res, val, key) => {
if (val) {
res.push(key)
}
return res
},
[] as string[]
)
)
const clearSelectedCommits = () => {
selectedCommitsState.value = {}
}
const hasSelectedCommits = computed(() => selectedCommitIds.value.length > 0)
return {
/**
* Selected commit IDs (read-only)
*/
selectedCommitIds,
/**
* Object with selected commit keys and bool values
*/
selectedCommitsState,
/**
* Whether there are any selected commit ids
*/
hasSelectedCommits,
/**
* Clear selected commits
*/
clearSelectedCommits
}
}
@@ -226,6 +226,7 @@ import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
export default {
components: {
@@ -301,7 +302,7 @@ export default {
},
mounted() {
this.branchMenuOpen = this.$route.name.toLowerCase().includes('branch')
this.$eventHub.$on('branch-refresh', async () => {
this.$eventHub.$on(StreamEvents.RefetchBranches, async () => {
await this.refetchBranches()
})
this.$eventHub.$on('show-new-branch-dialog', () => {
+14 -12
View File
@@ -1,15 +1,16 @@
<template>
<div>
<portal v-if="canRenderToolbarPortal" to="toolbar">
<!-- Toolbar -->
<prioritized-portal to="toolbar" identity="commits" :priority="0">
<div class="font-weight-bold">
Your Latest Commits
<span v-if="user" class="caption">({{ user.commits.totalCount }})</span>
</div>
</portal>
</prioritized-portal>
<v-row v-if="user && user.commits.totalCount !== 0">
<v-col
v-for="commit in user.commits.items.filter((c) => c.branchName !== 'globals')"
v-for="commit in commitItems"
:key="commit.id"
cols="12"
sm="6"
@@ -61,21 +62,18 @@
</template>
<script>
import { gql } from '@apollo/client/core'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { computed, defineComponent } from 'vue'
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
export default {
export default defineComponent({
name: 'TheCommits',
components: {
InfiniteLoading: () => import('vue-infinite-loading'),
CommitPreviewCard: () => import('@/main/components/common/CommitPreviewCard'),
NoDataPlaceholder: () => import('@/main/components/common/NoDataPlaceholder')
NoDataPlaceholder: () => import('@/main/components/common/NoDataPlaceholder'),
PrioritizedPortal
},
mixins: [buildPortalStateMixin([STANDARD_PORTAL_KEYS.Toolbar], 'commits', 0)],
setup() {
const { result, fetchMore: userFetchMore } = useQuery(gql`
query ($cursor: String) {
@@ -101,9 +99,13 @@ export default {
}
`)
const user = computed(() => result.value?.user)
const commitItems = computed(() =>
(user.value?.commits.items || []).filter((c) => c.branchName !== 'globals')
)
return {
user,
commitItems,
userFetchMore
}
},
@@ -123,5 +125,5 @@ export default {
}
}
}
}
})
</script>
@@ -1,7 +1,15 @@
<template>
<div>
<commit-multi-select-toolbar
v-if="hasSelectedCommits"
:selected-commit-ids="selectedCommitIds"
:stream-id="streamId"
:branch-name="branchName"
@clear="clearSelectedCommits"
@finish="onBatchCommitActionFinish"
/>
<branch-toolbar
v-if="canRenderToolbarPortal && stream && stream.branch"
v-else-if="canRenderToolbarPortal && stream && stream.branch"
:stream="stream"
@edit-branch="branchEditDialog = true"
/>
@@ -25,7 +33,7 @@
</v-row>
<v-row v-if="!listMode">
<v-col
v-for="(commit, index) in allCommits"
v-for="commit in allCommits"
:key="commit.id + 'card'"
cols="12"
sm="6"
@@ -35,7 +43,8 @@
<commit-preview-card
:commit="commit"
:show-stream-and-branch="false"
:highlight="index === 0"
:allow-select="isStreamOwner || isCommitOwner(commit)"
:selected.sync="selectedCommitsState[commit.id]"
/>
</v-col>
</v-row>
@@ -100,14 +109,16 @@
<script>
import { gql } from '@apollo/client/core'
import branchQuery from '@/graphql/branch.gql'
import {
STANDARD_PORTAL_KEYS,
buildPortalStateMixin
} from '@/main/utils/portalStateManager'
import { STANDARD_PORTAL_KEYS, usePortalState } from '@/main/utils/portalStateManager'
import { useQuery } from '@vue/apollo-composable'
import { computed } from 'vue'
import { useRoute } from '@/main/lib/core/composables/router'
import { AppLocalStorage } from '@/utils/localStorage'
import { useCommitMultiActions } from '@/main/lib/stream/composables/commitMultiActions'
import CommitMultiSelectToolbar from '@/main/components/stream/commit/CommitMultiSelectToolbar.vue'
import { Roles } from '@/helpers/mainConstants'
import { useEventHub, useIsLoggedIn } from '@/main/lib/core/composables/core'
import { StreamEvents } from '@/main/lib/core/helpers/eventHubHelper'
export default {
name: 'TheBranch',
@@ -118,11 +129,31 @@ export default {
ListItemCommit: () => import('@/main/components/stream/ListItemCommit'),
BranchEditDialog: () => import('@/main/dialogs/BranchEditDialog'),
BranchToolbar: () => import('@/main/toolbars/BranchToolbar'),
CommitPreviewCard: () => import('@/main/components/common/CommitPreviewCard')
CommitPreviewCard: () => import('@/main/components/common/CommitPreviewCard'),
CommitMultiSelectToolbar
},
mixins: [buildPortalStateMixin([STANDARD_PORTAL_KEYS.Toolbar], 'stream-branch', 1)],
setup() {
const eventHub = useEventHub()
const route = useRoute()
const streamId = computed(() => route.params.streamId)
const branchName = computed(() => (route.params.branchName || '').toLowerCase())
const { canRenderToolbarPortal } = usePortalState(
[STANDARD_PORTAL_KEYS.Toolbar],
'stream-branch',
1
)
const { userId } = useIsLoggedIn()
const {
selectedCommitIds,
hasSelectedCommits,
clearSelectedCommits,
selectedCommitsState
} = useCommitMultiActions()
const {
result,
fetchMore: streamFetchMore,
@@ -131,19 +162,41 @@ export default {
} = useQuery(
branchQuery,
() => ({
streamId: route.params.streamId,
branchName: (route.params.branchName || '').toLowerCase(),
streamId: streamId.value,
branchName: branchName.value,
cursor: null
}),
{ fetchPolicy: 'network-only' }
)
const stream = computed(() => result.value?.stream)
const isStreamOwner = computed(() => stream.value.role === Roles.Stream.Owner)
const isCommitOwner = (commit) => userId.value && commit.authorId === userId.value
const onBatchCommitActionFinish = () => {
// refetch the main branch query
streamRefetch()
// refetch stream & branches
eventHub.$emit(StreamEvents.Refetch)
eventHub.$emit(StreamEvents.RefetchBranches)
}
return {
stream,
streamFetchMore,
streamRefetch,
streamLoading
streamLoading,
streamId,
branchName,
selectedCommitIds,
hasSelectedCommits,
clearSelectedCommits,
selectedCommitsState,
canRenderToolbarPortal,
isStreamOwner,
isCommitOwner,
onBatchCommitActionFinish
}
},
data() {
@@ -210,9 +263,6 @@ export default {
loggedInUserId() {
return AppLocalStorage.get('uuid')
},
streamId() {
return this.$route.params.streamId
},
latestCommitObjectUrl() {
if ((this.stream?.branch?.commits?.items || []).length > 0)
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.branch.commits.items[0].referencedObject}`
@@ -52,7 +52,7 @@ type CommitCollectionUserNode {
type BranchCollection {
totalCount: Int!
cursor: String
items: [Branch]
items: [Branch!]
}
type CommitCollection {
@@ -90,6 +90,20 @@ extend type Mutation {
commitDelete(commit: CommitDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Move a batch of commits to a new branch
"""
commitsMove(input: CommitsMoveInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Delete a batch of commits
"""
commitsDelete(input: CommitsDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
}
extend type Subscription {
@@ -161,7 +175,7 @@ input CommitCreateInput {
"""
**DEPRECATED** Use the `parents` field.
"""
previousCommitIds: [String]
previousCommitIds: [String] @deprecated
parents: [String]
}
@@ -186,3 +200,12 @@ input CommitDeleteInput {
streamId: String!
id: String!
}
input CommitsMoveInput {
targetBranch: String!
commitIds: [String!]!
}
input CommitsDeleteInput {
commitIds: [String!]!
}
+33
View File
@@ -96,6 +96,8 @@ function buildTableHelper<T extends string, C extends string>(
*
* Streams.with({...}) - configure helper, e.g. disable table name being prefixed to col names:
* Streams.with({withoutTablePrefix: true}).col.id
*
* Streams.withoutTablePrefix.col.id - Shorthand for accessing columns without the table prefix
*/
export const Streams = buildTableHelper('streams', [
@@ -240,4 +242,35 @@ export const UserNotificationPreferences = buildTableHelper(
['userId', 'preferences']
)
export const Commits = buildTableHelper('commits', [
'id',
'referencedObject',
'author',
'message',
'createdAt',
'sourceApplication',
'totalChildrenCount',
'parents'
])
export const StreamCommits = buildTableHelper('stream_commits', [
'streamId',
'commitId'
])
export const BranchCommits = buildTableHelper('branch_commits', [
'branchId',
'commitId'
])
export const Branches = buildTableHelper('branches', [
'id',
'streamId',
'authorId',
'name',
'description',
'createdAt',
'updatedAt'
])
export { knex }
@@ -0,0 +1,11 @@
import { BaseError } from '@/modules/shared/errors'
export class CommitInvalidAccessError extends BaseError {
static defaultMessage = 'User does not have access to the specified commit'
static code = 'COMMIT_INVALID_ACCESS_ERROR'
}
export class CommitBatchUpdateError extends BaseError {
static defaultMessage = 'An issue occurred while batch updating commits'
static code = 'COMMIT_BATCH_UPDATE_ERROR'
}
@@ -165,7 +165,7 @@ export type BranchCommitsArgs = {
export type BranchCollection = {
__typename?: 'BranchCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Branch>>>;
items?: Maybe<Array<Branch>>;
totalCount: Scalars['Int'];
};
@@ -340,7 +340,10 @@ export type CommitCreateInput = {
message?: InputMaybe<Scalars['String']>;
objectId: Scalars['String'];
parents?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
/** **DEPRECATED** Use the `parents` field. */
/**
* **DEPRECATED** Use the `parents` field.
* @deprecated Field no longer supported
*/
previousCommitIds?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
sourceApplication?: InputMaybe<Scalars['String']>;
streamId: Scalars['String'];
@@ -367,6 +370,15 @@ export type CommitUpdateInput = {
streamId: Scalars['String'];
};
export type CommitsDeleteInput = {
commitIds: Array<Scalars['String']>;
};
export type CommitsMoveInput = {
commitIds: Array<Scalars['String']>;
targetBranch: Scalars['String'];
};
export enum DiscoverableStreamsSortType {
CreatedDate = 'CREATED_DATE',
FavoritesCount = 'FAVORITES_COUNT'
@@ -446,6 +458,10 @@ export type Mutation = {
commitDelete: Scalars['Boolean'];
commitReceive: Scalars['Boolean'];
commitUpdate: Scalars['Boolean'];
/** Delete a batch of commits */
commitsDelete: Scalars['Boolean'];
/** Move a batch of commits to a new branch */
commitsMove: Scalars['Boolean'];
/** Delete a pending invite */
inviteDelete: Scalars['Boolean'];
/** Re-send a pending invite */
@@ -599,6 +615,16 @@ export type MutationCommitUpdateArgs = {
};
export type MutationCommitsDeleteArgs = {
input: CommitsDeleteInput;
};
export type MutationCommitsMoveArgs = {
input: CommitsMoveInput;
};
export type MutationInviteDeleteArgs = {
inviteId: Scalars['String'];
};
@@ -1701,7 +1727,7 @@ export type ResolversTypes = {
BlobMetadataCollection: ResolverTypeWrapper<BlobMetadataCollection>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
Branch: ResolverTypeWrapper<Omit<Branch, 'author'> & { author?: Maybe<ResolversTypes['User']> }>;
BranchCollection: ResolverTypeWrapper<Omit<BranchCollection, 'items'> & { items?: Maybe<Array<Maybe<ResolversTypes['Branch']>>> }>;
BranchCollection: ResolverTypeWrapper<Omit<BranchCollection, 'items'> & { items?: Maybe<Array<ResolversTypes['Branch']>> }>;
BranchCreateInput: BranchCreateInput;
BranchDeleteInput: BranchDeleteInput;
BranchUpdateInput: BranchUpdateInput;
@@ -1719,6 +1745,8 @@ export type ResolversTypes = {
CommitDeleteInput: CommitDeleteInput;
CommitReceivedInput: CommitReceivedInput;
CommitUpdateInput: CommitUpdateInput;
CommitsDeleteInput: CommitsDeleteInput;
CommitsMoveInput: CommitsMoveInput;
DateTime: ResolverTypeWrapper<Scalars['DateTime']>;
DiscoverableStreamsSortType: DiscoverableStreamsSortType;
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
@@ -1794,7 +1822,7 @@ export type ResolversParentTypes = {
BlobMetadataCollection: BlobMetadataCollection;
Boolean: Scalars['Boolean'];
Branch: Omit<Branch, 'author'> & { author?: Maybe<ResolversParentTypes['User']> };
BranchCollection: Omit<BranchCollection, 'items'> & { items?: Maybe<Array<Maybe<ResolversParentTypes['Branch']>>> };
BranchCollection: Omit<BranchCollection, 'items'> & { items?: Maybe<Array<ResolversParentTypes['Branch']>> };
BranchCreateInput: BranchCreateInput;
BranchDeleteInput: BranchDeleteInput;
BranchUpdateInput: BranchUpdateInput;
@@ -1812,6 +1840,8 @@ export type ResolversParentTypes = {
CommitDeleteInput: CommitDeleteInput;
CommitReceivedInput: CommitReceivedInput;
CommitUpdateInput: CommitUpdateInput;
CommitsDeleteInput: CommitsDeleteInput;
CommitsMoveInput: CommitsMoveInput;
DateTime: Scalars['DateTime'];
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
EmailAddress: Scalars['EmailAddress'];
@@ -1993,7 +2023,7 @@ export type BranchResolvers<ContextType = GraphQLContext, ParentType extends Res
export type BranchCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['BranchCollection'] = ResolversParentTypes['BranchCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Maybe<Array<Maybe<ResolversTypes['Branch']>>>, ParentType, ContextType>;
items?: Resolver<Maybe<Array<ResolversTypes['Branch']>>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -2140,6 +2170,8 @@ export type MutationResolvers<ContextType = GraphQLContext, ParentType extends R
commitDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitDeleteArgs, 'commit'>>;
commitReceive?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitReceiveArgs, 'input'>>;
commitUpdate?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitUpdateArgs, 'commit'>>;
commitsDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitsDeleteArgs, 'input'>>;
commitsMove?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitsMoveArgs, 'input'>>;
inviteDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationInviteDeleteArgs, 'inviteId'>>;
inviteResend?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationInviteResendArgs, 'inviteId'>>;
objectCreate?: Resolver<Array<Maybe<ResolversTypes['String']>>, ParentType, ContextType, RequireFields<MutationObjectCreateArgs, 'objectInput'>>;
@@ -27,12 +27,17 @@ const { getStream } = require('../../services/streams')
const { getUser } = require('../../services/users')
const { respectsLimits } = require('../../services/ratelimits')
const {
batchMoveCommits,
batchDeleteCommits
} = require('@/modules/core/services/commit/batchCommitActions')
// subscription events
const COMMIT_CREATED = 'COMMIT_CREATED'
const COMMIT_UPDATED = 'COMMIT_UPDATED'
const COMMIT_DELETED = 'COMMIT_DELETED'
/** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */
module.exports = {
Query: {},
Stream: {
@@ -245,6 +250,16 @@ module.exports = {
}
return deleted
},
async commitsMove(_, args, ctx) {
await batchMoveCommits(args.input, ctx.userId)
return true
},
async commitsDelete(_, args, ctx) {
await batchDeleteCommits(args.input, ctx.userId)
return true
}
},
Subscription: {
@@ -74,3 +74,34 @@ export type ServerInfo = ServerConfigRecord & {
*/
version: string
}
export type CommitRecord = {
id: string
referencedObject: string
author: string
message: string
createdAt: Date
sourceApplication: Nullable<string>
totalChildrenCount: Nullable<number>
parents: Nullable<string>
}
export type BranchCommitRecord = {
branchId: string
commitId: string
}
export type StreamCommitRecord = {
streamId: string
commitId: string
}
export type BranchRecord = {
id: string
streamId: string
authorId: string
name: string
description: Nullable<string>
createdAt: Date
updatedAt: Date
}
@@ -0,0 +1,18 @@
import { Knex } from 'knex'
const TABLE_NAME = 'branches'
export async function up(knex: Knex): Promise<void> {
// delete all invalid branches (null name - shouldnt even exist)
await knex.table(TABLE_NAME).whereNull('name').del()
await knex.schema.alterTable(TABLE_NAME, (table) => {
table.string('name', 512).notNullable().alter()
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
table.string('name', 512).nullable().alter()
})
}
@@ -0,0 +1,12 @@
import { Branches, knex } from '@/modules/core/dbSchema'
import { BranchRecord } from '@/modules/core/helpers/types'
export async function getStreamBranchByName(streamId: string, name: string) {
if (!streamId || !name) return null
const q = Branches.knex<BranchRecord>()
.where(Branches.col.streamId, streamId)
.andWhere(knex.raw('LOWER(name) = ?', [name.toLowerCase()]))
return await q.first()
}
@@ -0,0 +1,71 @@
import {
BranchCommits,
Branches,
Commits,
StreamCommits
} from '@/modules/core/dbSchema'
import { BranchCommitRecord, CommitRecord } from '@/modules/core/helpers/types'
import { uniqBy } from 'lodash'
const CommitWithStreamBranchMetadataFields = [
...Commits.cols,
StreamCommits.col.streamId,
BranchCommits.col.branchId,
`${Branches.col.name} as branchName`
]
export type CommitWithStreamBranchMetadata = CommitRecord & {
streamId: string
branchId: string
branchName: string
}
/**
* Get commits with their stream and branch IDs
*/
export async function getCommits(commitIds: string[]) {
const q = Commits.knex()
.select<CommitWithStreamBranchMetadata[]>(CommitWithStreamBranchMetadataFields)
.whereIn(Commits.col.id, commitIds)
.leftJoin(StreamCommits.name, StreamCommits.col.commitId, Commits.col.id)
.leftJoin(BranchCommits.name, BranchCommits.col.commitId, Commits.col.id)
.innerJoin(Branches.name, Branches.col.id, BranchCommits.col.branchId)
const rows = await q
// in case the join tables have multiple values for each commit
// (shouldnt happen, but the schema allows for it)
const uniqueRows = uniqBy(rows, (r) => r.id)
return uniqueRows
}
/**
* Move all commits to the specified branch
* Note: Make sure to validate beforehand that the branch ID belongs to the
* same stream etc. THIS DOESN'T DO ANY VALIDATION!
* @returns The amount of commits that were moved
*/
export async function moveCommitsToBranch(commitIds: string[], branchId: string) {
if (!commitIds?.length) return
// delete old branch commits
await BranchCommits.knex().whereIn(BranchCommits.col.commitId, commitIds).del()
// insert new ones
const inserts = await BranchCommits.knex().insert(
commitIds.map(
(cId): BranchCommitRecord => ({
branchId,
commitId: cId
})
),
'*'
)
return inserts.length
}
export async function deleteCommits(commitIds: string[]) {
return await Commits.knex().whereIn(Commits.col.id, commitIds).del()
}
@@ -20,7 +20,7 @@ import {
QueryDiscoverableStreamsArgs,
SortDirection
} from '@/modules/core/graph/generated/graphql'
import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { Nullable, Optional } from '@/modules/shared/helpers/typeHelper'
import { decodeCursor, encodeCursor } from '@/modules/shared/helpers/graphqlHelper'
import dayjs from 'dayjs'
import { UserWithOptionalRole } from '@/modules/core/repositories/users'
@@ -42,25 +42,16 @@ export const STREAM_WITH_OPTIONAL_ROLE_COLUMNS = [...Streams.cols, StreamAcl.col
export const generateId = () => cryptoRandomString({ length: 10 })
/**
* Get multiple streams
* @param {string[]} streamIds
* Get multiple streams. If userId is specified, the role will be resolved as well.
*/
export async function getStreams(streamIds: string[]) {
if (!streamIds?.length) throw new InvalidArgumentError('Invalid stream IDs')
const q = Streams.knex<StreamRecord[]>().whereIn(Streams.col.id, streamIds)
return await q
}
export async function getStreams(
streamIds: string[],
options: Partial<{ userId: string }> = {}
) {
const { userId } = options
if (!streamIds?.length) throw new InvalidArgumentError('Empty stream IDs')
/**
* Get a single stream. If userId is specified, the role will be resolved as well.
*/
export async function getStream(params: { streamId: string; userId?: string }) {
const { streamId, userId } = params
if (!streamId) throw new InvalidArgumentError('Invalid stream ID')
const q = Streams.knex<StreamWithOptionalRole[]>().where({
[Streams.col.id]: streamId
})
const q = Streams.knex<StreamWithOptionalRole[]>().whereIn(Streams.col.id, streamIds)
if (userId) {
q.select([
@@ -74,11 +65,21 @@ export async function getStream(params: { streamId: string; userId?: string }) {
userId
)
})
q.groupBy(Streams.col.id) //
q.groupBy(Streams.col.id)
}
const res = await q.first()
return res
return await q
}
/**
* Get a single stream. If userId is specified, the role will be resolved as well.
*/
export async function getStream(params: { streamId: string; userId?: string }) {
const { streamId, userId } = params
if (!streamId) throw new InvalidArgumentError('Invalid stream ID')
const streams = await getStreams([streamId], { userId })
return <Optional<StreamWithOptionalRole>>streams[0]
}
/**
@@ -1,6 +1,7 @@
'use strict'
const crs = require('crypto-random-string')
const knex = require('@/db/knex')
const { getStreamBranchByName } = require('@/modules/core/repositories/branches')
const Streams = () => knex('streams')
const Branches = () => knex('branches')
@@ -66,12 +67,7 @@ module.exports = {
},
async getBranchByNameAndStreamId({ streamId, name }) {
const query = Branches()
.select('*')
.where({ streamId })
.andWhere(knex.raw('LOWER(name) = ?', [name.toLowerCase()]))
.first()
return await query
return await getStreamBranchByName(streamId, name)
},
async deleteBranchById({ id, streamId }) {
@@ -0,0 +1,135 @@
import {
CommitInvalidAccessError,
CommitBatchUpdateError
} from '@/modules/core/errors/commit'
import {
CommitsDeleteInput,
CommitsMoveInput
} from '@/modules/core/graph/generated/graphql'
import { Roles } from '@/modules/core/helpers/mainConstants'
import { getStreamBranchByName } from '@/modules/core/repositories/branches'
import {
deleteCommits,
getCommits,
moveCommitsToBranch
} from '@/modules/core/repositories/commits'
import { getStreams } from '@/modules/core/repositories/streams'
import { ensureError } from '@/modules/shared/helpers/errorHelper'
import { difference, groupBy, keyBy } from 'lodash'
type CommitBatchInput = CommitsMoveInput | CommitsDeleteInput
/**
* Do base validation that's going to be appropriate for all batch actions and return
* the DB entities that were tested
*/
async function validateBatchBaseRules(params: CommitBatchInput, userId: string) {
const { commitIds } = params
if (!userId) {
throw new CommitInvalidAccessError(
'User must be authenticate to operate with commits'
)
}
if (!commitIds?.length) {
throw new CommitBatchUpdateError('No commits specified')
}
const commits = await getCommits(commitIds)
const foundCommitIds = commits.map((c) => c.id)
if (
commitIds.length !== foundCommitIds.length ||
difference(commitIds, foundCommitIds).length > 0
) {
throw new CommitBatchUpdateError('At least one of the commits does not exist')
}
const streamGroups = groupBy(commits, (c) => c.streamId)
const streamIds = Object.keys(streamGroups)
const streams = await getStreams(streamIds, { userId })
if (
streamIds.length !== streams.length ||
difference(
streamIds,
streams.map((s) => s.id)
).length > 0
) {
throw new CommitBatchUpdateError("At least one commit stream wasn't found")
}
const streamsById = keyBy(streams, (s) => s.id)
const commitsWithStreams = commits.map((c) => ({
commit: c,
stream: streamsById[c.streamId]
}))
for (const { commit, stream } of commitsWithStreams) {
if (stream.role !== Roles.Stream.Owner && commit.author !== userId) {
throw new CommitInvalidAccessError(
'To operate on these commits you must either own them or their streams'
)
}
}
return { commitsWithStreams, commits, streams }
}
/**
* Validate batch move params
*/
async function validateCommitsMove(params: CommitsMoveInput, userId: string) {
const { targetBranch } = params
const { streams } = await validateBatchBaseRules(params, userId)
if (streams.length > 1) {
throw new CommitBatchUpdateError('Commits belong to different streams')
}
const stream = streams[0]
const branch = await getStreamBranchByName(stream.id, targetBranch)
if (!branch) {
throw new CommitBatchUpdateError('Invalid target branch')
}
return { stream, branch }
}
/**
* Validate batch delete params
*/
async function validateCommitsDelete(params: CommitsDeleteInput, userId: string) {
await validateBatchBaseRules(params, userId)
}
/**
* Move a batch of commits belonging to the same stream to another branch
*/
export async function batchMoveCommits(params: CommitsMoveInput, userId: string) {
const { commitIds } = params
const { branch } = await validateCommitsMove(params, userId)
try {
await moveCommitsToBranch(commitIds, branch.id)
} catch (e) {
const err = ensureError(e)
throw new CommitBatchUpdateError('Batch commit move failed', { cause: err })
}
}
/**
* Delete a batch of commits
*/
export async function batchDeleteCommits(params: CommitsDeleteInput, userId: string) {
const { commitIds } = params
await validateCommitsDelete(params, userId)
try {
await deleteCommits(commitIds)
} catch (e) {
const err = ensureError(e)
throw new CommitBatchUpdateError('Batch commit delete failed', { cause: err })
}
}
@@ -0,0 +1,287 @@
import { Commits, Streams, Users } from '@/modules/core/dbSchema'
import { Roles } from '@/modules/core/helpers/mainConstants'
import { getCommits } from '@/modules/core/repositories/commits'
import { createBranch } from '@/modules/core/services/branches'
import { addOrUpdateStreamCollaborator } from '@/modules/core/services/streams/streamAccessService'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
import { deleteCommits, moveCommits } from '@/test/graphql/commits'
import { truncateTables } from '@/test/hooks'
import {
buildAuthenticatedApolloServer,
buildUnauthenticatedApolloServer
} from '@/test/serverHelper'
import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper'
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
import { ApolloServer } from 'apollo-server-express'
import { expect } from 'chai'
import { times } from 'lodash'
enum BatchActionType {
Move,
Delete
}
const cleanup = async () => {
await truncateTables([Streams.name, Users.name, Commits.name])
}
describe('Batch commits', () => {
const userCommmitCount = 10
const secondBranchName = 'second'
const me: BasicTestUser = {
name: 'batch commit dude',
email: 'batchcommitguy@gmail.com',
id: ''
}
const otherGuy: BasicTestUser = {
name: 'other batch commit guy',
email: 'otherbatchcommitguy@gmail.com',
id: ''
}
const myStream: BasicTestStream = {
name: 'my first test stream',
isPublic: false,
ownerId: '',
id: ''
}
const otherStream: BasicTestStream = {
name: 'other guys first test stream',
isPublic: false,
ownerId: '',
id: ''
}
let myCommits: BasicTestCommit[]
let otherCommits: BasicTestCommit[]
before(async () => {
await cleanup()
await createTestUsers([me, otherGuy])
await createTestStreams([
[myStream, me],
[otherStream, otherGuy]
])
await Promise.all([
// create another branch for each stream
createBranch({
name: secondBranchName,
description: '',
streamId: myStream.id,
authorId: me.id
}),
createBranch({
name: secondBranchName,
description: '',
streamId: otherStream.id,
authorId: otherGuy.id
}),
// add users as contributors to each others streams
addOrUpdateStreamCollaborator(
otherStream.id,
me.id,
Roles.Stream.Contributor,
otherGuy.id
)
])
myCommits = times(
userCommmitCount,
(i): BasicTestCommit => ({
id: '',
objectId: '',
streamId: i % 2 === 0 ? myStream.id : otherStream.id,
authorId: me.id
})
)
otherCommits = times(
userCommmitCount,
(): BasicTestCommit => ({
id: '',
objectId: '',
streamId: otherStream.id,
authorId: otherGuy.id
})
)
await createTestCommits([...myCommits, ...otherCommits])
})
const batchActionDataSet = [
{ display: 'move', type: BatchActionType.Move },
{ display: 'delete', type: BatchActionType.Delete }
]
const buildBatchActionInvoker =
(apollo: ApolloServer) => (type: BatchActionType, commitIds: string[]) => {
if (type === BatchActionType.Delete) {
return deleteCommits(apollo, { input: { commitIds } })
} else if (type === BatchActionType.Move) {
return moveCommits(apollo, {
input: { commitIds, targetBranch: secondBranchName }
})
} else {
throw new Error('Unexpected batch action type')
}
}
type BatchActionInvoker = ReturnType<typeof buildBatchActionInvoker>
describe('when authenticated', () => {
let apollo: ApolloServer
let invokeBatchAction: BatchActionInvoker
before(async () => {
apollo = buildAuthenticatedApolloServer(me.id)
invokeBatchAction = buildBatchActionInvoker(apollo)
})
batchActionDataSet.forEach(({ display, type }) => {
it(`can't batch ${display} commits if not commit or stream author`, async () => {
const result = await invokeBatchAction(
type,
otherCommits.map((c) => c.id)
)
expect(result).to.haveGraphQLErrors('you must either own them or their streams')
})
it(`can't batch ${display} an empty commit array`, async () => {
const result = await invokeBatchAction(type, [])
expect(result).to.haveGraphQLErrors('No commits specified')
})
it(`can't batch ${display} commits if at least one is nonexistant`, async () => {
const result = await invokeBatchAction(type, [
...myCommits.map((c) => c.id),
'aaaaaaaa'
])
expect(result).to.haveGraphQLErrors('one of the commits does not exist')
})
})
describe('and deleting commits', async () => {
const deletableCommitCount = 5
let myDeletableCommits: BasicTestCommit[]
beforeEach(async () => {
myDeletableCommits = times(
deletableCommitCount,
(i): BasicTestCommit => ({
id: '',
objectId: '',
streamId: i % 2 === 0 ? myStream.id : otherStream.id,
authorId: me.id
})
)
await createTestCommits(myDeletableCommits)
})
const invokeDelete = (commitIds: string[]) =>
deleteCommits(apollo, { input: { commitIds } })
const validateDeleted = async (commitIds: string[]) => {
const commits = await getCommits(commitIds)
expect(commits).to.be.empty
}
it('can do it for commits of multiple streams', async () => {
const commitIds = myDeletableCommits.map((c) => c.id)
const result = await invokeDelete(commitIds)
expect(result).to.not.haveGraphQLErrors()
await validateDeleted(commitIds)
})
})
describe('and moving commits', async () => {
const movableCommitCount = 5
let myMovableCommits: BasicTestCommit[]
before(async () => {
myMovableCommits = times(
movableCommitCount,
(i): BasicTestCommit => ({
id: '',
objectId: '',
streamId: i % 2 === 0 ? myStream.id : otherStream.id,
authorId: me.id
})
)
await createTestCommits(myMovableCommits)
})
const invokeMove = (commitIds: string[], targetBranch = secondBranchName) =>
moveCommits(apollo, { input: { commitIds, targetBranch } })
const validateMoved = async (
commitIds: string[],
targetBranch = secondBranchName
) => {
const commits = await getCommits(commitIds)
const areAllMoved =
commits.length === commitIds.length &&
commits.every((c) => c.branchName === targetBranch)
expect(areAllMoved).to.be.true
}
it("can't do it for commits belonging to multiple streams", async () => {
const commitIds = myMovableCommits.map((c) => c.id)
const result = await invokeMove(commitIds)
expect(result).to.haveGraphQLErrors('commits belong to different streams')
})
it("can't do it when specifying a nonexistant target branch", async () => {
const commitIds = myMovableCommits
.filter((c) => c.streamId === myStream.id)
.map((c) => c.id)
const result = await invokeMove(commitIds, 'some-nonexistant-stream')
expect(result).to.haveGraphQLErrors('invalid target branch')
})
it('can do it with commits belonging to the same stream', async () => {
const commitIds = myMovableCommits
.filter((c) => c.streamId === myStream.id)
.map((c) => c.id)
const result = await invokeMove(commitIds)
expect(result).to.not.haveGraphQLErrors()
await validateMoved(commitIds)
})
})
})
describe('when not authenticated', () => {
let apollo: ApolloServer
let invokeBatchAction: BatchActionInvoker
before(async () => {
apollo = buildUnauthenticatedApolloServer()
invokeBatchAction = buildBatchActionInvoker(apollo)
})
batchActionDataSet.forEach(({ display, type }) => {
it(`can't batch ${display} commits`, async () => {
const result = await invokeBatchAction(
type,
myCommits.map((c) => c.id)
)
expect(result).to.haveGraphQLErrors('you do not have the required privileges')
})
})
})
})
@@ -20,6 +20,7 @@ const {
getCommitsByUserId,
getCommitsTotalCountByUserId
} = require('../services/commits')
const { times } = require('lodash')
describe('Commits @core-commits', () => {
const user = {
@@ -48,54 +49,92 @@ describe('Commits @core-commits', () => {
baz: 'qux5'
}
const generateObject = async (streamId = stream.id, object = testObject) =>
await createObject(streamId, object)
const generateStream = async (streamBase = stream, ownerId = user.id) =>
await createStream({ ...streamBase, ownerId })
let commitId1, commitId2, commitId3
before(async () => {
await beforeEachContext()
user.id = await createUser(user)
stream.id = await createStream({ ...stream, ownerId: user.id })
testObject.id = await createObject(stream.id, testObject)
testObject2.id = await createObject(stream.id, testObject2)
testObject3.id = await createObject(stream.id, testObject3)
})
const testObjectId = await createObject(stream.id, testObject)
const testObject2Id = await createObject(stream.id, testObject2)
const testObject3Id = await createObject(stream.id, testObject3)
let commitId1, commitId2, commitId3
it('Should create a commit by branch name', async () => {
commitId1 = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'first commit',
sourceApplication: 'tests',
objectId: testObject.id,
objectId: testObjectId,
authorId: user.id
})
expect(commitId1).to.be.a.string
})
it('Should create a commit with a previous commit id', async () => {
commitId2 = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'second commit',
sourceApplication: 'tests',
objectId: testObject2.id,
objectId: testObject2Id,
authorId: user.id,
parents: [commitId1]
})
expect(commitId2).to.be.a.string
commitId3 = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'third commit',
sourceApplication: 'tests',
objectId: testObject3.id,
objectId: testObject3Id,
authorId: user.id,
parents: [commitId1, commitId2]
})
})
it('Should create a commit by branch name', async () => {
const objectId = await generateObject()
const id = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'first commit',
sourceApplication: 'tests',
objectId,
authorId: user.id
})
expect(id).to.be.a.string
})
it('Should create a commit with a previous commit id', async () => {
const objectId = await generateObject()
const objectId2 = await generateObject()
const id = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'second commit',
sourceApplication: 'tests',
objectId,
authorId: user.id,
parents: [commitId1]
})
expect(id).to.be.a.string
const id2 = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'third commit',
sourceApplication: 'tests',
objectId: objectId2,
authorId: user.id,
parents: [commitId1, commitId2]
})
expect(commitId3).to.be.a.string
expect(id2).to.be.a.string
})
it('Should update a commit', async () => {
@@ -104,12 +143,13 @@ describe('Commits @core-commits', () => {
})
it('Should delete a commit', async () => {
const objectId = await generateObject()
const tempCommit = await createCommitByBranchName({
streamId: stream.id,
branchName: 'main',
message: 'temp commit',
sourceApplication: 'tests',
objectId: testObject.id,
objectId,
authorId: user.id
})
@@ -123,12 +163,14 @@ describe('Commits @core-commits', () => {
expect(cm.authorId).to.equal(user.id)
})
it('Should get the commits from a branch', async () => {
it('Should get the commits and their total count from a branch', async () => {
const streamId = await generateStream()
for (let i = 0; i < 10; i++) {
const t = { qux: i }
t.id = await createObject(stream.id, t)
t.id = await createObject(streamId, t)
await createCommitByBranchName({
streamId: stream.id,
streamId,
branchName: 'main',
message: `commit # ${i + 3}`,
sourceApplication: 'tests',
@@ -138,7 +180,7 @@ describe('Commits @core-commits', () => {
}
const { commits, cursor } = await getCommitsByBranchName({
streamId: stream.id,
streamId,
branchName: 'main',
limit: 2
})
@@ -146,30 +188,29 @@ describe('Commits @core-commits', () => {
expect(commits.length).to.equal(2)
const { commits: commits2 } = await getCommitsByBranchName({
streamId: stream.id,
streamId,
branchName: 'main',
limit: 5,
cursor
})
expect(commits2.length).to.equal(5)
})
it('Should get the commit count from a branch', async () => {
const c = await getCommitsTotalCountByBranchName({
streamId: stream.id,
streamId,
branchName: 'main'
})
expect(c).to.equal(13)
expect(c).to.equal(10)
})
it('Should get the commits from a stream', async () => {
await createBranch({ name: 'dim/dev', streamId: stream.id, authorId: user.id })
it('Should get the commits and their total count from a stream', async () => {
const streamId = await generateStream()
await createBranch({ name: 'dim/dev', streamId, authorId: user.id })
for (let i = 0; i < 10; i++) {
for (let i = 0; i < 15; i++) {
const t = { thud: i }
t.id = await createObject(stream.id, t)
t.id = await createObject(streamId, t)
await createCommitByBranchName({
streamId: stream.id,
streamId,
branchName: 'dim/dev',
message: `pushed something # ${i + 3}`,
sourceApplication: 'tests',
@@ -179,60 +220,106 @@ describe('Commits @core-commits', () => {
}
const { commits, cursor } = await getCommitsByStreamId({
streamId: stream.id,
streamId,
limit: 10
})
const { commits: commits2 } = await getCommitsByStreamId({
streamId: stream.id,
streamId,
limit: 20,
cursor
})
expect(commits.length).to.equal(10)
expect(commits2.length).to.equal(13)
expect(commits2.length).to.equal(5)
const c = await getCommitsTotalCountByStreamId({ streamId })
expect(c).to.equal(15)
})
it('Should get the commit count of a stream', async () => {
const c = await getCommitsTotalCountByStreamId({ streamId: stream.id })
expect(c).to.equal(23)
})
describe('when reading user commits', async () => {
const otherUser = {
name: 'Dimitrie Other',
email: 'otthhherrdidimitrie4342@gmail.com',
password: 'sn3aky-1337-b1m'
}
it('Should get the commits of a user', async () => {
const { commits, cursor } = await getCommitsByUserId({ userId: user.id, limit: 3 })
const otherStream = {
name: 'Other Test Stream References',
description: 'Whatever goes in here usually...'
}
const { commits: commits2 } = await getCommitsByUserId({
userId: user.id,
limit: 100,
cursor
const mainCommitCount = 16
before(async () => {
otherUser.id = await createUser(otherUser)
otherStream.id = await generateStream(otherStream, otherUser.id)
// create objects
const objectIds = await Promise.all(
times(mainCommitCount, () => generateObject(otherStream.id))
)
// create commits
await Promise.all(
objectIds.map((oid) =>
createCommitByBranchName({
streamId: otherStream.id,
branchName: 'main',
message: 'first commit',
sourceApplication: 'tests',
objectId: oid,
authorId: otherUser.id
})
)
)
})
expect(commits.length).to.equal(3)
expect(commits2.length).to.equal(20)
})
it('Should get the commits of a user', async () => {
const { commits, cursor } = await getCommitsByUserId({
userId: otherUser.id,
limit: 3
})
it('Should get the public commits of an user only', async () => {
const privateStreamId = await createStream({
name: 'private',
isPublic: false,
ownerId: user.id
})
const objectId = await createObject(privateStreamId, testObject)
await createCommitByBranchName({
streamId: privateStreamId,
branchName: 'main',
message: 'first commit',
sourceApplication: 'tests',
objectId,
authorId: user.id
const { commits: commits2 } = await getCommitsByUserId({
userId: otherUser.id,
limit: 100,
cursor
})
expect(commits.length).to.equal(3)
expect(commits2.length).to.equal(mainCommitCount - 3)
})
const { commits } = await getCommitsByUserId({ userId: user.id, limit: 1000 })
expect(commits.length).to.equal(23)
})
it('Should get the public commits of an user only', async () => {
const privateStreamId = await createStream({
name: 'private',
isPublic: false,
ownerId: otherUser.id
})
const objectId = await createObject(privateStreamId, testObject)
const commitId = await createCommitByBranchName({
streamId: privateStreamId,
branchName: 'main',
message: 'first commit',
sourceApplication: 'tests',
objectId,
authorId: otherUser.id
})
it('Should get the commit count of an user', async () => {
const c = await getCommitsTotalCountByUserId({ userId: user.id })
expect(c).to.equal(24)
const { commits } = await getCommitsByUserId({
userId: otherUser.id,
limit: 1000
})
expect(commits.length).to.equal(mainCommitCount)
// clean up
await deleteCommit({ id: commitId })
})
it('Should get the commit count of an user', async () => {
const c = await getCommitsTotalCountByUserId({ userId: otherUser.id })
expect(c).to.equal(mainCommitCount)
})
})
it('Commits should have source, total count, branch name and parents fields', async () => {
@@ -299,7 +299,7 @@ describe('Streams @core-streams', () => {
streamId: updatableStream.id,
name: 'dim/lol'
})
await deleteBranchById({ id: b.id, streamId: updatableStream.id })
await deleteBranchById({ id: b!.id, streamId: updatableStream.id })
const su2 = await getStream({ streamId: updatableStream.id })
expect(su2?.updatedAt).to.be.ok
+87
View File
@@ -0,0 +1,87 @@
import {
DeleteCommitsMutation,
DeleteCommitsMutationVariables,
MoveCommitsMutation,
MoveCommitsMutationVariables,
ReadStreamBranchCommitsQuery,
ReadStreamBranchCommitsQueryVariables
} from '@/test/graphql/generated/graphql'
import { executeOperation } from '@/test/graphqlHelper'
import { ApolloServer, gql } from 'apollo-server-express'
const readStreamBranchCommitsQuery = gql`
query ReadStreamBranchCommits(
$streamId: String!
$branchName: String!
$cursor: String
$limit: Int! = 10
) {
stream(id: $streamId) {
id
name
role
branch(name: $branchName) {
id
name
description
commits(cursor: $cursor, limit: $limit) {
totalCount
cursor
items {
id
authorName
authorId
authorAvatar
sourceApplication
message
referencedObject
createdAt
commentCount
}
}
}
}
}
`
const moveCommitsMutation = gql`
mutation MoveCommits($input: CommitsMoveInput!) {
commitsMove(input: $input)
}
`
const deleteCommitsMutation = gql`
mutation DeleteCommits($input: CommitsDeleteInput!) {
commitsDelete(input: $input)
}
`
export const readStreamBranchCommits = (
apollo: ApolloServer,
variables: ReadStreamBranchCommitsQueryVariables
) =>
executeOperation<ReadStreamBranchCommitsQuery, ReadStreamBranchCommitsQueryVariables>(
apollo,
readStreamBranchCommitsQuery,
variables
)
export const moveCommits = (
apollo: ApolloServer,
variables: MoveCommitsMutationVariables
) =>
executeOperation<MoveCommitsMutation, MoveCommitsMutationVariables>(
apollo,
moveCommitsMutation,
variables
)
export const deleteCommits = (
apollo: ApolloServer,
variables: DeleteCommitsMutationVariables
) =>
executeOperation<DeleteCommitsMutation, DeleteCommitsMutationVariables>(
apollo,
deleteCommitsMutation,
variables
)
@@ -159,7 +159,7 @@ export type BranchCommitsArgs = {
export type BranchCollection = {
__typename?: 'BranchCollection';
cursor?: Maybe<Scalars['String']>;
items?: Maybe<Array<Maybe<Branch>>>;
items?: Maybe<Array<Branch>>;
totalCount: Scalars['Int'];
};
@@ -334,7 +334,10 @@ export type CommitCreateInput = {
message?: InputMaybe<Scalars['String']>;
objectId: Scalars['String'];
parents?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
/** **DEPRECATED** Use the `parents` field. */
/**
* **DEPRECATED** Use the `parents` field.
* @deprecated Field no longer supported
*/
previousCommitIds?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
sourceApplication?: InputMaybe<Scalars['String']>;
streamId: Scalars['String'];
@@ -361,6 +364,15 @@ export type CommitUpdateInput = {
streamId: Scalars['String'];
};
export type CommitsDeleteInput = {
commitIds: Array<Scalars['String']>;
};
export type CommitsMoveInput = {
commitIds: Array<Scalars['String']>;
targetBranch: Scalars['String'];
};
export enum DiscoverableStreamsSortType {
CreatedDate = 'CREATED_DATE',
FavoritesCount = 'FAVORITES_COUNT'
@@ -440,6 +452,10 @@ export type Mutation = {
commitDelete: Scalars['Boolean'];
commitReceive: Scalars['Boolean'];
commitUpdate: Scalars['Boolean'];
/** Delete a batch of commits */
commitsDelete: Scalars['Boolean'];
/** Move a batch of commits to a new branch */
commitsMove: Scalars['Boolean'];
/** Delete a pending invite */
inviteDelete: Scalars['Boolean'];
/** Re-send a pending invite */
@@ -593,6 +609,16 @@ export type MutationCommitUpdateArgs = {
};
export type MutationCommitsDeleteArgs = {
input: CommitsDeleteInput;
};
export type MutationCommitsMoveArgs = {
input: CommitsMoveInput;
};
export type MutationInviteDeleteArgs = {
inviteId: Scalars['String'];
};
@@ -1675,6 +1701,30 @@ export type GetCommentsQueryVariables = Exact<{
export type GetCommentsQuery = { __typename?: 'Query', comments?: { __typename?: 'CommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null }, replies?: { __typename?: 'CommentCollection', items: Array<{ __typename?: 'Comment', id: string, text: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, attachments?: Array<{ __typename?: 'BlobMetadata', id: string, fileName: string, streamId: string }> | null } }> } | null }> } | null };
export type ReadStreamBranchCommitsQueryVariables = Exact<{
streamId: Scalars['String'];
branchName: Scalars['String'];
cursor?: InputMaybe<Scalars['String']>;
limit?: Scalars['Int'];
}>;
export type ReadStreamBranchCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, name: string, role?: string | null, branch?: { __typename?: 'Branch', id: string, name: string, description?: string | null, commits?: { __typename?: 'CommitCollection', totalCount: number, cursor?: string | null, items?: Array<{ __typename?: 'Commit', id: string, authorName?: string | null, authorId?: string | null, authorAvatar?: string | null, sourceApplication?: string | null, message?: string | null, referencedObject: string, createdAt?: string | null, commentCount: number } | null> | null } | null } | null } | null };
export type MoveCommitsMutationVariables = Exact<{
input: CommitsMoveInput;
}>;
export type MoveCommitsMutation = { __typename?: 'Mutation', commitsMove: boolean };
export type DeleteCommitsMutationVariables = Exact<{
input: CommitsDeleteInput;
}>;
export type DeleteCommitsMutation = { __typename?: 'Mutation', commitsDelete: boolean };
export type CreateServerInviteMutationVariables = Exact<{
input: ServerInviteCreateInput;
}>;
+2 -2
View File
@@ -13,8 +13,8 @@ type TypedGraphqlResponse<R = Record<string, any>> = GraphQLResponse & {
* a properly typed response
*/
export async function executeOperation<
R = Record<string, any>,
V = Record<string, unknown>
R extends Record<string, any> = Record<string, any>,
V extends Record<string, any> = Record<string, any>
>(
apollo: ApolloServer,
query: DocumentNode,
@@ -0,0 +1,61 @@
import { createCommitByBranchName } from '@/modules/core/services/commits'
import { createObject } from '@/modules/core/services/objects'
export type BasicTestCommit = {
/**
* Can be left empty, will be filled on creation
*/
id: string
/**
* Can be left empty, will be filled on creation
*/
objectId: string
streamId: string
authorId: string
/**
* Defaults to 'main'
*/
branchName?: string
/**
* Auto-generated, if empty
*/
message?: string
/**
* Empty array by default
*/
parents?: string[]
}
/**
* Ensure all commits have objectId set
*/
async function ensureObjects(commits: BasicTestCommit[]) {
const commitsWithoutObjects = commits.filter((c) => !c.objectId)
await Promise.all(
commitsWithoutObjects.map((c) =>
createObject(c.streamId, { foo: 'bar' }).then((oid) => (c.objectId = oid))
)
)
}
/**
* Create test commits
*/
export async function createTestCommits(commits: BasicTestCommit[]) {
await ensureObjects(commits)
await Promise.all(
commits.map((c) =>
createCommitByBranchName({
streamId: c.streamId,
branchName: 'main',
message: c.message || 'this message is auto generated',
sourceApplication: 'tests',
objectId: c.objectId,
authorId: c.authorId,
totalChildrenCount: 0,
parents: c.parents || []
}).then((cid) => (c.id = cid))
)
)
}