Feat: Move settings from individual pages into one settings modal (#2502)
This commit is contained in:
@@ -18,7 +18,6 @@ export type Scalars = {
|
||||
BigInt: { input: any; output: any; }
|
||||
/** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */
|
||||
DateTime: { input: string; output: string; }
|
||||
EmailAddress: { input: any; output: any; }
|
||||
/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
|
||||
JSONObject: { input: {}; output: {}; }
|
||||
};
|
||||
@@ -201,6 +200,8 @@ export type AutomateAuthCodePayloadTest = {
|
||||
export type AutomateFunction = {
|
||||
__typename?: 'AutomateFunction';
|
||||
automationCount: Scalars['Int']['output'];
|
||||
/** Only returned if user is a part of this speckle server */
|
||||
creator?: Maybe<LimitedUser>;
|
||||
description: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
isFeatured: Scalars['Boolean']['output'];
|
||||
@@ -373,62 +374,6 @@ export type AutomationCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AutomationCreateInput = {
|
||||
automationId: Scalars['String']['input'];
|
||||
automationName: Scalars['String']['input'];
|
||||
automationRevisionId: Scalars['String']['input'];
|
||||
modelId: Scalars['String']['input'];
|
||||
projectId: Scalars['String']['input'];
|
||||
webhookId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AutomationFunctionRun = {
|
||||
__typename?: 'AutomationFunctionRun';
|
||||
contextView?: Maybe<Scalars['String']['output']>;
|
||||
elapsed: Scalars['Float']['output'];
|
||||
functionId: Scalars['String']['output'];
|
||||
functionLogo?: Maybe<Scalars['String']['output']>;
|
||||
functionName: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
resultVersions: Array<Version>;
|
||||
/**
|
||||
* NOTE: this is the schema for the results field below!
|
||||
* Current schema: {
|
||||
* version: "1.0.0",
|
||||
* values: {
|
||||
* objectResults: Record<str, {
|
||||
* category: string
|
||||
* level: ObjectResultLevel
|
||||
* objectIds: string[]
|
||||
* message: str | null
|
||||
* metadata: Records<str, unknown> | null
|
||||
* visualoverrides: Records<str, unknown> | null
|
||||
* }[]>
|
||||
* blobIds?: string[]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
results?: Maybe<Scalars['JSONObject']['output']>;
|
||||
status: AutomationRunStatus;
|
||||
statusMessage?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type AutomationMutations = {
|
||||
__typename?: 'AutomationMutations';
|
||||
create: Scalars['Boolean']['output'];
|
||||
functionRunStatusReport: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type AutomationMutationsCreateArgs = {
|
||||
input: AutomationCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type AutomationMutationsFunctionRunStatusReportArgs = {
|
||||
input: AutomationRunStatusUpdateInput;
|
||||
};
|
||||
|
||||
export type AutomationRevision = {
|
||||
__typename?: 'AutomationRevision';
|
||||
functions: Array<AutomationRevisionFunction>;
|
||||
@@ -452,44 +397,8 @@ export type AutomationRevisionFunction = {
|
||||
|
||||
export type AutomationRevisionTriggerDefinition = VersionCreatedTriggerDefinition;
|
||||
|
||||
export type AutomationRun = {
|
||||
__typename?: 'AutomationRun';
|
||||
automationId: Scalars['String']['output'];
|
||||
automationName: Scalars['String']['output'];
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
functionRuns: Array<AutomationFunctionRun>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Resolved from all function run statuses */
|
||||
status: AutomationRunStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
versionId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export enum AutomationRunStatus {
|
||||
Failed = 'FAILED',
|
||||
Initializing = 'INITIALIZING',
|
||||
Running = 'RUNNING',
|
||||
Succeeded = 'SUCCEEDED'
|
||||
}
|
||||
|
||||
export type AutomationRunStatusUpdateInput = {
|
||||
automationId: Scalars['String']['input'];
|
||||
automationRevisionId: Scalars['String']['input'];
|
||||
automationRunId: Scalars['String']['input'];
|
||||
functionRuns: Array<FunctionRunStatusInput>;
|
||||
versionId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AutomationRunTrigger = VersionCreatedTrigger;
|
||||
|
||||
export type AutomationsStatus = {
|
||||
__typename?: 'AutomationsStatus';
|
||||
automationRuns: Array<AutomationRun>;
|
||||
id: Scalars['ID']['output'];
|
||||
status: AutomationRunStatus;
|
||||
statusMessage?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type AvatarUser = {
|
||||
__typename?: 'AvatarUser';
|
||||
avatar?: Maybe<Scalars['String']['output']>;
|
||||
@@ -529,7 +438,10 @@ export type BlobMetadataCollection = {
|
||||
|
||||
export type Branch = {
|
||||
__typename?: 'Branch';
|
||||
/** All the recent activity on this branch in chronological order */
|
||||
/**
|
||||
* All the recent activity on this branch in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
activity?: Maybe<ActivityCollection>;
|
||||
author?: Maybe<User>;
|
||||
commits?: Maybe<CommitCollection>;
|
||||
@@ -744,7 +656,10 @@ export type CommentThreadActivityMessage = {
|
||||
|
||||
export type Commit = {
|
||||
__typename?: 'Commit';
|
||||
/** All the recent activity on this commit in chronological order */
|
||||
/**
|
||||
* All the recent activity on this commit in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
activity?: Maybe<ActivityCollection>;
|
||||
authorAvatar?: Maybe<Scalars['String']['output']>;
|
||||
authorId?: Maybe<Scalars['String']['output']>;
|
||||
@@ -760,6 +675,7 @@ export type Commit = {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
commentCount: Scalars['Int']['output'];
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
@@ -881,6 +797,16 @@ export type CreateModelInput = {
|
||||
projectId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type CreateVersionInput = {
|
||||
message?: InputMaybe<Scalars['String']['input']>;
|
||||
modelId: Scalars['String']['input'];
|
||||
objectId: Scalars['String']['input'];
|
||||
parents?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
projectId: Scalars['String']['input'];
|
||||
sourceApplication?: InputMaybe<Scalars['String']['input']>;
|
||||
totalChildrenCount?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type DeleteModelInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
projectId: Scalars['ID']['input'];
|
||||
@@ -934,27 +860,6 @@ export type FileUpload = {
|
||||
userId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FunctionRunStatusInput = {
|
||||
contextView?: InputMaybe<Scalars['String']['input']>;
|
||||
elapsed: Scalars['Float']['input'];
|
||||
functionId: Scalars['String']['input'];
|
||||
functionLogo?: InputMaybe<Scalars['String']['input']>;
|
||||
functionName: Scalars['String']['input'];
|
||||
resultVersionIds: Array<Scalars['String']['input']>;
|
||||
/**
|
||||
* Current schema: {
|
||||
* version: "1.0.0",
|
||||
* values: {
|
||||
* speckleObjects: Record<ObjectId, {level: string; statusMessage: string}[]>
|
||||
* blobIds?: string[]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
results?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
status: AutomationRunStatus;
|
||||
statusMessage?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type GendoAiRender = {
|
||||
__typename?: 'GendoAIRender';
|
||||
camera?: Maybe<Scalars['JSONObject']['output']>;
|
||||
@@ -1013,21 +918,36 @@ export type LegacyCommentViewerData = {
|
||||
*/
|
||||
export type LimitedUser = {
|
||||
__typename?: 'LimitedUser';
|
||||
/** All the recent activity from this user in chronological order */
|
||||
/**
|
||||
* All the recent activity from this user in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
activity?: Maybe<ActivityCollection>;
|
||||
avatar?: Maybe<Scalars['String']['output']>;
|
||||
bio?: Maybe<Scalars['String']['output']>;
|
||||
/** Get public stream commits authored by the user */
|
||||
/**
|
||||
* Get public stream commits authored by the user
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
commits?: Maybe<CommitCollection>;
|
||||
company?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
/** Returns all discoverable streams that the user is a collaborator on */
|
||||
/**
|
||||
* Returns all discoverable streams that the user is a collaborator on
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
streams: StreamCollection;
|
||||
/** The user's timeline in chronological order */
|
||||
/**
|
||||
* The user's timeline in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
timeline?: Maybe<ActivityCollection>;
|
||||
/** Total amount of favorites attached to streams owned by the user */
|
||||
/**
|
||||
* Total amount of favorites attached to streams owned by the user
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
totalOwnedStreamsFavorites: Scalars['Int']['output'];
|
||||
verified?: Maybe<Scalars['Boolean']['output']>;
|
||||
};
|
||||
@@ -1077,10 +997,16 @@ export type LimitedUserTimelineArgs = {
|
||||
limit?: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
export type MarkReceivedVersionInput = {
|
||||
message?: InputMaybe<Scalars['String']['input']>;
|
||||
projectId: Scalars['String']['input'];
|
||||
sourceApplication: Scalars['String']['input'];
|
||||
versionId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type Model = {
|
||||
__typename?: 'Model';
|
||||
author: LimitedUser;
|
||||
automationStatus?: Maybe<AutomationsStatus>;
|
||||
automationsStatus?: Maybe<TriggeredAutomationsStatus>;
|
||||
/** Return a model tree of children */
|
||||
childrenTree: Array<ModelsTreeItem>;
|
||||
@@ -1213,9 +1139,11 @@ export type Mutation = {
|
||||
appUpdate: Scalars['Boolean']['output'];
|
||||
automateFunctionRunStatusReport: Scalars['Boolean']['output'];
|
||||
automateMutations: AutomateMutations;
|
||||
automationMutations: AutomationMutations;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use ModelMutations.create instead. */
|
||||
branchCreate: Scalars['String']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use ModelMutations.delete instead. */
|
||||
branchDelete: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use ModelMutations.update instead. */
|
||||
branchUpdate: Scalars['Boolean']['output'];
|
||||
/** Broadcast user activity in the viewer */
|
||||
broadcastViewerUserActivity: Scalars['Boolean']['output'];
|
||||
@@ -1245,13 +1173,23 @@ export type Mutation = {
|
||||
* @deprecated Use commentMutations version
|
||||
*/
|
||||
commentView: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.create instead. */
|
||||
commitCreate: Scalars['String']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.delete instead. */
|
||||
commitDelete: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.markReceived instead. */
|
||||
commitReceive: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.update/moveToModel instead. */
|
||||
commitUpdate: Scalars['Boolean']['output'];
|
||||
/** Delete a batch of commits */
|
||||
/**
|
||||
* Delete a batch of commits
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.delete instead.
|
||||
*/
|
||||
commitsDelete: Scalars['Boolean']['output'];
|
||||
/** Move a batch of commits to a new branch */
|
||||
/**
|
||||
* Move a batch of commits to a new branch
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.moveToModel instead.
|
||||
*/
|
||||
commitsMove: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Delete a pending invite
|
||||
@@ -1264,7 +1202,8 @@ export type Mutation = {
|
||||
*/
|
||||
inviteResend: Scalars['Boolean']['output'];
|
||||
modelMutations: ModelMutations;
|
||||
objectCreate: Array<Maybe<Scalars['String']['output']>>;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. */
|
||||
objectCreate: Array<Scalars['String']['output']>;
|
||||
projectMutations: ProjectMutations;
|
||||
/** (Re-)send the account verification e-mail */
|
||||
requestVerification: Scalars['Boolean']['output'];
|
||||
@@ -1274,37 +1213,71 @@ export type Mutation = {
|
||||
serverInviteBatchCreate: Scalars['Boolean']['output'];
|
||||
/** Invite a new user to the speckle server and return the invite ID */
|
||||
serverInviteCreate: Scalars['Boolean']['output'];
|
||||
/** Request access to a specific stream */
|
||||
/**
|
||||
* Request access to a specific stream
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectAccessRequestMutations.create instead.
|
||||
*/
|
||||
streamAccessRequestCreate: StreamAccessRequest;
|
||||
/** Accept or decline a stream access request. Must be a stream owner to invoke this. */
|
||||
/**
|
||||
* Accept or decline a stream access request. Must be a stream owner to invoke this.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectAccessRequestMutations.use instead.
|
||||
*/
|
||||
streamAccessRequestUse: Scalars['Boolean']['output'];
|
||||
/** Creates a new stream. */
|
||||
/**
|
||||
* Creates a new stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.create instead.
|
||||
*/
|
||||
streamCreate?: Maybe<Scalars['String']['output']>;
|
||||
/** Deletes an existing stream. */
|
||||
/**
|
||||
* Deletes an existing stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.delete instead.
|
||||
*/
|
||||
streamDelete: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. */
|
||||
streamFavorite?: Maybe<Stream>;
|
||||
/** Note: The required scope to invoke this is not given out to app or personal access tokens */
|
||||
/**
|
||||
* Note: The required scope to invoke this is not given out to app or personal access tokens
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectInviteMutations.batchCreate instead.
|
||||
*/
|
||||
streamInviteBatchCreate: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Cancel a pending stream invite. Can only be invoked by a stream owner.
|
||||
* Note: The required scope to invoke this is not given out to app or personal access tokens
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectInviteMutations.cancel instead.
|
||||
*/
|
||||
streamInviteCancel: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Invite a new or registered user to the specified stream
|
||||
* Note: The required scope to invoke this is not given out to app or personal access tokens
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectInviteMutations.create instead.
|
||||
*/
|
||||
streamInviteCreate: Scalars['Boolean']['output'];
|
||||
/** Accept or decline a stream invite */
|
||||
/**
|
||||
* Accept or decline a stream invite
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectInviteMutations.use instead.
|
||||
*/
|
||||
streamInviteUse: Scalars['Boolean']['output'];
|
||||
/** Remove yourself from stream collaborators (not possible for the owner) */
|
||||
/**
|
||||
* Remove yourself from stream collaborators (not possible for the owner)
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.leave instead.
|
||||
*/
|
||||
streamLeave: Scalars['Boolean']['output'];
|
||||
/** Revokes the permissions of a user on a given stream. */
|
||||
/**
|
||||
* Revokes the permissions of a user on a given stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.updateRole instead.
|
||||
*/
|
||||
streamRevokePermission?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** Updates an existing stream. */
|
||||
/**
|
||||
* Updates an existing stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.update instead.
|
||||
*/
|
||||
streamUpdate: Scalars['Boolean']['output'];
|
||||
/** Update permissions of a user on a given stream. */
|
||||
/**
|
||||
* Update permissions of a user on a given stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.updateRole instead.
|
||||
*/
|
||||
streamUpdatePermission?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.batchDelete instead. */
|
||||
streamsDelete: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Used for broadcasting real time typing status in comment threads. Does not persist any info.
|
||||
@@ -1621,6 +1594,7 @@ export type MutationWebhookUpdateArgs = {
|
||||
|
||||
export type Object = {
|
||||
__typename?: 'Object';
|
||||
/** @deprecated Not implemented. */
|
||||
applicationId?: Maybe<Scalars['String']['output']>;
|
||||
/**
|
||||
* Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects.
|
||||
@@ -1636,6 +1610,7 @@ export type Object = {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
commentCount: Scalars['Int']['output'];
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
@@ -1659,7 +1634,7 @@ export type ObjectChildrenArgs = {
|
||||
export type ObjectCollection = {
|
||||
__typename?: 'ObjectCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
objects: Array<Maybe<Object>>;
|
||||
objects: Array<Object>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
@@ -1699,7 +1674,9 @@ export type PendingStreamCollaborator = {
|
||||
projectId: Scalars['String']['output'];
|
||||
projectName: Scalars['String']['output'];
|
||||
role: Scalars['String']['output'];
|
||||
/** @deprecated Use projectId instead */
|
||||
streamId: Scalars['String']['output'];
|
||||
/** @deprecated Use projectName instead */
|
||||
streamName: Scalars['String']['output'];
|
||||
/** E-mail address or name of the invited user */
|
||||
title: Scalars['String']['output'];
|
||||
@@ -1718,6 +1695,8 @@ export type Project = {
|
||||
blob?: Maybe<BlobMetadata>;
|
||||
/** Get the metadata collection of blobs stored for this stream. */
|
||||
blobs?: Maybe<BlobMetadataCollection>;
|
||||
/** Get specific project comment/thread by ID */
|
||||
comment?: Maybe<Comment>;
|
||||
/** All comment threads in this project */
|
||||
commentThreads: ProjectCommentCollection;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@@ -1727,6 +1706,8 @@ export type Project = {
|
||||
invitedTeam?: Maybe<Array<PendingStreamCollaborator>>;
|
||||
/** Returns a specific model by its ID */
|
||||
model: Model;
|
||||
/** Retrieve a specific project model by its ID */
|
||||
modelByName: Model;
|
||||
/** Return a model tree of children for the specified model name */
|
||||
modelChildrenTree: Array<ModelsTreeItem>;
|
||||
/** Returns a flat list of all models */
|
||||
@@ -1737,6 +1718,9 @@ export type Project = {
|
||||
*/
|
||||
modelsTree: ModelsTreeItemCollection;
|
||||
name: Scalars['String']['output'];
|
||||
object?: Maybe<Object>;
|
||||
/** Pending project access requests */
|
||||
pendingAccessRequests?: Maybe<Array<ProjectAccessRequest>>;
|
||||
/** Returns a list models that are being created from a file import */
|
||||
pendingImportedModels: Array<FileUpload>;
|
||||
/** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */
|
||||
@@ -1780,6 +1764,11 @@ export type ProjectBlobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectCommentArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectCommentThreadsArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<ProjectCommentsFilter>;
|
||||
@@ -1792,6 +1781,11 @@ export type ProjectModelArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectModelByNameArgs = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectModelChildrenTreeArgs = {
|
||||
fullName: Scalars['String']['input'];
|
||||
};
|
||||
@@ -1811,6 +1805,11 @@ export type ProjectModelsTreeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectObjectArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectPendingImportedModelsArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
@@ -1837,6 +1836,38 @@ export type ProjectWebhooksArgs = {
|
||||
id?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
/** Created when a user requests to become a contributor on a project */
|
||||
export type ProjectAccessRequest = {
|
||||
__typename?: 'ProjectAccessRequest';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
/** Can only be selected if authed user has proper access */
|
||||
project: Project;
|
||||
projectId: Scalars['String']['output'];
|
||||
requester: LimitedUser;
|
||||
requesterId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ProjectAccessRequestMutations = {
|
||||
__typename?: 'ProjectAccessRequestMutations';
|
||||
/** Request access to a specific project */
|
||||
create: ProjectAccessRequest;
|
||||
/** Accept or decline a project access request. Must be a project owner to invoke this. */
|
||||
use: Project;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAccessRequestMutationsCreateArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectAccessRequestMutationsUseArgs = {
|
||||
accept: Scalars['Boolean']['input'];
|
||||
requestId: Scalars['String']['input'];
|
||||
role?: StreamRole;
|
||||
};
|
||||
|
||||
export type ProjectAutomationCreateInput = {
|
||||
enabled: Scalars['Boolean']['input'];
|
||||
name: Scalars['String']['input'];
|
||||
@@ -1899,14 +1930,6 @@ export type ProjectAutomationUpdateInput = {
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectAutomationsStatusUpdatedMessage = {
|
||||
__typename?: 'ProjectAutomationsStatusUpdatedMessage';
|
||||
model: Model;
|
||||
project: Project;
|
||||
status: AutomationsStatus;
|
||||
version: Version;
|
||||
};
|
||||
|
||||
export type ProjectAutomationsUpdatedMessage = {
|
||||
__typename?: 'ProjectAutomationsUpdatedMessage';
|
||||
automation?: Maybe<Automation>;
|
||||
@@ -1979,6 +2002,7 @@ export type ProjectCreateInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
visibility?: InputMaybe<ProjectVisibility>;
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectFileImportUpdatedMessage = {
|
||||
@@ -2087,7 +2111,11 @@ export enum ProjectModelsUpdatedMessageType {
|
||||
|
||||
export type ProjectMutations = {
|
||||
__typename?: 'ProjectMutations';
|
||||
/** Access request related mutations */
|
||||
accessRequestMutations: ProjectAccessRequestMutations;
|
||||
automationMutations: ProjectAutomationMutations;
|
||||
/** Batch delete projects */
|
||||
batchDelete: Scalars['Boolean']['output'];
|
||||
/** Create new project */
|
||||
create: Project;
|
||||
/**
|
||||
@@ -2113,6 +2141,11 @@ export type ProjectMutationsAutomationMutationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsBatchDeleteArgs = {
|
||||
ids: Array<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsCreateArgs = {
|
||||
input?: InputMaybe<ProjectCreateInput>;
|
||||
};
|
||||
@@ -2264,7 +2297,10 @@ export type Query = {
|
||||
adminUsers?: Maybe<AdminUsersListCollection>;
|
||||
/** Gets a specific app from the server. */
|
||||
app?: Maybe<ServerApp>;
|
||||
/** Returns all the publicly available apps on this server. */
|
||||
/**
|
||||
* Returns all the publicly available apps on this server.
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
apps?: Maybe<Array<Maybe<ServerAppListItem>>>;
|
||||
/** If user is authenticated using an app token, this will describe the app */
|
||||
authenticatedAsApp?: Maybe<ServerAppListItem>;
|
||||
@@ -2273,15 +2309,19 @@ export type Query = {
|
||||
automateFunctions: AutomateFunctionCollection;
|
||||
/** Part of the automation/function creation handshake mechanism */
|
||||
automateValidateAuthCode: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.comment instead. */
|
||||
comment?: Maybe<Comment>;
|
||||
/**
|
||||
* This query can be used in the following ways:
|
||||
* - get all the comments for a stream: **do not pass in any resource identifiers**.
|
||||
* - get the comments targeting any of a set of provided resources (comments/objects): **pass in an array of resources.**
|
||||
* @deprecated Use 'commentThreads' fields instead
|
||||
* @deprecated Use Project/Version/Model 'commentThreads' fields instead
|
||||
*/
|
||||
comments?: Maybe<CommentCollection>;
|
||||
/** All of the discoverable streams of the server */
|
||||
/**
|
||||
* All of the discoverable streams of the server
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
discoverableStreams?: Maybe<StreamCollection>;
|
||||
/** Get the (limited) profile information of another server user */
|
||||
otherUser?: Maybe<LimitedUser>;
|
||||
@@ -2303,20 +2343,29 @@ export type Query = {
|
||||
/**
|
||||
* Returns a specific stream. Will throw an authorization error if active user isn't authorized
|
||||
* to see it, for example, if a stream isn't public and the user doesn't have the appropriate rights.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Query.project instead.
|
||||
*/
|
||||
stream?: Maybe<Stream>;
|
||||
/** Get authed user's stream access request */
|
||||
/**
|
||||
* Get authed user's stream access request
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use User.projectAccessRequest instead.
|
||||
*/
|
||||
streamAccessRequest?: Maybe<StreamAccessRequest>;
|
||||
/**
|
||||
* Look for an invitation to a stream, for the current user (authed or not). If token
|
||||
* isn't specified, the server will look for any valid invite.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Query.projectInvite instead.
|
||||
*/
|
||||
streamInvite?: Maybe<PendingStreamCollaborator>;
|
||||
/** Get all invitations to streams that the active user has */
|
||||
/**
|
||||
* Get all invitations to streams that the active user has
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use User.projectInvites instead.
|
||||
*/
|
||||
streamInvites: Array<PendingStreamCollaborator>;
|
||||
/**
|
||||
* Returns all streams that the active user is a collaborator on.
|
||||
* Pass in the `query` parameter to search by name, description or ID.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use User.projects instead.
|
||||
*/
|
||||
streams?: Maybe<StreamCollection>;
|
||||
/**
|
||||
@@ -2324,7 +2373,10 @@ export type Query = {
|
||||
* @deprecated To be removed in the near future! Use 'activeUser' to get info about the active user or 'otherUser' to get info about another user.
|
||||
*/
|
||||
user?: Maybe<User>;
|
||||
/** Validate password strength */
|
||||
/**
|
||||
* Validate password strength
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
userPwdStrength: PasswordStrengthCheckResults;
|
||||
/**
|
||||
* Search for users and return limited metadata about them, if you have the server:user role.
|
||||
@@ -2648,13 +2700,22 @@ export enum SortDirection {
|
||||
|
||||
export type Stream = {
|
||||
__typename?: 'Stream';
|
||||
/** All the recent activity on this stream in chronological order */
|
||||
/**
|
||||
* All the recent activity on this stream in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
activity?: Maybe<ActivityCollection>;
|
||||
allowPublicComments: Scalars['Boolean']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.blob instead. */
|
||||
blob?: Maybe<BlobMetadata>;
|
||||
/** Get the metadata collection of blobs stored for this stream. */
|
||||
/**
|
||||
* Get the metadata collection of blobs stored for this stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Project.blobs instead.
|
||||
*/
|
||||
blobs?: Maybe<BlobMetadataCollection>;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.model or Project.modelByName instead. */
|
||||
branch?: Maybe<Branch>;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.models or Project.modelsTree instead. */
|
||||
branches?: Maybe<BranchCollection>;
|
||||
collaborators: Array<StreamCollaborator>;
|
||||
/**
|
||||
@@ -2666,18 +2727,27 @@ export type Stream = {
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
commentCount: Scalars['Int']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.version instead. */
|
||||
commit?: Maybe<Commit>;
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.versions instead. */
|
||||
commits?: Maybe<CommitCollection>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Date when you favorited this stream. `null` if stream isn't viewed from a specific user's perspective or if it isn't favorited. */
|
||||
favoritedDate?: Maybe<Scalars['DateTime']['output']>;
|
||||
favoritesCount: Scalars['Int']['output'];
|
||||
/** Returns a specific file upload that belongs to this stream. */
|
||||
/**
|
||||
* Returns a specific file upload that belongs to this stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Project.pendingImportedModels or Model.pendingImportedVersions instead.
|
||||
*/
|
||||
fileUpload?: Maybe<FileUpload>;
|
||||
/** Returns a list of all the file uploads for this stream. */
|
||||
/**
|
||||
* Returns a list of all the file uploads for this stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Project.pendingImportedModels or Model.pendingImportedVersions instead.
|
||||
*/
|
||||
fileUploads: Array<FileUpload>;
|
||||
id: Scalars['String']['output'];
|
||||
/**
|
||||
@@ -2688,8 +2758,12 @@ export type Stream = {
|
||||
/** Whether the stream can be viewed by non-contributors */
|
||||
isPublic: Scalars['Boolean']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.object instead. */
|
||||
object?: Maybe<Object>;
|
||||
/** Pending stream access requests */
|
||||
/**
|
||||
* Pending stream access requests
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use Project.pendingAccessRequests instead.
|
||||
*/
|
||||
pendingAccessRequests?: Maybe<Array<StreamAccessRequest>>;
|
||||
/** Collaborators who have been invited, but not yet accepted. */
|
||||
pendingCollaborators?: Maybe<Array<PendingStreamCollaborator>>;
|
||||
@@ -2697,6 +2771,7 @@ export type Stream = {
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
size?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** @deprecated Part of the old API surface and will be removed in the future. Use Project.webhooks instead. */
|
||||
webhooks: WebhookCollection;
|
||||
};
|
||||
|
||||
@@ -2799,6 +2874,7 @@ export type StreamCreateInput = {
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Optionally specify user IDs of users that you want to invite to be contributors to this stream */
|
||||
withContributors?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type StreamInviteCreateInput = {
|
||||
@@ -2847,11 +2923,20 @@ export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
/** It's lonely in the void. */
|
||||
_?: Maybe<Scalars['String']['output']>;
|
||||
/** Subscribe to branch created event */
|
||||
/**
|
||||
* Subscribe to branch created event
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectModelsUpdated' instead.
|
||||
*/
|
||||
branchCreated?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Subscribe to branch deleted event */
|
||||
/**
|
||||
* Subscribe to branch deleted event
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectModelsUpdated' instead.
|
||||
*/
|
||||
branchDeleted?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Subscribe to branch updated event. */
|
||||
/**
|
||||
* Subscribe to branch updated event.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectModelsUpdated' instead.
|
||||
*/
|
||||
branchUpdated?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/**
|
||||
* Subscribe to new comment events. There's two ways to use this subscription:
|
||||
@@ -2867,13 +2952,21 @@ export type Subscription = {
|
||||
* @deprecated Use projectCommentsUpdated or viewerUserActivityBroadcasted for reply status
|
||||
*/
|
||||
commentThreadActivity: CommentThreadActivityMessage;
|
||||
/** Subscribe to commit created event */
|
||||
/**
|
||||
* Subscribe to commit created event
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectVersionsUpdated' instead.
|
||||
*/
|
||||
commitCreated?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Subscribe to commit deleted event */
|
||||
/**
|
||||
* Subscribe to commit deleted event
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectVersionsUpdated' instead.
|
||||
*/
|
||||
commitDeleted?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Subscribe to commit updated event. */
|
||||
/**
|
||||
* Subscribe to commit updated event.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use 'projectVersionsUpdated' instead.
|
||||
*/
|
||||
commitUpdated?: Maybe<Scalars['JSONObject']['output']>;
|
||||
projectAutomationsStatusUpdated: ProjectAutomationsStatusUpdatedMessage;
|
||||
/** Subscribe to updates to automations in the project */
|
||||
projectAutomationsUpdated: ProjectAutomationsUpdatedMessage;
|
||||
/**
|
||||
@@ -2881,7 +2974,10 @@ export type Subscription = {
|
||||
* updates regarding comments for those resources.
|
||||
*/
|
||||
projectCommentsUpdated: ProjectCommentsUpdatedMessage;
|
||||
/** Subscribe to changes to any of a project's file imports */
|
||||
/**
|
||||
* Subscribe to changes to any of a project's file imports
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use projectPendingModelsUpdated or projectPendingVersionsUpdated instead.
|
||||
*/
|
||||
projectFileImportUpdated: ProjectFileImportUpdatedMessage;
|
||||
/** Subscribe to changes to a project's models. Optionally specify modelIds to track. */
|
||||
projectModelsUpdated: ProjectModelsUpdatedMessage;
|
||||
@@ -2899,20 +2995,28 @@ export type Subscription = {
|
||||
projectVersionsPreviewGenerated: ProjectVersionsPreviewGeneratedMessage;
|
||||
/** Subscribe to changes to a project's versions. */
|
||||
projectVersionsUpdated: ProjectVersionsUpdatedMessage;
|
||||
/** Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream. */
|
||||
/**
|
||||
* Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use projectUpdated instead.
|
||||
*/
|
||||
streamDeleted?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Subscribes to stream updated event. Use this in clients/components that pertain only to this stream. */
|
||||
/**
|
||||
* Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use projectUpdated instead.
|
||||
*/
|
||||
streamUpdated?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Track newly added or deleted projects owned by the active user */
|
||||
userProjectsUpdated: UserProjectsUpdatedMessage;
|
||||
/**
|
||||
* Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
|
||||
* **NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use userProjectsUpdated instead.
|
||||
*/
|
||||
userStreamAdded?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/**
|
||||
* Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile.
|
||||
* **NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use userProjectsUpdated instead.
|
||||
*/
|
||||
userStreamRemoved?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/**
|
||||
@@ -2969,11 +3073,6 @@ export type SubscriptionCommitUpdatedArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionProjectAutomationsStatusUpdatedArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionProjectAutomationsUpdatedArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
@@ -3130,7 +3229,10 @@ export type UpdateVersionInput = {
|
||||
*/
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
/** All the recent activity from this user in chronological order */
|
||||
/**
|
||||
* All the recent activity from this user in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
activity?: Maybe<ActivityCollection>;
|
||||
/** Returns a list of your personal api tokens. */
|
||||
apiTokens: Array<ApiToken>;
|
||||
@@ -3142,16 +3244,19 @@ export type User = {
|
||||
/**
|
||||
* Get commits authored by the user. If requested for another user, then only commits
|
||||
* from public streams will be returned.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use User.versions instead.
|
||||
*/
|
||||
commits?: Maybe<CommitCollection>;
|
||||
company?: Maybe<Scalars['String']['output']>;
|
||||
/** Returns the apps you have created. */
|
||||
createdApps?: Maybe<Array<ServerApp>>;
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** Only returned if API user is the user being requested or an admin */
|
||||
email?: Maybe<Scalars['String']['output']>;
|
||||
/**
|
||||
* All the streams that a active user has favorited.
|
||||
* Note: You can't use this to retrieve another user's favorite streams.
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
favoriteStreams: StreamCollection;
|
||||
/** Whether the user has a pending/active email verification token */
|
||||
@@ -3162,6 +3267,8 @@ export type User = {
|
||||
name: Scalars['String']['output'];
|
||||
notificationPreferences: Scalars['JSONObject']['output'];
|
||||
profiles?: Maybe<Scalars['JSONObject']['output']>;
|
||||
/** Get pending project access request, that the user made */
|
||||
projectAccessRequest?: Maybe<ProjectAccessRequest>;
|
||||
/** Get all invitations to projects that the active user has */
|
||||
projectInvites: Array<PendingStreamCollaborator>;
|
||||
/** Get projects that the user participates in */
|
||||
@@ -3170,11 +3277,18 @@ export type User = {
|
||||
/**
|
||||
* Returns all streams that the user is a collaborator on. If requested for a user, who isn't the
|
||||
* authenticated user, then this will only return discoverable streams.
|
||||
* @deprecated Part of the old API surface and will be removed in the future. Use User.projects instead.
|
||||
*/
|
||||
streams: StreamCollection;
|
||||
/** The user's timeline in chronological order */
|
||||
/**
|
||||
* The user's timeline in chronological order
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
timeline?: Maybe<ActivityCollection>;
|
||||
/** Total amount of favorites attached to streams owned by the user */
|
||||
/**
|
||||
* Total amount of favorites attached to streams owned by the user
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
totalOwnedStreamsFavorites: Scalars['Int']['output'];
|
||||
verified?: Maybe<Scalars['Boolean']['output']>;
|
||||
/**
|
||||
@@ -3220,6 +3334,15 @@ export type UserFavoriteStreamsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Full user type, should only be used in the context of admin operations or
|
||||
* when a user is reading/writing info about himself
|
||||
*/
|
||||
export type UserProjectAccessRequestArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Full user type, should only be used in the context of admin operations or
|
||||
* when a user is reading/writing info about himself
|
||||
@@ -3315,7 +3438,6 @@ export type UserUpdateInput = {
|
||||
export type Version = {
|
||||
__typename?: 'Version';
|
||||
authorUser?: Maybe<LimitedUser>;
|
||||
automationStatus?: Maybe<AutomationsStatus>;
|
||||
automationsStatus?: Maybe<TriggeredAutomationsStatus>;
|
||||
/** All comment threads in this version */
|
||||
commentThreads: CommentCollection;
|
||||
@@ -3365,18 +3487,30 @@ export type VersionCreatedTriggerDefinition = {
|
||||
|
||||
export type VersionMutations = {
|
||||
__typename?: 'VersionMutations';
|
||||
create: Version;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
markReceived: Scalars['Boolean']['output'];
|
||||
moveToModel: Model;
|
||||
requestGendoAIRender: Scalars['Boolean']['output'];
|
||||
update: Version;
|
||||
};
|
||||
|
||||
|
||||
export type VersionMutationsCreateArgs = {
|
||||
input: CreateVersionInput;
|
||||
};
|
||||
|
||||
|
||||
export type VersionMutationsDeleteArgs = {
|
||||
input: DeleteVersionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type VersionMutationsMarkReceivedArgs = {
|
||||
input: MarkReceivedVersionInput;
|
||||
};
|
||||
|
||||
|
||||
export type VersionMutationsMoveToModelArgs = {
|
||||
input: MoveVersionsInput;
|
||||
};
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
<NuxtLink
|
||||
:class="[
|
||||
active ? 'bg-foundation-focus' : '',
|
||||
'flex gap-2.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
|
||||
'flex gap-3.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
|
||||
]"
|
||||
@click="() => (showProfileEditDialog = true)"
|
||||
@click="toggleSettingsDialog(settingsQueries.user.profile)"
|
||||
>
|
||||
<UserAvatar :user="activeUser" size="sm" class="-ml-0.5 mr-px" />
|
||||
Edit profile
|
||||
<UserCircleIcon class="w-5 h-5" />
|
||||
Settings
|
||||
</NuxtLink>
|
||||
</MenuItem>
|
||||
<MenuItem v-if="isAdmin" v-slot="{ active }">
|
||||
@@ -51,10 +51,10 @@
|
||||
active ? 'bg-foundation-focus' : '',
|
||||
'flex gap-3.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
|
||||
]"
|
||||
@click="goToServerManagement()"
|
||||
@click="toggleSettingsDialog(settingsQueries.server.general)"
|
||||
>
|
||||
<Cog6ToothIcon class="w-5 h-5" />
|
||||
Server management
|
||||
<ServerStackIcon class="w-5 h-5" />
|
||||
Server settings
|
||||
</NuxtLink>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
@@ -127,11 +127,16 @@
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<ServerManagementInviteDialog v-model:open="showInviteDialog" />
|
||||
<UserProfileEditDialog v-model:open="showProfileEditDialog" />
|
||||
<SettingsServerUserInviteDialog v-model:open="showInviteDialog" />
|
||||
<SettingsDialog
|
||||
v-model:open="showSettingsDialog"
|
||||
v-model:target-menu-item="settingsDialogTarget"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isString } from 'lodash'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
import {
|
||||
XMarkIcon,
|
||||
@@ -140,54 +145,77 @@ import {
|
||||
SunIcon,
|
||||
MoonIcon,
|
||||
EnvelopeIcon,
|
||||
Cog6ToothIcon,
|
||||
CloudArrowDownIcon,
|
||||
ChatBubbleLeftRightIcon
|
||||
ChatBubbleLeftRightIcon,
|
||||
UserCircleIcon,
|
||||
ServerStackIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { useAuthManager } from '~~/lib/auth/composables/auth'
|
||||
import { useTheme } from '~~/lib/core/composables/theme'
|
||||
import { useServerInfo } from '~/lib/core/composables/server'
|
||||
import { homeRoute, profileRoute, connectorsPageUrl } from '~/lib/common/helpers/route'
|
||||
import { connectorsPageUrl, settingsQueries } from '~/lib/common/helpers/route'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
|
||||
|
||||
defineProps<{
|
||||
loginUrl?: RouteLocationRaw
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const { logout } = useAuthManager()
|
||||
const { activeUser, isGuest } = useActiveUser()
|
||||
const { isDarkTheme, toggleTheme } = useTheme()
|
||||
const { serverInfo } = useServerInfo()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const showInviteDialog = ref(false)
|
||||
const showProfileEditDialog = ref(false)
|
||||
const showSettingsDialog = ref(false)
|
||||
const settingsDialogTarget = ref<string | null>(null)
|
||||
const menuButtonId = useId()
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const Icon = computed(() => (isDarkTheme.value ? SunIcon : MoonIcon))
|
||||
const version = computed(() => serverInfo.value?.version)
|
||||
const isAdmin = computed(() => activeUser.value?.role === Roles.Server.Admin)
|
||||
const isProfileRoute = computed(() => route.path === profileRoute)
|
||||
|
||||
const toggleInviteDialog = () => {
|
||||
showInviteDialog.value = true
|
||||
}
|
||||
|
||||
const goToServerManagement = () => {
|
||||
router.push('/server-management')
|
||||
const toggleSettingsDialog = (target: string) => {
|
||||
showSettingsDialog.value = true
|
||||
|
||||
// On mobile open the modal but dont set the target
|
||||
settingsDialogTarget.value = !isMobile.value ? target : null
|
||||
}
|
||||
|
||||
watch(
|
||||
isProfileRoute,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
showProfileEditDialog.value = true
|
||||
void router.replace({ path: homeRoute, force: true }) // in-place replace
|
||||
const deleteSettingsQuery = (): void => {
|
||||
const currentQueryParams = { ...route.query }
|
||||
delete currentQueryParams.settings
|
||||
router.push({ query: currentQueryParams })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const settingsQuery = route.query?.settings
|
||||
|
||||
if (settingsQuery && isString(settingsQuery)) {
|
||||
if (settingsQuery.includes('server') && !isAdmin.value) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: "You don't have access to server settings"
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
showSettingsDialog.value = true
|
||||
settingsDialogTarget.value = settingsQuery
|
||||
deleteSettingsQuery()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
>
|
||||
<template #header>Your first upload</template>
|
||||
</OnboardingDialogFirstSend>
|
||||
<ServerManagementInviteDialog
|
||||
<SettingsServerUserInviteDialog
|
||||
v-model:open="showServerInviteDialog"
|
||||
@update:open="(v) => (!v ? markComplete(3) : '')"
|
||||
/>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-foundation p-4 flex flex-col gap-4 rounded-md shadow-md">
|
||||
<ServerManagementCardRow
|
||||
v-for="info in serverInfo"
|
||||
:key="info.title"
|
||||
:title="info.title"
|
||||
:value="info.value"
|
||||
:cta="info.cta"
|
||||
:icon="info.icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CardInfo } from '~~/lib/server-management/helpers/types'
|
||||
|
||||
defineProps<{
|
||||
serverInfo: CardInfo[]
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-1 text-foreground-2 text-sm">
|
||||
<Component :is="icon" class="h-4 w-4" />
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center gap-4 sm:gap-8">
|
||||
<span class="text-xl sm:text-2xl font-bold">{{ value }}</span>
|
||||
<template v-if="cta?.type === 'button'">
|
||||
<FormButton @click="cta?.action">
|
||||
{{ cta.label }}
|
||||
</FormButton>
|
||||
</template>
|
||||
<template v-else-if="cta?.type === 'link'">
|
||||
<FormButton
|
||||
color="invert"
|
||||
class="shrink-0"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
@click="cta?.action"
|
||||
>
|
||||
{{ cta.label }}
|
||||
</FormButton>
|
||||
</template>
|
||||
<template v-else-if="cta?.type === 'text'">
|
||||
<div
|
||||
class="flex items-center gap-1 text-xs text-center sm:text-sm opacity-50 shrink-0"
|
||||
>
|
||||
<CheckCircleIcon class="h-4 w-4" />
|
||||
{{ cta.label }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowTopRightOnSquareIcon, CheckCircleIcon } from '@heroicons/vue/24/outline'
|
||||
import type { ConcreteComponent } from 'vue'
|
||||
import type { CTA } from '~~/lib/server-management/helpers/types'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'cta-clicked', v: MouseEvent): void
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
value: string
|
||||
icon: ConcreteComponent
|
||||
cta?: CTA
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
v-bind="
|
||||
isMobile ? { title: selectedMenuItem ? selectedMenuItem.title : 'Settings' } : {}
|
||||
"
|
||||
fullscreen
|
||||
:show-back-button="isMobile && !!selectedMenuItem"
|
||||
@back="targetMenuItem = null"
|
||||
>
|
||||
<div class="w-full h-full flex">
|
||||
<LayoutSidebar
|
||||
v-if="!isMobile || !selectedMenuItem"
|
||||
class="w-full md:w-56 lg:w-60 md:p-4 md:pt-6 md:bg-foundation-page md:border-r md:border-outline-3"
|
||||
>
|
||||
<LayoutSidebarMenu>
|
||||
<LayoutSidebarMenuGroup title="Account settings">
|
||||
<template #title-icon>
|
||||
<UserIcon class="h-5 w-5" />
|
||||
</template>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
v-for="(sidebarMenuItem, key) in menuItemConfig.user"
|
||||
:key="key"
|
||||
:label="sidebarMenuItem.title"
|
||||
:class="{
|
||||
'bg-foundation-focus hover:!bg-foundation-focus':
|
||||
selectedMenuItem?.title === sidebarMenuItem.title
|
||||
}"
|
||||
@click="targetMenuItem = `${key}`"
|
||||
/>
|
||||
</LayoutSidebarMenuGroup>
|
||||
<LayoutSidebarMenuGroup v-if="isAdmin" title="Server settings">
|
||||
<template #title-icon>
|
||||
<ServerStackIcon class="h-5 w-5" />
|
||||
</template>
|
||||
<LayoutSidebarMenuGroupItem
|
||||
v-for="(sidebarMenuItem, key) in menuItemConfig.server"
|
||||
:key="key"
|
||||
:label="sidebarMenuItem.title"
|
||||
:class="{
|
||||
'bg-foundation-focus hover:!bg-foundation-focus':
|
||||
selectedMenuItem?.title === sidebarMenuItem.title
|
||||
}"
|
||||
@click="targetMenuItem = `${key}`"
|
||||
/>
|
||||
</LayoutSidebarMenuGroup>
|
||||
</LayoutSidebarMenu>
|
||||
</LayoutSidebar>
|
||||
<component
|
||||
:is="selectedMenuItem.component"
|
||||
v-if="selectedMenuItem"
|
||||
:class="[
|
||||
'bg-foundation md:px-10 md:py-12 md:bg-foundation w-full',
|
||||
!isMobile && 'simple-scrollbar overflow-y-auto flex-1'
|
||||
]"
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { defineComponent } from 'vue'
|
||||
import SettingsUserProfile from '~/components/settings/user/Profile.vue'
|
||||
import SettingsUserNotifications from '~/components/settings/user/Notifications.vue'
|
||||
import SettingsUserDeveloper from '~/components/settings/user/Developer.vue'
|
||||
import SettingsServerGeneral from '~/components/settings/server/General.vue'
|
||||
import SettingsServerProjects from '~/components/settings/server/Projects.vue'
|
||||
import SettingsServerActiveUsers from '~/components/settings/server/ActiveUsers.vue'
|
||||
import SettingsServerPendingInvitations from '~/components/settings/server/PendingInvitations.vue'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { UserIcon, ServerStackIcon } from '@heroicons/vue/24/outline'
|
||||
import { settingsQueries } from '~/lib/common/helpers/route'
|
||||
import { useActiveUser } from '~/lib/auth/composables/activeUser'
|
||||
import {
|
||||
LayoutSidebar,
|
||||
LayoutSidebarMenu,
|
||||
LayoutSidebarMenuGroup
|
||||
} from '@speckle/ui-components'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
type MenuItem = {
|
||||
title: string
|
||||
component: ReturnType<typeof defineComponent>
|
||||
}
|
||||
|
||||
const { activeUser: user } = useActiveUser()
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const menuItemConfig = shallowRef<{ [key: string]: { [key: string]: MenuItem } }>({
|
||||
user: {
|
||||
[settingsQueries.user.profile]: {
|
||||
title: 'Profile',
|
||||
component: SettingsUserProfile
|
||||
},
|
||||
[settingsQueries.user.notifications]: {
|
||||
title: 'Notifications',
|
||||
component: SettingsUserNotifications
|
||||
},
|
||||
[settingsQueries.user.developerSettings]: {
|
||||
title: 'Developer settings',
|
||||
component: SettingsUserDeveloper
|
||||
}
|
||||
},
|
||||
server: {
|
||||
[settingsQueries.server.general]: {
|
||||
title: 'General',
|
||||
component: SettingsServerGeneral
|
||||
},
|
||||
[settingsQueries.server.projects]: {
|
||||
title: 'Projects',
|
||||
component: SettingsServerProjects
|
||||
},
|
||||
[settingsQueries.server.activeUsers]: {
|
||||
title: 'Active users',
|
||||
component: SettingsServerActiveUsers
|
||||
},
|
||||
[settingsQueries.server.pendingInvitations]: {
|
||||
title: 'Pending invitations',
|
||||
component: SettingsServerPendingInvitations
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
const targetMenuItem = defineModel<string | null>('targetMenuItem', { required: true })
|
||||
|
||||
const isAdmin = computed(() => user.value?.role === Roles.Server.Admin)
|
||||
const selectedMenuItem = computed((): MenuItem | null => {
|
||||
const categories = [menuItemConfig.value.user, menuItemConfig.value.server]
|
||||
for (const category of categories) {
|
||||
if (targetMenuItem.value && targetMenuItem.value in category) {
|
||||
return category[targetMenuItem.value]
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMobile.value && targetMenuItem.value) {
|
||||
// Fallback for invalid queries/typos
|
||||
return targetMenuItem.value.includes('server') && isAdmin.value
|
||||
? menuItemConfig.value.server.general
|
||||
: menuItemConfig.value.user.profile
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
watch(
|
||||
() => user.value,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
+14
-6
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col md:flex-row gap-3 md:gap-0 justify-between">
|
||||
<h2 v-if="subheading" class="h5 font-bold">{{ title }}</h2>
|
||||
<h1 v-else class="h4 font-bold">{{ title }}</h1>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<h2 v-if="subheading" class="text-xl">{{ title }}</h2>
|
||||
<h1 v-else class="text-2xl font-semibold hidden md:block">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div v-if="buttons.length > 0" class="flex flex-wrap gap-2">
|
||||
<FormButton
|
||||
v-for="(button, index) in buttons"
|
||||
:key="index"
|
||||
@@ -15,9 +17,15 @@
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm max-w-5xl">
|
||||
<slot></slot>
|
||||
<p
|
||||
v-if="text"
|
||||
class="text-sm pt-2 md:pt-4 text-secondary-2"
|
||||
:class="{ 'pt-6': subheading }"
|
||||
>
|
||||
{{ text }}
|
||||
</p>
|
||||
<hr v-if="!subheading" class="my-6 md:my-10" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader title="Active users" text="Manage server members" />
|
||||
<div class="flex flex-col-reverse md:justify-between md:flex-row md:gap-x-4">
|
||||
<div class="relative w-full md:max-w-md mt-6 md:mt-0">
|
||||
<FormTextInput
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!search"
|
||||
placeholder="Search users"
|
||||
class="rounded-md border border-outline-3"
|
||||
:model-value="bind.modelValue.value"
|
||||
v-on="on"
|
||||
/>
|
||||
</div>
|
||||
<FormButton :icon-left="UserPlusIcon" @click="toggleInviteDialog">
|
||||
Invite
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<LayoutTable
|
||||
class="mt-6 md:mt-8"
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'email', header: 'Email', classes: 'col-span-3 truncate' },
|
||||
{ id: 'emailState', header: 'Email state', classes: 'col-span-2' },
|
||||
{ id: 'company', header: 'Company', classes: 'col-span-2 truncate' },
|
||||
{ id: 'role', header: 'Role', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="users"
|
||||
:buttons="[{ icon: TrashIcon, label: 'Delete', action: openUserDeleteDialog }]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UserAvatar v-if="isUser(item)" :user="item" />
|
||||
<span class="truncate">
|
||||
{{ isUser(item) ? item.name : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #email="{ item }">
|
||||
{{ isUser(item) ? item.email : '' }}
|
||||
</template>
|
||||
|
||||
<template #emailState="{ item }">
|
||||
<div class="flex items-center gap-2 select-none">
|
||||
<template v-if="isUser(item) && item.verified">
|
||||
<CheckCircleIcon class="h-4 w-4 text-primary" />
|
||||
<span>Verified</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ExclamationCircleIcon class="h-4 w-4 text-danger" />
|
||||
<span>Not verified</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #company="{ item }">
|
||||
{{ isUser(item) ? item.company : '' }}
|
||||
</template>
|
||||
|
||||
<template #role="{ item }">
|
||||
<FormSelectServerRoles
|
||||
:allow-guest="isGuestMode"
|
||||
allow-admin
|
||||
allow-archived
|
||||
:model-value="isUser(item) ? item.role : undefined"
|
||||
:disabled="isUser(item) && isCurrentUser(item)"
|
||||
fully-control-value
|
||||
@update:model-value="(newRoleValue) => isUser(item) && !isArray(newRoleValue) && newRoleValue && openChangeUserRoleDialog(item, newRoleValue as ServerRoles)"
|
||||
/>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !users?.length" loading />
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="users?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="-mt-24 -mb-24"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
|
||||
<SettingsServerUserDeleteDialog
|
||||
v-model:open="showUserDeleteDialog"
|
||||
:user="userToModify"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
|
||||
<SettingsServerUserChangeRoleDialog
|
||||
v-model:open="showChangeUserRoleDialog"
|
||||
:user="userToModify"
|
||||
:old-role="oldRole"
|
||||
:new-role="newRole"
|
||||
hide-closer
|
||||
/>
|
||||
<SettingsServerUserInviteDialog v-model:open="showInviteDialog" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { isArray } from 'lodash-es'
|
||||
import { useLogger } from '~~/composables/logging'
|
||||
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
|
||||
import type { Nullable, ServerRoles, Optional } from '@speckle/shared'
|
||||
import { getUsersQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import type { ItemType, UserItem } from '~~/lib/server-management/helpers/types'
|
||||
import { isUser } from '~~/lib/server-management/helpers/utils'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
ExclamationCircleIcon,
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
UserPlusIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { useServerInfo } from '~~/lib/core/composables/server'
|
||||
import { useDebouncedTextInput } from '@speckle/ui-components'
|
||||
|
||||
const search = defineModel<string>('search')
|
||||
|
||||
const logger = useLogger()
|
||||
const { activeUser } = useActiveUser()
|
||||
const { isGuestMode } = useServerInfo()
|
||||
const { on, bind } = useDebouncedTextInput({ model: search })
|
||||
|
||||
const userToModify: Ref<Nullable<UserItem>> = ref(null)
|
||||
|
||||
const showUserDeleteDialog = ref(false)
|
||||
const showChangeUserRoleDialog = ref(false)
|
||||
const newRole = ref<ServerRoles>()
|
||||
const infiniteLoaderId = ref('')
|
||||
const showInviteDialog = ref(false)
|
||||
|
||||
const queryVariables = computed(() => ({
|
||||
limit: 50,
|
||||
query: search.value
|
||||
}))
|
||||
|
||||
const {
|
||||
result: extraPagesResult,
|
||||
fetchMore: fetchMorePages,
|
||||
variables: resultVariables,
|
||||
onResult,
|
||||
loading
|
||||
} = useQuery(getUsersQuery, queryVariables)
|
||||
|
||||
const oldRole = computed(() => userToModify.value?.role as Optional<ServerRoles>)
|
||||
|
||||
const moreToLoad = computed(
|
||||
() =>
|
||||
!extraPagesResult.value?.admin?.userList ||
|
||||
extraPagesResult.value.admin.userList.items.length <
|
||||
extraPagesResult.value.admin.userList.totalCount
|
||||
)
|
||||
|
||||
const users = computed(() => extraPagesResult.value?.admin.userList.items || [])
|
||||
|
||||
const isCurrentUser = (userItem: UserItem) => {
|
||||
return userItem.id === activeUser.value?.id
|
||||
}
|
||||
|
||||
const openUserDeleteDialog = (item: ItemType) => {
|
||||
if (isUser(item)) {
|
||||
userToModify.value = item
|
||||
showUserDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const openChangeUserRoleDialog = (user: UserItem, newRoleValue: ServerRoles) => {
|
||||
if (user.role === newRoleValue) {
|
||||
return
|
||||
}
|
||||
userToModify.value = user
|
||||
newRole.value = newRoleValue
|
||||
showChangeUserRoleDialog.value = true
|
||||
}
|
||||
|
||||
const infiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
const cursor = extraPagesResult.value?.admin?.userList.cursor || null
|
||||
if (!moreToLoad.value || !cursor) return state.complete()
|
||||
|
||||
try {
|
||||
await fetchMorePages({
|
||||
variables: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
state.error()
|
||||
return
|
||||
}
|
||||
|
||||
state.loaded()
|
||||
if (!moreToLoad.value) {
|
||||
state.complete()
|
||||
}
|
||||
}
|
||||
|
||||
const calculateLoaderId = () => {
|
||||
infiniteLoaderId.value = resultVariables.value?.query || ''
|
||||
}
|
||||
|
||||
const toggleInviteDialog = () => {
|
||||
showInviteDialog.value = true
|
||||
}
|
||||
|
||||
onResult(calculateLoaderId)
|
||||
</script>
|
||||
+75
-84
@@ -1,68 +1,78 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Edit Settings</template>
|
||||
<form @submit="onSubmit">
|
||||
<div class="flex flex-col gap-4">
|
||||
<FormTextInput
|
||||
v-model="name"
|
||||
label="This server's public name"
|
||||
name="serverName"
|
||||
color="foundation"
|
||||
placeholder="Server name"
|
||||
show-label
|
||||
:show-required="true"
|
||||
:rules="requiredRule"
|
||||
:type="'text'"
|
||||
/>
|
||||
<FormTextArea
|
||||
v-model="description"
|
||||
color="foundation"
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
show-label
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="company"
|
||||
color="foundation"
|
||||
label="Owner"
|
||||
name="owner"
|
||||
placeholder="Owner"
|
||||
show-label
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="adminContact"
|
||||
color="foundation"
|
||||
label="Admin Email"
|
||||
name="adminEmail"
|
||||
placeholder="Admin Email"
|
||||
show-label
|
||||
:type="'email'"
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="termsOfService"
|
||||
color="foundation"
|
||||
label="Url pointing to the terms of service page"
|
||||
name="terms"
|
||||
show-label
|
||||
/>
|
||||
<div class="text-sm flex flex-col gap-2 mt-2">
|
||||
<FormCheckbox
|
||||
v-model="inviteOnly"
|
||||
label="Invite only mode - Only users with an invitation will be able to join"
|
||||
name="inviteOnly"
|
||||
show-label
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-model="guestModeEnabled"
|
||||
label="Guest mode - Enables the 'Guest' server role, which allows users to only contribute to projects that they're invited to"
|
||||
name="guestModeEnabled"
|
||||
show-label
|
||||
/>
|
||||
</div>
|
||||
<section>
|
||||
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader title="General" text="Manage general server information" />
|
||||
<div class="flex flex-col space-y-6">
|
||||
<SettingsSectionHeader title="Server details" subheading />
|
||||
<form class="flex flex-col gap-2" @submit="onSubmit">
|
||||
<div class="flex flex-col gap-4">
|
||||
<FormTextInput
|
||||
v-model="name"
|
||||
label="Public name"
|
||||
name="serverName"
|
||||
color="foundation"
|
||||
placeholder="Server name"
|
||||
show-label
|
||||
:show-required="true"
|
||||
:rules="requiredRule"
|
||||
type="text"
|
||||
/>
|
||||
<FormTextArea
|
||||
v-model="description"
|
||||
color="foundation"
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
show-label
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="company"
|
||||
color="foundation"
|
||||
label="Owner"
|
||||
name="owner"
|
||||
placeholder="Owner"
|
||||
show-label
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="adminContact"
|
||||
color="foundation"
|
||||
label="Admin email"
|
||||
name="adminEmail"
|
||||
placeholder="Admin email"
|
||||
show-label
|
||||
type="email"
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="termsOfService"
|
||||
color="foundation"
|
||||
label="URL to the Terms of Service"
|
||||
name="terms"
|
||||
show-label
|
||||
/>
|
||||
<div class="text-sm flex flex-col gap-2 mt-2">
|
||||
<FormCheckbox
|
||||
v-model="inviteOnly"
|
||||
label="Invite only mode - Only users with an invitation will be able to join"
|
||||
name="inviteOnly"
|
||||
show-label
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-model="guestModeEnabled"
|
||||
label="Guest mode - Enables the 'Guest' server role, which allows users to only contribute to projects that they're invited to"
|
||||
name="guestModeEnabled"
|
||||
show-label
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormButton color="default" @click="onSubmit">Save changes</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</LayoutDialog>
|
||||
<hr class="my-6 md:my-10" />
|
||||
<SettingsServerGeneralVersion />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -71,11 +81,9 @@ import { useForm } from 'vee-validate'
|
||||
import { isRequired } from '~~/lib/common/helpers/validation'
|
||||
import { useGlobalToast, ToastNotificationType } from '~~/lib/common/composables/toast'
|
||||
import {
|
||||
LayoutDialog,
|
||||
FormTextInput,
|
||||
FormTextArea,
|
||||
useFormCheckboxModel,
|
||||
type LayoutDialogButton
|
||||
useFormCheckboxModel
|
||||
} from '@speckle/ui-components'
|
||||
import { useLogger } from '~~/composables/logging'
|
||||
import {
|
||||
@@ -115,21 +123,6 @@ const { model: inviteOnly, isChecked: isInviteOnlyChecked } = useFormCheckboxMod
|
||||
const { model: guestModeEnabled, isChecked: isGuestModeChecked } =
|
||||
useFormCheckboxModel()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'secondary', fullWidth: true, outline: true },
|
||||
onClick: () => (isOpen.value = false)
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
props: { color: 'default', fullWidth: true, outline: false },
|
||||
onClick: onSubmit
|
||||
}
|
||||
])
|
||||
|
||||
const requiredRule = [isRequired]
|
||||
|
||||
const updateServerInfoAndCache = async (
|
||||
@@ -181,7 +174,6 @@ const onSubmit = handleSubmit(async () => {
|
||||
title: 'Successfully saved',
|
||||
description: 'Your server settings have been saved.'
|
||||
})
|
||||
isOpen.value = false
|
||||
} else {
|
||||
logger.error(result && result.errors)
|
||||
triggerNotification({
|
||||
@@ -192,8 +184,7 @@ const onSubmit = handleSubmit(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(isOpen, (newVal, oldVal) => {
|
||||
if (!newVal || oldVal) return
|
||||
onBeforeMount(() => {
|
||||
if (!result.value?.serverInfo) return
|
||||
|
||||
name.value = result.value.serverInfo.name
|
||||
+86
-103
@@ -1,100 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink to="/server-management" name="Server Management"></HeaderNavLink>
|
||||
<HeaderNavLink
|
||||
to="/server-management/pending-invitations"
|
||||
name="Pending Invitations"
|
||||
></HeaderNavLink>
|
||||
</Portal>
|
||||
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="h4 font-bold">Pending Invitations</h1>
|
||||
<FormButton :icon-left="UserPlusIcon" @click="toggleInviteDialog">
|
||||
Invite
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<FormTextInput
|
||||
size="lg"
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!searchString"
|
||||
placeholder="Search Invitations"
|
||||
class="rounded-md border border-outline-3"
|
||||
@update:model-value="debounceSearchUpdate"
|
||||
@change="($event) => searchUpdateHandler($event.value)"
|
||||
/>
|
||||
|
||||
<LayoutTable
|
||||
class="mt-8"
|
||||
:columns="[
|
||||
{ id: 'email', header: 'Email', classes: 'col-span-5 truncate' },
|
||||
{ id: 'invitedBy', header: 'Invited By', classes: 'col-span-4' },
|
||||
{ id: 'resend', header: '', classes: 'col-span-3' }
|
||||
]"
|
||||
:items="invites"
|
||||
:buttons="[
|
||||
{ icon: TrashIcon, label: 'Delete', action: openDeleteInvitationDialog }
|
||||
]"
|
||||
>
|
||||
<template #email="{ item }">
|
||||
{{ isInvite(item) ? item.email : '' }}
|
||||
</template>
|
||||
|
||||
<template #invitedBy="{ item }">
|
||||
<div class="flex items-center gap-2 py-1">
|
||||
<UserAvatar v-if="isInvite(item)" :user="item.invitedBy" />
|
||||
<span class="truncate">
|
||||
{{ isInvite(item) ? item.invitedBy.name : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #resend="{ item }">
|
||||
<FormButton
|
||||
:link="true"
|
||||
:class="{
|
||||
'font-semibold': true,
|
||||
'text-primary': !successfullyResentInvites.includes(item.id),
|
||||
'text-foreground': successfullyResentInvites.includes(item.id)
|
||||
}"
|
||||
:disabled="successfullyResentInvites.includes(item.id)"
|
||||
@click="resendInvitation(item as InviteItem)"
|
||||
>
|
||||
{{
|
||||
successfullyResentInvites.includes(item.id)
|
||||
? 'Invitation Resent'
|
||||
: 'Resend Invitation'
|
||||
}}
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader
|
||||
title="Pending invitations"
|
||||
text="And overview of all your pending invititations"
|
||||
/>
|
||||
<div class="flex flex-col-reverse md:flex-row">
|
||||
<FormTextInput
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!search"
|
||||
placeholder="Search invitations"
|
||||
class="rounded-md border border-outline-3 md:max-w-md mt-6 md:mt-0"
|
||||
:model-value="bind.modelValue.value"
|
||||
v-on="on"
|
||||
/>
|
||||
<FormButton :icon-left="UserPlusIcon" @click="toggleInviteDialog">
|
||||
Invite
|
||||
</FormButton>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
|
||||
<ServerManagementDeleteInvitationDialog
|
||||
v-model:open="showDeleteInvitationDialog"
|
||||
:invite="inviteToModify"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
<LayoutTable
|
||||
class="mt-6 md:mt-8"
|
||||
:columns="[
|
||||
{ id: 'email', header: 'Email', classes: 'col-span-5 truncate' },
|
||||
{ id: 'invitedBy', header: 'Invited by', classes: 'col-span-4' },
|
||||
{ id: 'resend', header: 'Resend', classes: 'col-span-3' }
|
||||
]"
|
||||
:items="invites"
|
||||
:buttons="[
|
||||
{ icon: TrashIcon, label: 'Delete', action: openDeleteInvitationDialog }
|
||||
]"
|
||||
>
|
||||
<template #email="{ item }">
|
||||
{{ isInvite(item) ? item.email : '' }}
|
||||
</template>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !invites?.length" loading />
|
||||
<template #invitedBy="{ item }">
|
||||
<div class="flex items-center gap-2 py-1">
|
||||
<UserAvatar v-if="isInvite(item)" :user="item.invitedBy" />
|
||||
<span class="truncate">
|
||||
{{ isInvite(item) ? item.invitedBy.name : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="invites?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="py-4"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
<ServerManagementInviteDialog v-model:open="showInviteDialog" />
|
||||
</div>
|
||||
<template #resend="{ item }">
|
||||
<FormButton
|
||||
:link="true"
|
||||
:class="{
|
||||
'font-semibold': true,
|
||||
'text-primary': !successfullyResentInvites.includes(item.id),
|
||||
'text-foreground': successfullyResentInvites.includes(item.id)
|
||||
}"
|
||||
:disabled="successfullyResentInvites.includes(item.id)"
|
||||
@click="resendInvitation(item as InviteItem)"
|
||||
>
|
||||
{{
|
||||
successfullyResentInvites.includes(item.id)
|
||||
? 'Invitation resent'
|
||||
: 'Resend invitation'
|
||||
}}
|
||||
</FormButton>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
|
||||
<SettingsServerPendingInvitationsDeleteDialog
|
||||
v-model:open="showDeleteInvitationDialog"
|
||||
:invite="inviteToModify"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !invites?.length" loading />
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="invites?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="py-4"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
<SettingsServerUserInviteDialog v-model:open="showInviteDialog" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
import { MagnifyingGlassIcon, TrashIcon, UserPlusIcon } from '@heroicons/vue/24/outline'
|
||||
import type { ItemType, InviteItem } from '~~/lib/server-management/helpers/types'
|
||||
@@ -107,21 +101,16 @@ import {
|
||||
convertThrowIntoFetchResult,
|
||||
getFirstErrorMessage
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { useDebouncedTextInput } from '@speckle/ui-components'
|
||||
|
||||
useHead({
|
||||
title: 'Pending Invitations'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['admin']
|
||||
})
|
||||
const search = defineModel<string>('search')
|
||||
|
||||
const logger = useLogger()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const { mutate: resendInvitationMutation } = useMutation(adminResendInviteMutation)
|
||||
const { on, bind } = useDebouncedTextInput({ model: search })
|
||||
|
||||
const inviteToModify = ref<InviteItem | null>(null)
|
||||
const searchString = ref('')
|
||||
const showDeleteInvitationDialog = ref(false)
|
||||
const infiniteLoaderId = ref('')
|
||||
const successfullyResentInvites = ref<string[]>([])
|
||||
@@ -135,7 +124,7 @@ const {
|
||||
loading
|
||||
} = useQuery(getInvitesQuery, () => ({
|
||||
limit: 50,
|
||||
query: searchString.value
|
||||
query: search.value
|
||||
}))
|
||||
|
||||
const moreToLoad = computed(
|
||||
@@ -201,12 +190,6 @@ const infiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
}
|
||||
}
|
||||
|
||||
const searchUpdateHandler = (value: string) => {
|
||||
searchString.value = value
|
||||
}
|
||||
|
||||
const debounceSearchUpdate = debounce(searchUpdateHandler, 500)
|
||||
|
||||
const calculateLoaderId = () => {
|
||||
infiniteLoaderId.value = resultVariables.value?.query || ''
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader
|
||||
title="Projects"
|
||||
text="Manage projects across the server"
|
||||
/>
|
||||
<div class="flex flex-col-reverse md:justify-between md:flex-row md:gap-x-4">
|
||||
<div class="relative w-full md:max-w-md mt-6 md:mt-0">
|
||||
<FormTextInput
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!search"
|
||||
placeholder="Search projects"
|
||||
class="rounded-md border border-outline-3 md:max-w-md mt-6 md:mt-0"
|
||||
:model-value="bind.modelValue.value"
|
||||
v-on="on"
|
||||
/>
|
||||
</div>
|
||||
<FormButton :icon-left="PlusIcon" @click="openNewProject = true">
|
||||
New
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<LayoutTable
|
||||
class="mt-6 md:mt-8"
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'type', header: 'Type', classes: 'col-span-1' },
|
||||
{ id: 'created', header: 'Created', classes: 'col-span-2' },
|
||||
{ id: 'modified', header: 'Modified', classes: 'col-span-2' },
|
||||
{ id: 'models', header: 'Models', classes: 'col-span-1' },
|
||||
{ id: 'versions', header: 'Versions', classes: 'col-span-1' },
|
||||
{ id: 'contributors', header: 'Contributors', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="projects"
|
||||
:buttons="[
|
||||
{ icon: TrashIcon, label: 'Delete', action: openProjectDeleteDialog }
|
||||
]"
|
||||
:on-row-click="handleProjectClick"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ isProject(item) ? item.name : '' }}
|
||||
</template>
|
||||
|
||||
<template #type="{ item }">
|
||||
<div class="capitalize">
|
||||
{{ isProject(item) ? item.visibility.toLowerCase() : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #created="{ item }">
|
||||
<div class="text-xs">
|
||||
{{ formattedFullDate(item.createdAt) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #modified="{ item }">
|
||||
<div class="text-xs">
|
||||
{{ formattedFullDate(item.updatedAt) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #models="{ item }">
|
||||
<div class="text-xs">
|
||||
{{ isProject(item) ? item.models.totalCount : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #versions="{ item }">
|
||||
<div class="text-xs">
|
||||
{{ isProject(item) ? item.versions.totalCount : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #contributors="{ item }">
|
||||
<div v-if="isProject(item)" class="py-1">
|
||||
<UserAvatarGroup :users="item.team.map((t) => t.user)" :max-count="3" />
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !projects?.length" loading />
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="projects?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="py-4"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
|
||||
<SettingsServerProjectDeleteDialog
|
||||
v-model:open="showProjectDeleteDialog"
|
||||
:project="projectToModify"
|
||||
title="Delete project"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
|
||||
<ProjectsAddDialog v-model:open="openNewProject" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { MagnifyingGlassIcon, TrashIcon, PlusIcon } from '@heroicons/vue/24/outline'
|
||||
import { getProjectsQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import type { ItemType, ProjectItem } from '~~/lib/server-management/helpers/types'
|
||||
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
|
||||
import { isProject } from '~~/lib/server-management/helpers/utils'
|
||||
import { useDebouncedTextInput } from '@speckle/ui-components'
|
||||
|
||||
const search = defineModel<string>('search')
|
||||
|
||||
const { on, bind } = useDebouncedTextInput({ model: search })
|
||||
const logger = useLogger()
|
||||
const router = useRouter()
|
||||
|
||||
const projectToModify = ref<ProjectItem | null>(null)
|
||||
const showProjectDeleteDialog = ref(false)
|
||||
const infiniteLoaderId = ref('')
|
||||
const openNewProject = ref(false)
|
||||
|
||||
const {
|
||||
result: extraPagesResult,
|
||||
fetchMore: fetchMorePages,
|
||||
variables: resultVariables,
|
||||
onResult,
|
||||
loading
|
||||
} = useQuery(getProjectsQuery, () => ({
|
||||
limit: 50,
|
||||
query: search.value
|
||||
}))
|
||||
|
||||
const moreToLoad = computed(
|
||||
() =>
|
||||
!extraPagesResult.value?.admin?.projectList ||
|
||||
extraPagesResult.value.admin.projectList.items.length <
|
||||
extraPagesResult.value.admin.projectList.totalCount
|
||||
)
|
||||
|
||||
const projects = computed(() => extraPagesResult.value?.admin.projectList.items || [])
|
||||
|
||||
const openProjectDeleteDialog = (item: ItemType) => {
|
||||
if (isProject(item)) {
|
||||
projectToModify.value = item
|
||||
showProjectDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleProjectClick = (item: ItemType) => {
|
||||
router.push(`/projects/${item.id}`)
|
||||
}
|
||||
|
||||
const infiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
const cursor = extraPagesResult.value?.admin?.projectList.cursor || null
|
||||
if (!moreToLoad.value || !cursor) return state.complete()
|
||||
|
||||
try {
|
||||
await fetchMorePages({
|
||||
variables: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
state.error()
|
||||
return
|
||||
}
|
||||
|
||||
state.loaded()
|
||||
if (!moreToLoad.value) {
|
||||
state.complete()
|
||||
}
|
||||
}
|
||||
|
||||
const calculateLoaderId = () => {
|
||||
infiniteLoaderId.value = resultVariables.value?.query || ''
|
||||
}
|
||||
|
||||
onResult(calculateLoaderId)
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<h2 class="text-xl">Speckle Version</h2>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[50%]">
|
||||
<p class="text-sm">
|
||||
<span class="font-semibold">Current version:</span>
|
||||
{{ currentVersion }}
|
||||
</p>
|
||||
<p v-if="!isLatestVersion" class="text-sm pt-2">New version available</p>
|
||||
</div>
|
||||
<div class="flex justify-end w-[50%]">
|
||||
<FormButton
|
||||
color="secondary"
|
||||
:disabled="isLatestVersion"
|
||||
@click="openGithubReleasePage"
|
||||
>
|
||||
{{ isLatestVersion ? 'You are up to date' : `Update to ${latestVersion}` }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { serverManagementDataQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface GithubRelease {
|
||||
url: string
|
||||
assets_url: string
|
||||
upload_url: string
|
||||
html_url: string
|
||||
id: number
|
||||
node_id: string
|
||||
tag_name: string
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
const { result } = useQuery(serverManagementDataQuery)
|
||||
|
||||
const latestVersion = ref<string | null>(null)
|
||||
|
||||
const currentVersion = computed(() => result.value?.serverInfo.version)
|
||||
const isLatestVersion = computed(() => {
|
||||
return (
|
||||
!latestVersion.value ||
|
||||
currentVersion.value === latestVersion.value ||
|
||||
currentVersion.value === 'dev' ||
|
||||
currentVersion.value?.includes('alpha') ||
|
||||
currentVersion.value === 'N/A'
|
||||
)
|
||||
})
|
||||
|
||||
const openGithubReleasePage = () => {
|
||||
window.open('https://github.com/specklesystems/speckle-server/releases', '_blank')
|
||||
}
|
||||
|
||||
async function getLatestVersion(): Promise<string | null> {
|
||||
try {
|
||||
const response: Response = await fetch(
|
||||
'https://api.github.com/repos/specklesystems/speckle-server/releases/latest'
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
} else {
|
||||
const data = (await response.json()) as GithubRelease
|
||||
return data.tag_name
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
latestVersion.value = await getLatestVersion()
|
||||
</script>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Delete Invitation</template>
|
||||
<template #header>Delete invitation</template>
|
||||
<div class="flex flex-col gap-6 text-sm text-foreground">
|
||||
<p>Are you sure you want to delete the selected invitation?</p>
|
||||
<div v-if="invite" class="flex flex-col gap-2">
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Change Role</template>
|
||||
<template #header>Change role</template>
|
||||
<div class="flex flex-col gap-6 text-sm text-foreground">
|
||||
<p>
|
||||
Are you sure you want to
|
||||
@@ -117,7 +117,7 @@ const changeUserRoleConfirmed = async () => {
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Change Role',
|
||||
text: 'Change role',
|
||||
props: { color: 'danger', fullWidth: true },
|
||||
onClick: changeUserRoleConfirmed
|
||||
},
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Delete User</template>
|
||||
<template #header>Delete user</template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<p>
|
||||
Are you sure you want to
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
|
||||
<div class="flex flex-col">
|
||||
<SettingsSectionHeader
|
||||
title="Developer settings"
|
||||
text="Manage your tokens and authorized app"
|
||||
/>
|
||||
<div class="flex flex-col gap-6 md:gap-12">
|
||||
<div class="flex flex-col">
|
||||
<SettingsSectionHeader
|
||||
title="Explore GraphQL"
|
||||
class="md:gap-0"
|
||||
subheading
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'secondary',
|
||||
target: '_blank',
|
||||
external: true,
|
||||
iconLeft: BookOpenIcon
|
||||
},
|
||||
onClick: goToExplorer,
|
||||
label: 'Open docs'
|
||||
}
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingsSectionHeader
|
||||
title="Access tokens"
|
||||
subheading
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'secondary',
|
||||
to: 'https://speckle.guide/dev/tokens.html',
|
||||
iconLeft: BookOpenIcon,
|
||||
target: '_blank',
|
||||
external: true
|
||||
},
|
||||
label: 'Open docs'
|
||||
},
|
||||
{
|
||||
props: {
|
||||
iconLeft: PlusIcon,
|
||||
onClick: openCreateTokenDialog
|
||||
},
|
||||
label: 'New token'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<p class="text-sm pt-6 md:pt-4">
|
||||
Personal Access Tokens can be used to access the Speckle API on this
|
||||
server; they function like ordinary OAuth access tokens. Use them in
|
||||
your scripts or apps!
|
||||
<strong>
|
||||
Treat them like a password: do not post them anywhere where they could
|
||||
be accessed by others (e.g., public repos).
|
||||
</strong>
|
||||
</p>
|
||||
</SettingsSectionHeader>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'id', header: 'ID', classes: 'col-span-2' },
|
||||
{
|
||||
id: 'scope',
|
||||
header: 'Scope',
|
||||
classes: 'col-span-7 whitespace-break-spaces text-xs'
|
||||
}
|
||||
]"
|
||||
:items="tokens"
|
||||
:buttons="[
|
||||
{
|
||||
icon: TrashIcon,
|
||||
label: 'Delete',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #id="{ item }">
|
||||
<span class="rounded text-xs font-mono bg-foundation-page p-2">
|
||||
{{ item.id }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #scope="{ item }">
|
||||
{{ getItemScopes(item) }}
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingsSectionHeader
|
||||
subheading
|
||||
title="Applications"
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'secondary',
|
||||
to: 'https://speckle.guide/dev/apps.html',
|
||||
target: '_blank',
|
||||
external: true,
|
||||
iconLeft: BookOpenIcon
|
||||
},
|
||||
label: 'Open docs'
|
||||
},
|
||||
{
|
||||
props: {
|
||||
onClick: openCreateApplicationDialog,
|
||||
iconLeft: PlusIcon
|
||||
},
|
||||
label: 'New application'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<p class="text-sm pt-6 md:pt-4">
|
||||
Register and manage third-party Speckle Apps that, once authorised by a
|
||||
user on this server, can act on their behalf.
|
||||
</p>
|
||||
</SettingsSectionHeader>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3' },
|
||||
{ id: 'id', header: 'ID', classes: 'col-span-2' },
|
||||
{
|
||||
id: 'scope',
|
||||
header: 'Scope',
|
||||
classes: 'col-span-7 whitespace-break-spaces text-xs'
|
||||
}
|
||||
]"
|
||||
:items="applications"
|
||||
:buttons="[
|
||||
{
|
||||
icon: LockOpenIcon,
|
||||
label: 'Reveal Secret',
|
||||
action: openRevealSecretDialog,
|
||||
textColor: 'primary'
|
||||
},
|
||||
{
|
||||
icon: PencilIcon,
|
||||
label: 'Edit',
|
||||
action: openEditApplicationDialog,
|
||||
textColor: 'primary'
|
||||
},
|
||||
{
|
||||
icon: TrashIcon,
|
||||
label: 'Delete',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #id="{ item }">
|
||||
<span class="rounded text-xs font-mono bg-foundation-page p-2">
|
||||
{{ item.id }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #scope="{ item }">
|
||||
{{ getItemScopes(item) }}
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingsSectionHeader
|
||||
subheading
|
||||
title="Authorized Apps"
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'secondary',
|
||||
to: 'https://speckle.guide/dev/apps.html',
|
||||
target: '_blank',
|
||||
external: true,
|
||||
iconLeft: BookOpenIcon
|
||||
},
|
||||
label: 'Open docs'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<p class="text-sm pt-6 md:pt-4">
|
||||
Here you can review the apps that you have granted access to. If
|
||||
something looks suspicious, revoke the access.
|
||||
</p>
|
||||
</SettingsSectionHeader>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 ' },
|
||||
{ id: 'author', header: 'Author', classes: 'col-span-3 ' },
|
||||
{
|
||||
id: 'description',
|
||||
header: 'Description',
|
||||
classes: 'col-span-6 !pt-1.5'
|
||||
}
|
||||
]"
|
||||
:items="authorizedApps"
|
||||
:buttons="[
|
||||
{
|
||||
icon: XMarkIcon,
|
||||
label: 'Revoke Access',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
row-items-align="stretch"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #author="{ item }">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<template v-if="item.author">
|
||||
<UserAvatar :user="item.author" />
|
||||
<span>{{ item.author.name }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HeaderLogoBlock minimal no-link />
|
||||
<span>Speckle</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #description="{ item }">
|
||||
{{ item.description }}
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsUserDeveloperCreateTokenDialog
|
||||
v-model:open="showCreateTokenDialog"
|
||||
@token-created="(token) => handleTokenCreated(token)"
|
||||
/>
|
||||
<SettingsUserDeveloperDeleteDialog
|
||||
v-model:open="showDeleteDialog"
|
||||
:item="itemToModify"
|
||||
/>
|
||||
<SettingsUserDeveloperCreateEditApplicationDialog
|
||||
v-model:open="showCreateEditApplicationDialog"
|
||||
:application="(itemToModify as ApplicationItem)"
|
||||
@application-created="handleApplicationCreated"
|
||||
/>
|
||||
<SettingsUserDeveloperRevealSecretDialog
|
||||
v-model:open="showRevealSecretDialog"
|
||||
:application="itemToModify && 'secret' in itemToModify ? itemToModify : null"
|
||||
/>
|
||||
<SettingsUserDeveloperCreateTokenSuccessDialog
|
||||
v-model:open="showCreateTokenSuccessDialog"
|
||||
:token="tokenSuccess"
|
||||
/>
|
||||
<SettingsUserDeveloperCreateApplicationSuccessDialog
|
||||
v-model:open="showCreateApplicationSuccessDialog"
|
||||
:application="(itemToModify as ApplicationItem)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PlusIcon,
|
||||
BookOpenIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
LockOpenIcon,
|
||||
XMarkIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import type {
|
||||
TokenItem,
|
||||
ApplicationItem,
|
||||
AuthorizedAppItem
|
||||
} from '~~/lib/developer-settings/helpers/types'
|
||||
import {
|
||||
developerSettingsAccessTokensQuery,
|
||||
developerSettingsApplicationsQuery,
|
||||
developerSettingsAuthorizedAppsQuery
|
||||
} from '~~/lib/developer-settings/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
useHead({
|
||||
title: 'Developer Settings'
|
||||
})
|
||||
|
||||
const apiOrigin = useApiOrigin()
|
||||
|
||||
const { result: tokensResult, refetch: refetchTokens } = useQuery(
|
||||
developerSettingsAccessTokensQuery
|
||||
)
|
||||
const { result: applicationsResult, refetch: refetchApplications } = useQuery(
|
||||
developerSettingsApplicationsQuery
|
||||
)
|
||||
const { result: authorizedAppsResult } = useQuery(developerSettingsAuthorizedAppsQuery)
|
||||
|
||||
const itemToModify = ref<TokenItem | ApplicationItem | AuthorizedAppItem | null>(null)
|
||||
const tokenSuccess = ref('')
|
||||
const showCreateTokenDialog = ref(false)
|
||||
const showCreateTokenSuccessDialog = ref(false)
|
||||
const showCreateApplicationSuccessDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showCreateEditApplicationDialog = ref(false)
|
||||
const showRevealSecretDialog = ref(false)
|
||||
|
||||
const tokens = computed<TokenItem[]>(() => {
|
||||
return (
|
||||
tokensResult.value?.activeUser?.apiTokens?.filter(
|
||||
(token): token is TokenItem => token !== null
|
||||
) || []
|
||||
)
|
||||
})
|
||||
|
||||
const applications = computed<ApplicationItem[]>(() => {
|
||||
return applicationsResult.value?.activeUser?.createdApps || []
|
||||
})
|
||||
|
||||
const authorizedApps = computed(() =>
|
||||
(authorizedAppsResult.value?.activeUser?.authorizedApps || []).filter(
|
||||
(app) => app.id !== 'spklwebapp'
|
||||
)
|
||||
)
|
||||
|
||||
const openDeleteDialog = (item: TokenItem | ApplicationItem | AuthorizedAppItem) => {
|
||||
itemToModify.value = item
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const openCreateApplicationDialog = () => {
|
||||
itemToModify.value = null
|
||||
showCreateEditApplicationDialog.value = true
|
||||
}
|
||||
|
||||
const openCreateTokenDialog = () => {
|
||||
showCreateTokenDialog.value = true
|
||||
}
|
||||
|
||||
const openEditApplicationDialog = (item: ApplicationItem) => {
|
||||
itemToModify.value = item
|
||||
showCreateEditApplicationDialog.value = true
|
||||
}
|
||||
|
||||
const openRevealSecretDialog = (item: ApplicationItem) => {
|
||||
itemToModify.value = item
|
||||
showRevealSecretDialog.value = true
|
||||
}
|
||||
|
||||
const handleTokenCreated = (token: string) => {
|
||||
refetchTokens()
|
||||
tokenSuccess.value = token
|
||||
showCreateTokenSuccessDialog.value = true
|
||||
}
|
||||
|
||||
const handleApplicationCreated = (applicationId: string) => {
|
||||
refetchApplications()?.then(() => {
|
||||
const newApplication = applications.value.find((app) => app.id === applicationId)
|
||||
|
||||
if (newApplication) {
|
||||
itemToModify.value = newApplication
|
||||
showCreateApplicationSuccessDialog.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToExplorer = () => {
|
||||
if (!import.meta.client) return
|
||||
window.location.href = new URL('/explorer', apiOrigin).toString()
|
||||
}
|
||||
|
||||
const getItemScopes = (item: TokenItem | ApplicationItem): string => {
|
||||
return item.scopes
|
||||
? item.scopes
|
||||
.map(
|
||||
(event, index, array) => `"${event}"${index < array.length - 1 ? ',' : ''}`
|
||||
)
|
||||
.join(' ')
|
||||
: 'No scopes available'
|
||||
}
|
||||
</script>
|
||||
+48
-43
@@ -1,54 +1,59 @@
|
||||
<template>
|
||||
<LayoutDialogSection title="Notification preferences" border-t border-b>
|
||||
<template #icon>
|
||||
<BellIcon class="h-full w-full" />
|
||||
</template>
|
||||
<table class="table-auto w-full rounded-t overflow-hidden">
|
||||
<thead class="text-foreground-1">
|
||||
<tr>
|
||||
<th class="bg-primary-muted py-2 px-4">Notification type</th>
|
||||
<th
|
||||
v-for="channel in notificationChannels"
|
||||
:key="channel"
|
||||
class="bg-primary-muted text-right py-2 px-4"
|
||||
<section>
|
||||
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader
|
||||
title="Notifications"
|
||||
text="Manage your notification preferences"
|
||||
/>
|
||||
<table class="table-auto w-full rounded-t overflow-hidden">
|
||||
<thead class="text-foreground-1">
|
||||
<tr>
|
||||
<th class="pb-4 font-semibold text-sm text-left">Notification type</th>
|
||||
<th
|
||||
v-for="channel in notificationChannels"
|
||||
:key="channel"
|
||||
class="text-right font-semibold pb-4 text-sm"
|
||||
>
|
||||
{{ capitalize(channel) }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="[type, settings] in Object.entries(localPreferences)"
|
||||
:key="type"
|
||||
class="border-t"
|
||||
>
|
||||
{{ capitalize(channel) }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="[type, settings] in Object.entries(localPreferences)" :key="type">
|
||||
<td class="px-4 pb-1 pt-2 text-xs sm:text-sm">
|
||||
{{ notificationTypeMapping[type] || 'Unknown' }}
|
||||
</td>
|
||||
<td
|
||||
v-for="channel in notificationChannels"
|
||||
:key="channel"
|
||||
class="flex justify-end pt-2 pr-4"
|
||||
>
|
||||
<FormCheckbox
|
||||
:name="`${type} (${channel})`"
|
||||
:disabled="loading"
|
||||
hide-label
|
||||
:model-value="settings[channel] || undefined"
|
||||
@update:model-value="
|
||||
($event) => onUpdate({ value: !!$event, type, channel })
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</LayoutDialogSection>
|
||||
<td class="text-xs sm:text-sm py-4">
|
||||
{{ notificationTypeMapping[type] || 'Unknown' }}
|
||||
</td>
|
||||
<td
|
||||
v-for="channel in notificationChannels"
|
||||
:key="channel"
|
||||
class="flex justify-end py-4"
|
||||
>
|
||||
<FormCheckbox
|
||||
:name="`${type} (${channel})`"
|
||||
:disabled="loading"
|
||||
hide-label
|
||||
:model-value="settings[channel] || undefined"
|
||||
@update:model-value="
|
||||
($event) => onUpdate({ value: !!$event, type, channel })
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { LayoutDialogSection } from '@speckle/ui-components'
|
||||
import { BellIcon } from '@heroicons/vue/24/outline'
|
||||
import { capitalize, cloneDeep } from 'lodash-es'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { UserProfileEditDialogNotificationPreferences_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { useUpdateNotificationPreferences } from '~~/lib/user/composables/management'
|
||||
import type { NotificationPreferences } from '~~/lib/user/helpers/components'
|
||||
import type { UserProfileEditDialogNotificationPreferences_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment UserProfileEditDialogNotificationPreferences_User on User {
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
|
||||
<SettingsSectionHeader title="Profile" text="Manage your profile information" />
|
||||
<SettingsUserProfileDetails :user="user" />
|
||||
<hr class="my-6 md:my-10" />
|
||||
<SettingsUserProfileChangePassword :user="user" />
|
||||
<hr class="my-6 md:my-10" />
|
||||
<SettingsUserProfileDeleteAccount :user="user" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { SettingsUserProfile_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsUserProfile_User on User {
|
||||
...UserProfileEditDialogChangePassword_User
|
||||
...UserProfileEditDialogDeleteAccount_User
|
||||
...UserProfileEditDialogBio_User
|
||||
}
|
||||
`)
|
||||
|
||||
defineProps<{
|
||||
user: SettingsUserProfile_UserFragment
|
||||
}>()
|
||||
</script>
|
||||
+3
-3
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Create Application</template>
|
||||
<template #header>Create application</template>
|
||||
<div class="flex flex-col gap-4 text-sm text-foreground">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h6 class="h6 font-bold text-center">Your new app is ready</h6>
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-3 py-2 text-sm max-w-xs mx-auto">
|
||||
<div class="flex items-center">App Id:</div>
|
||||
<div class="flex items-center">App id:</div>
|
||||
<div class="w-40">
|
||||
<CommonClipboardInputWithToast
|
||||
v-if="props.application?.id"
|
||||
:value="props.application?.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">App Secret:</div>
|
||||
<div class="flex items-center">App secret:</div>
|
||||
<div class="w-40">
|
||||
<CommonClipboardInputWithToast
|
||||
v-if="props.application?.secret"
|
||||
+5
-7
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
prevent-close-on-click-outside
|
||||
>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>
|
||||
{{ props.application ? 'Edit Application' : 'Create Application' }}
|
||||
{{ props.application ? 'Edit application' : 'Create application' }}
|
||||
</template>
|
||||
<form @submit="onSubmit">
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -15,6 +10,7 @@
|
||||
label="Name"
|
||||
help="The name of your app"
|
||||
name="hookName"
|
||||
color="foundation"
|
||||
show-required
|
||||
:rules="[isRequired]"
|
||||
show-label
|
||||
@@ -41,6 +37,7 @@
|
||||
help="After authentication, the users will be redirected (together with an access token) to this URL."
|
||||
show-required
|
||||
name="redirectUrl"
|
||||
color="foundation"
|
||||
show-label
|
||||
:rules="[isRequired, isUrl]"
|
||||
type="text"
|
||||
@@ -50,6 +47,7 @@
|
||||
label="Description"
|
||||
help="A short description of your application."
|
||||
name="description"
|
||||
color="foundation"
|
||||
show-label
|
||||
type="text"
|
||||
/>
|
||||
+4
-7
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
prevent-close-on-click-outside
|
||||
>
|
||||
<template #header>Create Token</template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Create token</template>
|
||||
<form @submit="onSubmit">
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormTextInput
|
||||
@@ -13,6 +8,8 @@
|
||||
label="Name"
|
||||
help="A name to remember this token by. For example, the name of the script or application you're planning to use it in!"
|
||||
name="hookName"
|
||||
placeholder="Token name"
|
||||
color="foundation"
|
||||
:rules="[isRequired]"
|
||||
show-required
|
||||
show-label
|
||||
+3
-8
@@ -1,18 +1,13 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
prevent-close-on-click-outside
|
||||
>
|
||||
<template #header>Reveal Application Secret</template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Reveal application secret</template>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 py-2 text-sm">
|
||||
<div class="text-center sm:text-right font-bold sm:font-normal">App Name:</div>
|
||||
<p class="truncate text-center sm:text-left">{{ props.application?.name }}</p>
|
||||
<div
|
||||
class="text-center sm:text-right flex items-center justify-center sm:justify-end font-bold sm:font-normal"
|
||||
>
|
||||
App Secret:
|
||||
App secret:
|
||||
</div>
|
||||
<div class="w-44 mx-auto sm:ml-0">
|
||||
<CommonClipboardInputWithToast
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<SettingsSectionHeader title="Change password" subheading />
|
||||
<p class="text-sm">
|
||||
Press the button below to start the password reset process.
|
||||
<br />
|
||||
Once pressed, you will receive an e-mail with further instructions.
|
||||
</p>
|
||||
<div>
|
||||
<FormButton color="default" @click="onClick">Reset password</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { UserProfileEditDialogChangePassword_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { usePasswordReset } from '~~/lib/auth/composables/passwordReset'
|
||||
|
||||
graphql(`
|
||||
fragment UserProfileEditDialogChangePassword_User on User {
|
||||
id
|
||||
email
|
||||
}
|
||||
`)
|
||||
|
||||
const { sendResetEmail } = usePasswordReset()
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserProfileEditDialogChangePassword_UserFragment
|
||||
}>()
|
||||
|
||||
const onClick = async () => {
|
||||
const email = props.user.email
|
||||
if (!email) return
|
||||
await sendResetEmail(email)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<SettingsUserProfileDeleteAccountDialog
|
||||
v-model:open="showDeleteDialog"
|
||||
:user="user"
|
||||
/>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<SettingsSectionHeader title="Delete account" subheading />
|
||||
<div class="rounded border bg-foundation-page border-outline-3 text-sm py-4 px-6">
|
||||
We will delete all projects where you are the sole owner, and any associated
|
||||
data. We will ask you to type in your email address and press the delete button.
|
||||
</div>
|
||||
<div>
|
||||
<FormButton color="danger" @click="toggleDeleteDialog">
|
||||
Delete account
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UserProfileEditDialogDeleteAccount_UserFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
defineProps<{
|
||||
user: UserProfileEditDialogDeleteAccount_UserFragment
|
||||
}>()
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
function toggleDeleteDialog() {
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
</script>
|
||||
+5
-7
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<LayoutDialogSection border-b title="Delete Account" title-color="danger">
|
||||
<template #icon>
|
||||
<TrashIcon class="h-full w-full" />
|
||||
</template>
|
||||
<LayoutDialog v-model:open="isOpen" title="Delete account" max-width="md">
|
||||
<form class="flex flex-col gap-2" @submit="onDelete">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 py-3 px-4 bg-danger-lighter dark:bg-danger-darker rounded-md select-none mb-4"
|
||||
@@ -42,11 +39,10 @@
|
||||
</FormButton>
|
||||
</div>
|
||||
</form>
|
||||
</LayoutDialogSection>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { LayoutDialogSection } from '@speckle/ui-components'
|
||||
import { TrashIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
|
||||
import { useForm } from 'vee-validate'
|
||||
import type { GenericValidateFunction } from 'vee-validate'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
@@ -74,6 +70,8 @@ const props = defineProps<{
|
||||
user: UserProfileEditDialogDeleteAccount_UserFragment
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const { handleSubmit, errors } = useForm<{ deleteEmail: string }>()
|
||||
const { mutate, loading } = useDeleteAccount()
|
||||
|
||||
+29
-32
@@ -1,41 +1,38 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-4 mb-8">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col md:flex-row gap-2 sm:gap-8 items-center">
|
||||
<div class="w-full md:w-4/12">
|
||||
<UserProfileEditDialogAvatar :user="user" size="xxl" />
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4 w-full md:w-9/12">
|
||||
<FormTextInput
|
||||
v-model="name"
|
||||
color="foundation"
|
||||
label="Name"
|
||||
name="name"
|
||||
placeholder="John Doe"
|
||||
:custom-icon="UserIcon"
|
||||
show-label
|
||||
show-required
|
||||
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
|
||||
@change="save"
|
||||
/>
|
||||
<FormTextInput
|
||||
v-model="company"
|
||||
color="foundation"
|
||||
label="Company"
|
||||
name="company"
|
||||
placeholder="Example Ltd."
|
||||
:custom-icon="BriefcaseIcon"
|
||||
show-label
|
||||
:rules="[isStringOfLength({ maxLength: 512 })]"
|
||||
@change="save"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<SettingsSectionHeader title="Your details" subheading />
|
||||
<div class="grid md:grid-cols-2 pt-4">
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<SettingsUserProfileEditAvatar :user="user" size="xxl" />
|
||||
</div>
|
||||
<div class="pt-6 md:pt-0">
|
||||
<FormTextInput
|
||||
v-model="name"
|
||||
class="pt-2 pb-1"
|
||||
color="foundation"
|
||||
label="Name"
|
||||
name="name"
|
||||
placeholder="John Doe"
|
||||
show-label
|
||||
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
|
||||
@change="save"
|
||||
/>
|
||||
<hr class="mt-4 mb-2" />
|
||||
<FormTextInput
|
||||
v-model="company"
|
||||
color="foundation"
|
||||
label="Company"
|
||||
name="company"
|
||||
placeholder="Example Ltd."
|
||||
show-label
|
||||
:rules="[isStringOfLength({ maxLength: 512 })]"
|
||||
@change="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { UserIcon, BriefcaseIcon } from '@heroicons/vue/24/solid'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type {
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="isOpen" max-width="md">
|
||||
<template #header>Edit Profile</template>
|
||||
<div v-if="user" class="flex flex-col text-foreground">
|
||||
<UserProfileEditDialogBio :user="user" />
|
||||
<UserProfileEditDialogNotificationPreferences :user="user" />
|
||||
<LayoutDialogSection
|
||||
title="Developer Settings"
|
||||
:button="developerSettingsButton"
|
||||
border-b
|
||||
>
|
||||
<template #icon>
|
||||
<CodeBracketIcon class="h-full w-full" />
|
||||
</template>
|
||||
</LayoutDialogSection>
|
||||
<UserProfileEditDialogChangePassword :user="user" />
|
||||
<UserProfileEditDialogDeleteAccount :user="user" @deleted="isOpen = false" />
|
||||
<div class="text-xs text-foreground-2 mt-4">
|
||||
User ID:
|
||||
<CommonTextLink size="xs" no-underline @click="copyUserId">
|
||||
#{{ user.id }}
|
||||
</CommonTextLink>
|
||||
<template v-if="distinctId">
|
||||
|
|
||||
<CommonTextLink size="xs" no-underline @click="copyDistinctId">
|
||||
{{ distinctId }}
|
||||
</CommonTextLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CodeBracketIcon, ChevronRightIcon } from '@heroicons/vue/24/outline'
|
||||
import { useActiveUser } from '~/lib/auth/composables/activeUser'
|
||||
|
||||
type FormButtonColor =
|
||||
| 'default'
|
||||
| 'invert'
|
||||
| 'danger'
|
||||
| 'warning'
|
||||
| 'success'
|
||||
| 'card'
|
||||
| 'secondary'
|
||||
| 'info'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', val: boolean): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
const { activeUser: user, distinctId } = useActiveUser()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => !!(props.open && user.value),
|
||||
set: (newVal) => emit('update:open', newVal)
|
||||
})
|
||||
|
||||
const developerSettingsButton = computed(() => ({
|
||||
text: 'Manage',
|
||||
color: 'default' as FormButtonColor,
|
||||
to: '/developer-settings/',
|
||||
iconRight: ChevronRightIcon,
|
||||
onClick: () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
}))
|
||||
|
||||
const copyUserId = () => {
|
||||
if (!user.value) return
|
||||
copy(user.value.id)
|
||||
}
|
||||
|
||||
const copyDistinctId = () => {
|
||||
if (!distinctId.value) return
|
||||
copy(distinctId.value)
|
||||
}
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<LayoutDialogSection border-b title="Change Password" title-color="default">
|
||||
<template #icon>
|
||||
<LockClosedIcon class="h-full w-full" />
|
||||
</template>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
Press the button below to start the password reset process. Once pressed, you
|
||||
will receive an e-mail with further instructions.
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<FormButton color="default" @click="onClick">Reset password</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDialogSection>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { LayoutDialogSection } from '@speckle/ui-components'
|
||||
import { LockClosedIcon } from '@heroicons/vue/24/outline'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import { usePasswordReset } from '~~/lib/auth/composables/passwordReset'
|
||||
|
||||
const { activeUser } = useActiveUser()
|
||||
const { sendResetEmail } = usePasswordReset()
|
||||
|
||||
const onClick = async () => {
|
||||
const email = activeUser.value?.email
|
||||
if (!email) return
|
||||
await sendResetEmail(email)
|
||||
}
|
||||
</script>
|
||||
@@ -83,10 +83,12 @@ const documents = {
|
||||
"\n fragment ProjectsDashboardFilled on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": types.ProjectsDashboardFilledFragmentDoc,
|
||||
"\n fragment ProjectsInviteBanner on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.ProjectsInviteBannerFragmentDoc,
|
||||
"\n fragment ProjectsInviteBanners on User {\n projectInvites {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectsInviteBannersFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserProfileEditDialogAvatar_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n": types.UserProfileEditDialogBio_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogDeleteAccount_User on User {\n id\n email\n }\n": types.UserProfileEditDialogDeleteAccount_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n": types.UserProfileEditDialogNotificationPreferences_UserFragmentDoc,
|
||||
"\n fragment SettingsUserProfile_User on User {\n ...UserProfileEditDialogChangePassword_User\n ...UserProfileEditDialogDeleteAccount_User\n ...UserProfileEditDialogBio_User\n }\n": types.SettingsUserProfile_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogChangePassword_User on User {\n id\n email\n }\n": types.UserProfileEditDialogChangePassword_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogDeleteAccount_User on User {\n id\n email\n }\n": types.UserProfileEditDialogDeleteAccount_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n": types.UserProfileEditDialogBio_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserProfileEditDialogAvatar_UserFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
@@ -538,11 +540,15 @@ export function graphql(source: "\n fragment ProjectsInviteBanners on User {\n
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n"];
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsUserProfile_User on User {\n ...UserProfileEditDialogChangePassword_User\n ...UserProfileEditDialogDeleteAccount_User\n ...UserProfileEditDialogBio_User\n }\n"): (typeof documents)["\n fragment SettingsUserProfile_User on User {\n ...UserProfileEditDialogChangePassword_User\n ...UserProfileEditDialogDeleteAccount_User\n ...UserProfileEditDialogBio_User\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogChangePassword_User on User {\n id\n email\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogChangePassword_User on User {\n id\n email\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -550,7 +556,11 @@ export function graphql(source: "\n fragment UserProfileEditDialogDeleteAccount
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n"];
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserProfileEditDialogAvatar_User\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,6 +14,24 @@ export const downloadManagerRoute = '/download-manager'
|
||||
export const serverManagementRoute = '/server-management'
|
||||
export const connectorsPageUrl = 'https://speckle.systems/features/connectors/'
|
||||
|
||||
export const settingsQueries: {
|
||||
[key: string]: {
|
||||
[key: string]: string
|
||||
}
|
||||
} = {
|
||||
user: {
|
||||
profile: 'user/profile',
|
||||
notifications: 'user/notifications',
|
||||
developerSettings: 'user/developer-settings'
|
||||
},
|
||||
server: {
|
||||
general: 'server/general',
|
||||
projects: 'server/projects',
|
||||
activeUsers: 'server/active-users',
|
||||
pendingInvitations: 'server/pending-invitations'
|
||||
}
|
||||
}
|
||||
|
||||
export const projectRoute = (
|
||||
id: string,
|
||||
tab?: 'models' | 'discussions' | 'automations' | 'settings'
|
||||
|
||||
@@ -159,6 +159,51 @@ export default defineNuxtConfig({
|
||||
'access-control-allow-methods': 'GET',
|
||||
'Access-Control-Expose-Headers': '*'
|
||||
}
|
||||
},
|
||||
// Redirect old settings pages
|
||||
'/server-management/projects': {
|
||||
redirect: {
|
||||
to: '/?settings=server/projects',
|
||||
statusCode: 301
|
||||
}
|
||||
},
|
||||
'/server-management/active-users': {
|
||||
redirect: {
|
||||
to: '/?settings=server/active-users',
|
||||
statusCode: 301
|
||||
}
|
||||
},
|
||||
'/server-management/pending-invitations': {
|
||||
redirect: {
|
||||
to: '/?settings=server/pending-invitations',
|
||||
statusCode: 301
|
||||
}
|
||||
},
|
||||
'/server-management': {
|
||||
redirect: {
|
||||
to: '/?settings=server/general',
|
||||
statusCode: 301
|
||||
}
|
||||
},
|
||||
'/profile': {
|
||||
redirect: {
|
||||
to: '/?settings=user/profile',
|
||||
statusCode: 301
|
||||
}
|
||||
},
|
||||
|
||||
// Redirect settings 'route' to homepage with added query
|
||||
'/settings': { redirect: '/?settings=user/profile' },
|
||||
'/settings/user/profile': { redirect: '/?settings=user/profile' },
|
||||
'/settings/user/notifications': { redirect: '/?settings=user/notifications' },
|
||||
'/settings/user/developer-settings': {
|
||||
redirect: '/?settings=user/developer-settings'
|
||||
},
|
||||
'/settings/server/general': { redirect: '/?settings=server/general' },
|
||||
'/settings/server/projects': { redirect: '/?settings=server/projects' },
|
||||
'/settings/server/active-users': { redirect: '/?settings=server/active-users' },
|
||||
'/settings/server/pending-invitations': {
|
||||
redirect: '/?settings=server/pending-invitations'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink
|
||||
to="/developer-settings/"
|
||||
name="Developer Settings"
|
||||
></HeaderNavLink>
|
||||
</Portal>
|
||||
<div class="flex flex-col gap-16">
|
||||
<DeveloperSettingsSectionHeader
|
||||
title="Developer Settings"
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
target: '_blank',
|
||||
external: true
|
||||
},
|
||||
onClick: goToExplorer,
|
||||
label: 'Explore GraphQL'
|
||||
}
|
||||
]"
|
||||
>
|
||||
Heads up! The sections below are intended for developers.
|
||||
</DeveloperSettingsSectionHeader>
|
||||
<div class="flex flex-col gap-4">
|
||||
<DeveloperSettingsSectionHeader
|
||||
title="Access Tokens"
|
||||
subheading
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'invert',
|
||||
to: 'https://speckle.guide/dev/tokens.html',
|
||||
iconLeft: BookOpenIcon,
|
||||
target: '_blank',
|
||||
external: true
|
||||
},
|
||||
label: 'Open Docs'
|
||||
},
|
||||
{
|
||||
props: {
|
||||
iconLeft: PlusIcon,
|
||||
onClick: openCreateTokenDialog
|
||||
},
|
||||
label: 'New Token'
|
||||
}
|
||||
]"
|
||||
>
|
||||
Personal Access Tokens can be used to access the Speckle API on this server;
|
||||
they function like ordinary OAuth access tokens. Use them in your scripts or
|
||||
apps!
|
||||
<strong>
|
||||
Treat them like a password: do not post them anywhere where they could be
|
||||
accessed by others (e.g., public repos).
|
||||
</strong>
|
||||
</DeveloperSettingsSectionHeader>
|
||||
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'id', header: 'ID', classes: 'col-span-2' },
|
||||
{
|
||||
id: 'scope',
|
||||
header: 'Scope',
|
||||
classes: 'col-span-7 whitespace-break-spaces text-xs'
|
||||
}
|
||||
]"
|
||||
:items="tokens"
|
||||
:buttons="[
|
||||
{
|
||||
icon: TrashIcon,
|
||||
label: 'Delete',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #id="{ item }">
|
||||
<span class="rounded text-xs font-mono bg-foundation-page p-2">
|
||||
{{ item.id }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #scope="{ item }">
|
||||
<div>
|
||||
{{
|
||||
item.scopes
|
||||
? item.scopes
|
||||
.map(
|
||||
(event, index, array) =>
|
||||
`"${event}"${index < array.length - 1 ? ',' : ''}`
|
||||
)
|
||||
.join(' ')
|
||||
: 'No scopes available'
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<DeveloperSettingsSectionHeader
|
||||
subheading
|
||||
title="Applications"
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'invert',
|
||||
to: 'https://speckle.guide/dev/apps.html',
|
||||
target: '_blank',
|
||||
external: true,
|
||||
iconLeft: BookOpenIcon
|
||||
},
|
||||
label: 'Open Docs'
|
||||
},
|
||||
{
|
||||
props: {
|
||||
onClick: openCreateApplicationDialog,
|
||||
iconLeft: PlusIcon
|
||||
},
|
||||
label: 'New Application'
|
||||
}
|
||||
]"
|
||||
>
|
||||
Register and manage third-party Speckle Apps that, once authorised by a user
|
||||
on this server, can act on their behalf.
|
||||
</DeveloperSettingsSectionHeader>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3' },
|
||||
{ id: 'id', header: 'ID', classes: 'col-span-2' },
|
||||
{
|
||||
id: 'scope',
|
||||
header: 'Scope',
|
||||
classes: 'col-span-7 whitespace-break-spaces text-xs'
|
||||
}
|
||||
]"
|
||||
:items="applications"
|
||||
:buttons="[
|
||||
{
|
||||
icon: LockOpenIcon,
|
||||
label: 'Reveal Secret',
|
||||
action: openRevealSecretDialog,
|
||||
textColor: 'primary'
|
||||
},
|
||||
{
|
||||
icon: PencilIcon,
|
||||
label: 'Edit',
|
||||
action: openEditApplicationDialog,
|
||||
textColor: 'primary'
|
||||
},
|
||||
{
|
||||
icon: TrashIcon,
|
||||
label: 'Delete',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #id="{ item }">
|
||||
<span class="rounded text-xs font-mono bg-foundation-page p-2">
|
||||
{{ item.id }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #scope="{ item }">
|
||||
<div>
|
||||
{{
|
||||
item.scopes
|
||||
.map(
|
||||
(event, index, array) =>
|
||||
`"${event.name}"${index < array.length - 1 ? ',' : ''}`
|
||||
)
|
||||
.join(' ')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<DeveloperSettingsSectionHeader
|
||||
subheading
|
||||
title="Authorized Apps"
|
||||
:buttons="[
|
||||
{
|
||||
props: {
|
||||
color: 'invert',
|
||||
to: 'https://speckle.guide/dev/apps.html',
|
||||
target: '_blank',
|
||||
external: true,
|
||||
iconLeft: BookOpenIcon
|
||||
},
|
||||
label: 'Open Docs'
|
||||
}
|
||||
]"
|
||||
>
|
||||
Here you can review the apps that you have granted access to. If something
|
||||
looks suspicious, revoke the access.
|
||||
</DeveloperSettingsSectionHeader>
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 ' },
|
||||
{ id: 'author', header: 'Author', classes: 'col-span-3 ' },
|
||||
{ id: 'description', header: 'Description', classes: 'col-span-6 !pt-1.5' }
|
||||
]"
|
||||
:items="authorizedApps"
|
||||
:buttons="[
|
||||
{
|
||||
icon: XMarkIcon,
|
||||
label: 'Revoke Access',
|
||||
action: openDeleteDialog,
|
||||
textColor: 'danger'
|
||||
}
|
||||
]"
|
||||
row-items-align="stretch"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #author="{ item }">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<template v-if="item.author">
|
||||
<UserAvatar :user="item.author" />
|
||||
<span>{{ item.author.name }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HeaderLogoBlock minimal no-link />
|
||||
<span>Speckle</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #description="{ item }">
|
||||
{{ item.description }}
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeveloperSettingsCreateTokenDialog
|
||||
v-model:open="showCreateTokenDialog"
|
||||
@token-created="(token) => handleTokenCreated(token)"
|
||||
/>
|
||||
<DeveloperSettingsDeleteDialog
|
||||
v-model:open="showDeleteDialog"
|
||||
:item="itemToModify"
|
||||
/>
|
||||
<DeveloperSettingsCreateEditApplicationDialog
|
||||
v-model:open="showCreateEditApplicationDialog"
|
||||
:application="(itemToModify as ApplicationItem)"
|
||||
@application-created="handleApplicationCreated"
|
||||
/>
|
||||
<DeveloperSettingsRevealSecretDialog
|
||||
v-model:open="showRevealSecretDialog"
|
||||
:application="itemToModify && 'secret' in itemToModify ? itemToModify : null"
|
||||
/>
|
||||
<DeveloperSettingsCreateTokenSuccessDialog
|
||||
v-model:open="showCreateTokenSuccessDialog"
|
||||
:token="tokenSuccess"
|
||||
/>
|
||||
<DeveloperSettingsCreateApplicationSuccessDialog
|
||||
v-model:open="showCreateApplicationSuccessDialog"
|
||||
:application="(itemToModify as ApplicationItem)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PlusIcon,
|
||||
BookOpenIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
LockOpenIcon,
|
||||
XMarkIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import type {
|
||||
TokenItem,
|
||||
ApplicationItem,
|
||||
AuthorizedAppItem
|
||||
} from '~~/lib/developer-settings/helpers/types'
|
||||
import {
|
||||
developerSettingsAccessTokensQuery,
|
||||
developerSettingsApplicationsQuery,
|
||||
developerSettingsAuthorizedAppsQuery
|
||||
} from '~~/lib/developer-settings/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
useHead({
|
||||
title: 'Developer Settings'
|
||||
})
|
||||
|
||||
const apiOrigin = useApiOrigin()
|
||||
|
||||
const { result: tokensResult, refetch: refetchTokens } = useQuery(
|
||||
developerSettingsAccessTokensQuery
|
||||
)
|
||||
const { result: applicationsResult, refetch: refetchApplications } = useQuery(
|
||||
developerSettingsApplicationsQuery
|
||||
)
|
||||
const { result: authorizedAppsResult } = useQuery(developerSettingsAuthorizedAppsQuery)
|
||||
|
||||
const itemToModify = ref<TokenItem | ApplicationItem | AuthorizedAppItem | null>(null)
|
||||
const tokenSuccess = ref('')
|
||||
const showCreateTokenDialog = ref(false)
|
||||
const showCreateTokenSuccessDialog = ref(false)
|
||||
const showCreateApplicationSuccessDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showCreateEditApplicationDialog = ref(false)
|
||||
const showRevealSecretDialog = ref(false)
|
||||
|
||||
const tokens = computed<TokenItem[]>(() => {
|
||||
return (
|
||||
tokensResult.value?.activeUser?.apiTokens?.filter(
|
||||
(token): token is TokenItem => token !== null
|
||||
) || []
|
||||
)
|
||||
})
|
||||
|
||||
const applications = computed<ApplicationItem[]>(() => {
|
||||
return applicationsResult.value?.activeUser?.createdApps || []
|
||||
})
|
||||
|
||||
const authorizedApps = computed(() =>
|
||||
(authorizedAppsResult.value?.activeUser?.authorizedApps || []).filter(
|
||||
(app) => app.id !== 'spklwebapp'
|
||||
)
|
||||
)
|
||||
|
||||
const openDeleteDialog = (item: TokenItem | ApplicationItem | AuthorizedAppItem) => {
|
||||
itemToModify.value = item
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const openCreateApplicationDialog = () => {
|
||||
itemToModify.value = null
|
||||
showCreateEditApplicationDialog.value = true
|
||||
}
|
||||
|
||||
const openCreateTokenDialog = () => {
|
||||
showCreateTokenDialog.value = true
|
||||
}
|
||||
|
||||
const openEditApplicationDialog = (item: ApplicationItem) => {
|
||||
itemToModify.value = item
|
||||
showCreateEditApplicationDialog.value = true
|
||||
}
|
||||
|
||||
const openRevealSecretDialog = (item: ApplicationItem) => {
|
||||
itemToModify.value = item
|
||||
showRevealSecretDialog.value = true
|
||||
}
|
||||
|
||||
const handleTokenCreated = (token: string) => {
|
||||
refetchTokens()
|
||||
tokenSuccess.value = token
|
||||
showCreateTokenSuccessDialog.value = true
|
||||
}
|
||||
|
||||
const handleApplicationCreated = (applicationId: string) => {
|
||||
refetchApplications()?.then(() => {
|
||||
const newApplication = applications.value.find((app) => app.id === applicationId)
|
||||
|
||||
if (newApplication) {
|
||||
itemToModify.value = newApplication
|
||||
showCreateApplicationSuccessDialog.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToExplorer = () => {
|
||||
if (!import.meta.client) return
|
||||
window.location.href = new URL('/explorer', apiOrigin).toString()
|
||||
}
|
||||
</script>
|
||||
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink to="/server-management" name="Server Management"></HeaderNavLink>
|
||||
<HeaderNavLink
|
||||
to="/server-management/active-users"
|
||||
name="Active Users"
|
||||
></HeaderNavLink>
|
||||
</Portal>
|
||||
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="h4 font-bold">Active Users</h1>
|
||||
<FormButton :icon-left="UserPlusIcon" @click="toggleInviteDialog">
|
||||
Invite
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<FormTextInput
|
||||
size="lg"
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!searchString"
|
||||
placeholder="Search Users"
|
||||
class="rounded-md border border-outline-3"
|
||||
@update:model-value="debounceSearchUpdate"
|
||||
@change="($event) => searchUpdateHandler($event.value)"
|
||||
/>
|
||||
|
||||
<LayoutTable
|
||||
class="mt-8"
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'email', header: 'Email', classes: 'col-span-3 truncate' },
|
||||
{ id: 'emailState', header: 'Email State', classes: 'col-span-2' },
|
||||
{ id: 'company', header: 'Company', classes: 'col-span-2 truncate' },
|
||||
{ id: 'role', header: 'Role', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="users"
|
||||
:buttons="[{ icon: TrashIcon, label: 'Delete', action: openUserDeleteDialog }]"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<UserAvatar v-if="isUser(item)" :user="item" />
|
||||
<span class="truncate">
|
||||
{{ isUser(item) ? item.name : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #email="{ item }">
|
||||
{{ isUser(item) ? item.email : '' }}
|
||||
</template>
|
||||
|
||||
<template #emailState="{ item }">
|
||||
<div class="flex items-center gap-2 select-none">
|
||||
<template v-if="isUser(item) && item.verified">
|
||||
<CheckCircleIcon class="h-4 w-4 text-primary" />
|
||||
<span>Verified</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ExclamationCircleIcon class="h-4 w-4 text-danger" />
|
||||
<span>Not verified</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #company="{ item }">
|
||||
{{ isUser(item) ? item.company : '' }}
|
||||
</template>
|
||||
|
||||
<template #role="{ item }">
|
||||
<FormSelectServerRoles
|
||||
:allow-guest="isGuestMode"
|
||||
allow-admin
|
||||
allow-archived
|
||||
:model-value="isUser(item) ? item.role : undefined"
|
||||
:disabled="isUser(item) && isCurrentUser(item)"
|
||||
fully-control-value
|
||||
@update:model-value="(newRoleValue) => isUser(item) && !isArray(newRoleValue) && newRoleValue && openChangeUserRoleDialog(item, newRoleValue as ServerRoles)"
|
||||
/>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !users?.length" loading />
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="users?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="-mt-24 -mb-24"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
|
||||
<ServerManagementDeleteUserDialog
|
||||
v-model:open="showUserDeleteDialog"
|
||||
:user="userToModify"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
|
||||
<ServerManagementChangeUserRoleDialog
|
||||
v-model:open="showChangeUserRoleDialog"
|
||||
:user="userToModify"
|
||||
:old-role="oldRole"
|
||||
:new-role="newRole"
|
||||
hide-closer
|
||||
/>
|
||||
<ServerManagementInviteDialog v-model:open="showInviteDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { debounce, isArray } from 'lodash-es'
|
||||
import { useLogger } from '~~/composables/logging'
|
||||
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
|
||||
import type { Nullable, ServerRoles, Optional } from '@speckle/shared'
|
||||
import { getUsersQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import type { ItemType, UserItem } from '~~/lib/server-management/helpers/types'
|
||||
import { isUser } from '~~/lib/server-management/helpers/utils'
|
||||
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
ExclamationCircleIcon,
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
UserPlusIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { useServerInfo } from '~~/lib/core/composables/server'
|
||||
|
||||
useHead({
|
||||
title: 'Active Users'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['admin']
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
const { activeUser } = useActiveUser()
|
||||
const { isGuestMode } = useServerInfo()
|
||||
|
||||
const userToModify: Ref<Nullable<UserItem>> = ref(null)
|
||||
const searchString = ref('')
|
||||
const showUserDeleteDialog = ref(false)
|
||||
const showChangeUserRoleDialog = ref(false)
|
||||
const newRole = ref<ServerRoles>()
|
||||
const infiniteLoaderId = ref('')
|
||||
const showInviteDialog = ref(false)
|
||||
|
||||
const queryVariables = computed(() => ({
|
||||
limit: 50,
|
||||
query: searchString.value
|
||||
}))
|
||||
|
||||
const {
|
||||
result: extraPagesResult,
|
||||
fetchMore: fetchMorePages,
|
||||
variables: resultVariables,
|
||||
onResult,
|
||||
loading
|
||||
} = useQuery(getUsersQuery, queryVariables)
|
||||
|
||||
const oldRole = computed(() => userToModify.value?.role as Optional<ServerRoles>)
|
||||
|
||||
const moreToLoad = computed(
|
||||
() =>
|
||||
!extraPagesResult.value?.admin?.userList ||
|
||||
extraPagesResult.value.admin.userList.items.length <
|
||||
extraPagesResult.value.admin.userList.totalCount
|
||||
)
|
||||
|
||||
const users = computed(() => extraPagesResult.value?.admin.userList.items || [])
|
||||
|
||||
const isCurrentUser = (userItem: UserItem) => {
|
||||
return userItem.id === activeUser.value?.id
|
||||
}
|
||||
|
||||
const openUserDeleteDialog = (item: ItemType) => {
|
||||
if (isUser(item)) {
|
||||
userToModify.value = item
|
||||
showUserDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const openChangeUserRoleDialog = (user: UserItem, newRoleValue: ServerRoles) => {
|
||||
if (user.role === newRoleValue) {
|
||||
return
|
||||
}
|
||||
userToModify.value = user
|
||||
newRole.value = newRoleValue
|
||||
showChangeUserRoleDialog.value = true
|
||||
}
|
||||
|
||||
const infiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
const cursor = extraPagesResult.value?.admin?.userList.cursor || null
|
||||
if (!moreToLoad.value || !cursor) return state.complete()
|
||||
|
||||
try {
|
||||
await fetchMorePages({
|
||||
variables: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
state.error()
|
||||
return
|
||||
}
|
||||
|
||||
state.loaded()
|
||||
if (!moreToLoad.value) {
|
||||
state.complete()
|
||||
}
|
||||
}
|
||||
|
||||
const searchUpdateHandler = (value: string) => {
|
||||
searchString.value = value
|
||||
}
|
||||
|
||||
const debounceSearchUpdate = debounce(searchUpdateHandler, 500)
|
||||
|
||||
const calculateLoaderId = () => {
|
||||
infiniteLoaderId.value = resultVariables.value?.query || ''
|
||||
}
|
||||
|
||||
const toggleInviteDialog = () => {
|
||||
showInviteDialog.value = true
|
||||
}
|
||||
|
||||
onResult(calculateLoaderId)
|
||||
</script>
|
||||
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink to="/server-management" name="Server Management"></HeaderNavLink>
|
||||
</Portal>
|
||||
<div
|
||||
class="flex flex-col md:flex-row space-y-2 space-x-2 justify-between mb-4 md:items-center"
|
||||
>
|
||||
<div>
|
||||
<h5 class="h4 font-bold">Your server at a glance</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-10">
|
||||
<ServerManagementCard
|
||||
:server-info="serverData"
|
||||
@cta-clicked="showDialog = true"
|
||||
/>
|
||||
|
||||
<ServerManagementSettingsDialog v-model:open="showDialog" />
|
||||
|
||||
<ServerManagementCard :server-info="userData" />
|
||||
<ServerManagementCard :server-info="projectData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { serverManagementDataQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { CardInfo } from '~~/lib/server-management/helpers/types'
|
||||
import {
|
||||
ServerIcon,
|
||||
UsersIcon,
|
||||
EnvelopeIcon,
|
||||
ChartBarIcon,
|
||||
HomeIcon
|
||||
} from '@heroicons/vue/24/solid'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface GithubRelease {
|
||||
url: string
|
||||
assets_url: string
|
||||
upload_url: string
|
||||
html_url: string
|
||||
id: number
|
||||
node_id: string
|
||||
tag_name: string
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Server Management'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['admin']
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
const router = useRouter()
|
||||
const { result } = useQuery(serverManagementDataQuery)
|
||||
|
||||
const showDialog = ref(false)
|
||||
const latestVersion = ref<string | null>(null)
|
||||
|
||||
const currentVersion = computed(() => result.value?.serverInfo.version)
|
||||
const isLatestVersion = computed(() => {
|
||||
return (
|
||||
!latestVersion.value ||
|
||||
currentVersion.value === latestVersion.value ||
|
||||
currentVersion.value === 'dev' ||
|
||||
currentVersion.value?.includes('alpha') ||
|
||||
currentVersion.value === 'N/A'
|
||||
)
|
||||
})
|
||||
const serverData = computed((): CardInfo[] => [
|
||||
{
|
||||
title: 'Server',
|
||||
value: result.value?.serverInfo.name || 'N/A',
|
||||
icon: ServerIcon,
|
||||
cta: {
|
||||
type: 'button',
|
||||
label: 'Edit',
|
||||
action: () => {
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Speckle Version',
|
||||
value: currentVersion.value || 'N/A',
|
||||
icon: ChartBarIcon,
|
||||
cta: !isLatestVersion.value
|
||||
? {
|
||||
type: 'link',
|
||||
label: 'Update is available',
|
||||
action: openGithubReleasePage
|
||||
}
|
||||
: {
|
||||
type: 'text',
|
||||
label: 'Up-to-date'
|
||||
}
|
||||
}
|
||||
])
|
||||
const userData = computed((): CardInfo[] => [
|
||||
{
|
||||
title: 'Active users',
|
||||
value: result.value?.admin.userList.totalCount?.toString() || 'N/A',
|
||||
icon: UsersIcon,
|
||||
cta: {
|
||||
type: 'button',
|
||||
label: 'Manage',
|
||||
action: () => navigate('/server-management/active-users/')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Pending invitations',
|
||||
value: result.value?.admin.inviteList.totalCount?.toString() || 'N/A',
|
||||
icon: EnvelopeIcon,
|
||||
cta: {
|
||||
type: 'button',
|
||||
label: 'Manage',
|
||||
action: () => navigate('/server-management/pending-invitations/')
|
||||
}
|
||||
}
|
||||
])
|
||||
const projectData = computed((): CardInfo[] => [
|
||||
{
|
||||
title: 'Projects',
|
||||
value: result.value?.admin.projectList.totalCount?.toString() || 'N/A',
|
||||
icon: HomeIcon,
|
||||
cta: {
|
||||
type: 'button',
|
||||
label: 'Manage',
|
||||
action: () => navigate('/server-management/projects/')
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const openGithubReleasePage = () => {
|
||||
window.open('https://github.com/specklesystems/speckle-server/releases', '_blank')
|
||||
}
|
||||
|
||||
const navigate = async (path: string) => {
|
||||
try {
|
||||
await router.push(path)
|
||||
} catch (error) {
|
||||
logger.error('Failed to navigate:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function getLatestVersion(): Promise<string | null> {
|
||||
try {
|
||||
const response: Response = await fetch(
|
||||
'https://api.github.com/repos/specklesystems/speckle-server/releases/latest'
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
} else {
|
||||
const data = (await response.json()) as GithubRelease
|
||||
return data.tag_name
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
latestVersion.value = await getLatestVersion()
|
||||
</script>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink to="/server-management" name="Server Management"></HeaderNavLink>
|
||||
<HeaderNavLink to="/server-management/projects" name="Projects"></HeaderNavLink>
|
||||
</Portal>
|
||||
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="h4 font-bold">Projects</h1>
|
||||
<FormButton :icon-left="PlusIcon" @click="openNewProject = true">New</FormButton>
|
||||
</div>
|
||||
|
||||
<FormTextInput
|
||||
size="lg"
|
||||
name="search"
|
||||
:custom-icon="MagnifyingGlassIcon"
|
||||
color="foundation"
|
||||
full-width
|
||||
search
|
||||
:show-clear="!!searchString"
|
||||
placeholder="Search Projects"
|
||||
class="rounded-md border border-outline-3"
|
||||
@update:model-value="debounceSearchUpdate"
|
||||
@change="($event) => searchUpdateHandler($event.value)"
|
||||
/>
|
||||
|
||||
<LayoutTable
|
||||
class="mt-8"
|
||||
:columns="[
|
||||
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
||||
{ id: 'type', header: 'Type', classes: 'col-span-1' },
|
||||
{ id: 'created', header: 'Created', classes: 'col-span-2' },
|
||||
{ id: 'modified', header: 'Modified', classes: 'col-span-2' },
|
||||
{ id: 'models', header: 'Models', classes: 'col-span-1 text-right' },
|
||||
{ id: 'versions', header: 'Versions', classes: 'col-span-1 text-right' },
|
||||
{ id: 'contributors', header: 'Contributors', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="projects"
|
||||
:buttons="[{ icon: TrashIcon, label: 'Delete', action: openProjectDeleteDialog }]"
|
||||
:on-row-click="handleProjectClick"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
{{ isProject(item) ? item.name : '' }}
|
||||
</template>
|
||||
|
||||
<template #type="{ item }">
|
||||
<span class="capitalize">
|
||||
{{ isProject(item) ? item.visibility.toLowerCase() : '' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #created="{ item }">
|
||||
{{ isProject(item) ? new Date(item.createdAt).toLocaleString('en-GB') : '' }}
|
||||
</template>
|
||||
|
||||
<template #modified="{ item }">
|
||||
{{ isProject(item) ? new Date(item.updatedAt).toLocaleString('en-GB') : '' }}
|
||||
</template>
|
||||
|
||||
<template #models="{ item }">
|
||||
{{ isProject(item) ? item.models.totalCount : '' }}
|
||||
</template>
|
||||
|
||||
<template #versions="{ item }">
|
||||
{{ isProject(item) ? item.versions.totalCount : '' }}
|
||||
</template>
|
||||
|
||||
<template #contributors="{ item }">
|
||||
<div v-if="isProject(item)" class="py-1">
|
||||
<UserAvatarGroup :users="item.team.map((t) => t.user)" :max-count="3" />
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
|
||||
<CommonLoadingBar v-if="loading && !projects?.length" loading />
|
||||
|
||||
<InfiniteLoading
|
||||
v-if="projects?.length"
|
||||
:settings="{ identifier: infiniteLoaderId }"
|
||||
class="py-4"
|
||||
@infinite="infiniteLoad"
|
||||
/>
|
||||
|
||||
<ServerManagementDeleteProjectDialog
|
||||
v-model:open="showProjectDeleteDialog"
|
||||
:project="projectToModify"
|
||||
title="Delete project"
|
||||
:result-variables="resultVariables"
|
||||
/>
|
||||
|
||||
<ProjectsAddDialog v-model:open="openNewProject" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { MagnifyingGlassIcon, TrashIcon, PlusIcon } from '@heroicons/vue/24/outline'
|
||||
import { getProjectsQuery } from '~~/lib/server-management/graphql/queries'
|
||||
import type { ItemType, ProjectItem } from '~~/lib/server-management/helpers/types'
|
||||
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
|
||||
import { isProject } from '~~/lib/server-management/helpers/utils'
|
||||
|
||||
const logger = useLogger()
|
||||
const router = useRouter()
|
||||
|
||||
useHead({
|
||||
title: 'Projects'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['admin']
|
||||
})
|
||||
|
||||
const projectToModify = ref<ProjectItem | null>(null)
|
||||
const searchString = ref('')
|
||||
const showProjectDeleteDialog = ref(false)
|
||||
const infiniteLoaderId = ref('')
|
||||
const openNewProject = ref(false)
|
||||
|
||||
const {
|
||||
result: extraPagesResult,
|
||||
fetchMore: fetchMorePages,
|
||||
variables: resultVariables,
|
||||
onResult,
|
||||
loading
|
||||
} = useQuery(getProjectsQuery, () => ({
|
||||
limit: 50,
|
||||
query: searchString.value
|
||||
}))
|
||||
|
||||
const moreToLoad = computed(
|
||||
() =>
|
||||
!extraPagesResult.value?.admin?.projectList ||
|
||||
extraPagesResult.value.admin.projectList.items.length <
|
||||
extraPagesResult.value.admin.projectList.totalCount
|
||||
)
|
||||
|
||||
const projects = computed(() => extraPagesResult.value?.admin.projectList.items || [])
|
||||
|
||||
const openProjectDeleteDialog = (item: ItemType) => {
|
||||
if (isProject(item)) {
|
||||
projectToModify.value = item
|
||||
showProjectDeleteDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleProjectClick = (item: ItemType) => {
|
||||
router.push(`/projects/${item.id}`)
|
||||
}
|
||||
|
||||
const infiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
const cursor = extraPagesResult.value?.admin?.projectList.cursor || null
|
||||
if (!moreToLoad.value || !cursor) return state.complete()
|
||||
|
||||
try {
|
||||
await fetchMorePages({
|
||||
variables: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
state.error()
|
||||
return
|
||||
}
|
||||
|
||||
state.loaded()
|
||||
if (!moreToLoad.value) {
|
||||
state.complete()
|
||||
}
|
||||
}
|
||||
|
||||
const searchUpdateHandler = (value: string) => {
|
||||
searchString.value = value
|
||||
}
|
||||
|
||||
const debounceSearchUpdate = debounce(searchUpdateHandler, 500)
|
||||
|
||||
const calculateLoaderId = () => {
|
||||
infiniteLoaderId.value = resultVariables.value?.query || ''
|
||||
}
|
||||
|
||||
onResult(calculateLoaderId)
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="rounded-md" :class="[containerClasses, textClasses]">
|
||||
<div class="flex" :class="subcontainerClasses">
|
||||
<div class="flex-shrink-0">
|
||||
<div v-if="!hideIcon" class="flex-shrink-0">
|
||||
<Component :is="icon" :class="iconClasses" aria-hidden="true" />
|
||||
</div>
|
||||
<div :class="mainContentContainerClasses">
|
||||
<h3 class="text-sm" :class="[hasDescription ? 'font-medium' : '']">
|
||||
<h3 v-if="hasTitle" class="text-sm" :class="{ 'font-medium': hasDescription }">
|
||||
<slot name="title">Title</slot>
|
||||
</h3>
|
||||
<div v-if="hasDescription" :class="descriptionWrapperClasses">
|
||||
@@ -75,6 +75,7 @@ const props = withDefaults(
|
||||
externalUrl?: boolean
|
||||
}>
|
||||
customIcon?: PropAnyComponent
|
||||
hideIcon?: boolean
|
||||
size?: Size
|
||||
}>(),
|
||||
{
|
||||
@@ -85,6 +86,7 @@ const props = withDefaults(
|
||||
|
||||
const slots = useSlots()
|
||||
const hasDescription = computed(() => !!slots['description'])
|
||||
const hasTitle = computed(() => !!slots['title'])
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.customIcon) return props.customIcon
|
||||
@@ -117,16 +119,22 @@ const containerClasses = computed(() => {
|
||||
|
||||
switch (props.color) {
|
||||
case 'success':
|
||||
classParts.push('bg-success-lighter border-l-4 border-success')
|
||||
classParts.push(
|
||||
`bg-success-lighter ${!props.hideIcon && 'border-l-4 border-success'}`
|
||||
)
|
||||
break
|
||||
case 'info':
|
||||
classParts.push('bg-info-lighter border-l-4 border-info')
|
||||
classParts.push(`bg-info-lighter ${!props.hideIcon && 'border-l-4 border-info'}`)
|
||||
break
|
||||
case 'danger':
|
||||
classParts.push('bg-danger-lighter border-l-4 border-danger')
|
||||
classParts.push(
|
||||
`bg-danger-lighter ${!props.hideIcon && 'border-l-4 border-danger'}`
|
||||
)
|
||||
break
|
||||
case 'warning':
|
||||
classParts.push('bg-warning-lighter border-l-4 border-warning')
|
||||
classParts.push(
|
||||
`bg-warning-lighter ${!props.hideIcon && 'border-l-4 border-warning'}`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -185,10 +193,14 @@ const descriptionWrapperClasses = computed(() => {
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
classParts.push('mt-1 sm:mt-2 text-xs sm:text-sm')
|
||||
classParts.push('text-xs sm:text-sm')
|
||||
break
|
||||
}
|
||||
|
||||
if (hasTitle.value && props.size !== 'xs') {
|
||||
classParts.push('mt-1 sm:mt-2')
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
})
|
||||
|
||||
|
||||
@@ -6,42 +6,54 @@
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave="ease-in duration-400"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-neutral-100/70 dark:bg-neutral-900/70 transition-opacity backdrop-blur-xs"
|
||||
class="fixed top-0 left-0 w-full h-full bg-black/70 dark:bg-neutral-900/70 transition-opacity"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<div class="fixed top-0 left-0 z-10 h-screen !h-[100dvh] w-screen">
|
||||
<div class="flex justify-center items-center h-full w-full p-4 sm:p-0">
|
||||
<div
|
||||
class="flex md:justify-center items-end md:items-center h-full w-full md:p-6"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter="ease-out duration-5000"
|
||||
enter-from="md:opacity-0 translate-y-[100%] md:translate-y-4"
|
||||
enter-to="md:opacity-100 translate-y-0"
|
||||
leave="ease-in duration-5000"
|
||||
leave-from="md:opacity-100 translate-y-0"
|
||||
leave-to="md:opacity-0 translate-y-[100%] md:translate-y-4"
|
||||
@after-leave="$emit('fully-closed')"
|
||||
>
|
||||
<DialogPanel
|
||||
:class="[
|
||||
'transform rounded-lg text-foreground overflow-hidden bg-foundation text-left shadow-xl transition-all flex flex-col max-h-[90vh]',
|
||||
'dialog-panel transform rounded-t-lg md:rounded-xl text-foreground overflow-hidden transition-all bg-foundation text-left shadow-xl flex flex-col md:h-auto',
|
||||
fullscreen ? 'md:h-full' : 'md:max-h-[90vh]',
|
||||
widthClasses
|
||||
]"
|
||||
:as="isForm ? 'form' : 'div'"
|
||||
@submit.prevent="onFormSubmit"
|
||||
>
|
||||
<div :class="scrolledFromTop && 'relative z-20 shadow-lg'">
|
||||
<div
|
||||
v-if="hasTitle"
|
||||
:class="scrolledFromTop && 'relative z-20 shadow-lg'"
|
||||
>
|
||||
<div
|
||||
v-if="hasTitle"
|
||||
class="flex items-center justify-start rounded-t-lg shrink-0 min-h-[2rem] sm:min-h-[4rem] p-4 sm:p-6 truncate text-lg sm:text-2xl font-bold"
|
||||
class="flex items-center justify-start rounded-t-lg shrink-0 min-h-[2rem] sm:min-h-[4rem] p-6 truncate text-lg sm:text-2xl font-bold"
|
||||
>
|
||||
<div class="w-full truncate pr-12">
|
||||
{{ title }}
|
||||
<slot name="header"></slot>
|
||||
<div class="flex items-center pr-12">
|
||||
<ChevronLeftIcon
|
||||
v-if="showBackButton"
|
||||
class="w-5 h-5 -ml-1 mr-3"
|
||||
@click="$emit('back')"
|
||||
/>
|
||||
<div class="w-full truncate">
|
||||
{{ title }}
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,23 +68,26 @@
|
||||
<button
|
||||
v-if="!hideCloser"
|
||||
type="button"
|
||||
class="absolute z-20 bg-foundation rounded-full p-1"
|
||||
:class="hasTitle ? 'top-2 right-3 sm:top-4' : 'right-4 top-3'"
|
||||
class="absolute z-20 bg-foundation hover:bg-foundation-page transition rounded-full p-1.5 shadow border top-5 right-5 border-outline-3"
|
||||
@click="open = false"
|
||||
>
|
||||
<XMarkIcon class="h-5 sm:h-6 w-5 sm:w-6" />
|
||||
<XMarkIcon class="h-4 w-4 md:w-5 md:h-5" />
|
||||
</button>
|
||||
<div
|
||||
ref="slotContainer"
|
||||
class="flex-1 simple-scrollbar overflow-y-auto text-sm sm:text-base"
|
||||
:class="hasTitle ? 'px-4 pb-4 sm:px-6' : 'p-4 sm:p-6'"
|
||||
:class="
|
||||
hasTitle
|
||||
? `px-6 pb-4 ${fullscreen && 'md:p-0'}`
|
||||
: !fullscreen && 'p-6'
|
||||
"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<slot>Put your content here!</slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasButtons"
|
||||
class="relative z-50 flex p-4 sm:p-6 gap-3 shrink-0 bg-foundation"
|
||||
class="relative z-50 flex p-6 gap-3 shrink-0 bg-foundation"
|
||||
:class="{
|
||||
'shadow-t': !scrolledToBottom,
|
||||
[buttonsWrapperClasses || '']: true
|
||||
@@ -104,7 +119,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
|
||||
import { FormButton, type LayoutDialogButton } from '~~/src/lib'
|
||||
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||
import { XMarkIcon, ChevronLeftIcon } from '@heroicons/vue/24/outline'
|
||||
import { useResizeObserver, type ResizeObserverCallback } from '@vueuse/core'
|
||||
import { computed, ref, useSlots, watch, onUnmounted } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
@@ -115,12 +130,15 @@ type MaxWidthValue = 'sm' | 'md' | 'lg' | 'xl'
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', v: boolean): void
|
||||
(e: 'fully-closed'): void
|
||||
(e: 'back'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
maxWidth?: MaxWidthValue
|
||||
fullscreen?: boolean
|
||||
hideCloser?: boolean
|
||||
showBackButton?: boolean
|
||||
/**
|
||||
* Prevent modal from closing when the user clicks outside of the modal or presses Esc
|
||||
*/
|
||||
@@ -154,7 +172,7 @@ useResizeObserver(
|
||||
|
||||
const isForm = computed(() => !!props.onSubmit)
|
||||
const hasButtons = computed(() => props.buttons || slots.buttons)
|
||||
const hasTitle = computed(() => props.title || slots.header)
|
||||
const hasTitle = computed(() => !!props.title || !!slots.header)
|
||||
|
||||
const open = computed({
|
||||
get: () => props.open,
|
||||
@@ -177,19 +195,20 @@ const maxWidthWeight = computed(() => {
|
||||
})
|
||||
|
||||
const widthClasses = computed(() => {
|
||||
const classParts: string[] = ['w-full', 'sm:w-full sm:max-w-2xl']
|
||||
const classParts: string[] = ['w-full', 'sm:w-full']
|
||||
|
||||
if (maxWidthWeight.value >= 1) {
|
||||
if (!props.fullscreen) {
|
||||
classParts.push('md:max-w-2xl')
|
||||
}
|
||||
if (maxWidthWeight.value >= 2) {
|
||||
classParts.push('lg:max-w-4xl')
|
||||
}
|
||||
if (maxWidthWeight.value >= 3) {
|
||||
classParts.push('xl:max-w-6xl')
|
||||
}
|
||||
if (maxWidthWeight.value >= 4) {
|
||||
classParts.push('2xl:max-w-7xl')
|
||||
|
||||
if (maxWidthWeight.value >= 2) {
|
||||
classParts.push('lg:max-w-4xl')
|
||||
}
|
||||
if (maxWidthWeight.value >= 3) {
|
||||
classParts.push('xl:max-w-6xl')
|
||||
}
|
||||
if (maxWidthWeight.value >= 4) {
|
||||
classParts.push('2xl:max-w-7xl')
|
||||
}
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
@@ -241,4 +260,9 @@ html.dialog-open {
|
||||
html.dialog-open body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/* Workaround because in Tailwind vh gets added after dvh */
|
||||
.dialog-panel {
|
||||
height: 98vh;
|
||||
height: 98dvh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
v-for="(column, colIndex) in columns"
|
||||
:key="column.id"
|
||||
:class="getHeaderClasses(column.id, colIndex)"
|
||||
class="capitalize"
|
||||
>
|
||||
{{ column.header }}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{{ title }}
|
||||
</h6>
|
||||
</button>
|
||||
<div v-else class="flex gap-1 items-center w-full p-0.5 text-foreground-2">
|
||||
<div v-else class="flex gap-1 items-center w-full p-1 text-foreground-2">
|
||||
<div
|
||||
v-if="$slots['title-icon']"
|
||||
class="h-5 w-5 flex items-center justify-center"
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLink
|
||||
v-if="!hasChildren"
|
||||
:to="to"
|
||||
class="group flex items-center justify-between gap-2 shrink-0 text-sm select-none rounded-md w-full hover:bg-primary-muted p-1.5 cursor-pointer"
|
||||
active-class="bg-foundation-focus hover:!bg-foundation-focus"
|
||||
:external="external"
|
||||
:target="external ? '_blank' : undefined"
|
||||
<component
|
||||
:is="linkComponent"
|
||||
v-if="!hasChildren"
|
||||
:to="to"
|
||||
class="group flex items-center justify-between gap-2 shrink-0 text-sm select-none rounded-md w-full hover:bg-primary-muted py-1.5 px-5 cursor-pointer"
|
||||
exact-active-class="bg-foundation-focus hover:!bg-foundation-focus"
|
||||
:external="external"
|
||||
:target="external ? '_blank' : undefined"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="$slots.icon" class="h-5 w-5 flex items-center justify-center">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<span :class="$slots.icon ? '' : 'pl-2'">
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="tag"
|
||||
class="text-xs uppercase bg-primary-muted py-0.5 px-2 rounded-full font-medium text-primary-focus group-hover:bg-white"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="$slots.icon" class="h-5 w-5 flex items-center justify-center">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<span :class="$slots.icon ? '' : 'pl-2'">
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="tag"
|
||||
class="text-xs uppercase bg-primary-muted py-0.5 px-2 rounded-full font-medium text-primary-focus group-hover:bg-white"
|
||||
>
|
||||
{{ tag }}
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div v-else class="flex flex-col">
|
||||
<button
|
||||
class="group flex gap-1.5 items-center w-full hover:bg-foundation-3 rounded-md p-0.5 cursor-pointer"
|
||||
@click="isOpen = !isOpen"
|
||||
>
|
||||
<ChevronDownIcon :class="isOpen ? '' : 'rotate-180'" class="h-2.5 w-2.5" />
|
||||
<h6 class="font-bold text-foreground-2 text-xs flex items-center gap-1.5">
|
||||
{{ label }}
|
||||
</h6>
|
||||
</button>
|
||||
<div v-show="isOpen" class="pl-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
{{ tag }}
|
||||
</div>
|
||||
</component>
|
||||
<div v-else class="flex flex-col">
|
||||
<button
|
||||
class="group flex gap-1.5 items-center w-full hover:bg-foundation-3 rounded-md p-0.5 cursor-pointer"
|
||||
@click="isOpen = !isOpen"
|
||||
>
|
||||
<ChevronDownIcon :class="isOpen ? '' : 'rotate-180'" class="h-2.5 w-2.5" />
|
||||
<h6 class="font-bold text-foreground-2 text-xs flex items-center gap-1.5">
|
||||
{{ label }}
|
||||
</h6>
|
||||
</button>
|
||||
<div v-show="isOpen" class="pl-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
||||
import { ref, resolveDynamicComponent, useSlots } from 'vue'
|
||||
import { ref, computed, resolveDynamicComponent, useSlots } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
to: string
|
||||
to?: string
|
||||
tag?: string
|
||||
external?: boolean
|
||||
}>()
|
||||
@@ -55,6 +54,8 @@ const isOpen = ref(true)
|
||||
|
||||
const NuxtLink = resolveDynamicComponent('NuxtLink')
|
||||
|
||||
const linkComponent = computed(() => (props.to ? NuxtLink : 'a'))
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const hasChildren = !!slots.default
|
||||
|
||||
@@ -19,7 +19,7 @@ export function markClassesUsed(classes: string[]) {
|
||||
*/
|
||||
export enum TailwindBreakpoints {
|
||||
sm = 640,
|
||||
md = 746,
|
||||
md = 768,
|
||||
lg = 1024,
|
||||
xl = 1280,
|
||||
'2xl' = 1536
|
||||
|
||||
Reference in New Issue
Block a user