Compare commits

..

5 Commits

46 changed files with 2404 additions and 2886 deletions

View File

@@ -321,28 +321,25 @@ export class InfraConfigService implements OnModuleInit {
* Reset all the InfraConfigs to their default values (from .env)
*/
async reset() {
// These are all the infra-configs that should not be reset
const RESET_EXCLUSION_LIST = [
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
];
try {
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(p) => RESET_EXCLUSION_LIST.includes(p.name) === false,
);
await this.prisma.infraConfig.deleteMany({
where: {
name: {
in: updatedInfraConfigDefaultObjs.map((p) => p.name),
},
},
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
});
// Hardcode t
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
);
await this.prisma.infraConfig.createMany({
data: updatedInfraConfigDefaultObjs,
data: [
...updatedInfraConfigDefaultObjs,
{
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
value: 'true',
},
],
});
stopApp();

View File

@@ -1125,7 +1125,7 @@ export class TeamCollectionService {
id: searchResults[i].id,
path: !fetchedParentTree
? []
: ([fetchedParentTree.right] as CollectionSearchNode[]),
: (fetchedParentTree.right as CollectionSearchNode[]),
});
}
@@ -1151,8 +1151,8 @@ export class TeamCollectionService {
select id,title,'collection' AS type
from "TeamCollection"
where "TeamCollection"."teamID"=${teamID}
and titlesearch @@ to_tsquery(${searchQuery})
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
and titlesearch @@ plainto_tsquery(${searchQuery})
order by ts_rank(titlesearch,plainto_tsquery(${searchQuery}))
limit ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
@@ -1183,8 +1183,8 @@ export class TeamCollectionService {
select id,title,request->>'method' as method,'request' AS type
from "TeamRequest"
where "TeamRequest"."teamID"=${teamID}
and titlesearch @@ to_tsquery(${searchQuery})
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
and titlesearch @@ plainto_tsquery(${searchQuery})
order by ts_rank(titlesearch,plainto_tsquery(${searchQuery}))
limit ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
@@ -1250,45 +1250,53 @@ export class TeamCollectionService {
* @returns The parent tree of the parent collections
*/
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
function findChildren(id) {
function findChildren(id: string): CollectionSearchNode[] {
const collection = parentCollections.filter((item) => item.id === id)[0];
if (collection.parentID == null) {
return {
id: collection.id,
title: collection.title,
type: 'collection',
path: [],
};
return <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: [],
},
];
}
const res = {
id: collection.id,
title: collection.title,
type: 'collection',
path: findChildren(collection.parentID),
};
const res = <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: findChildren(collection.parentID),
},
];
return res;
}
if (parentCollections.length > 0) {
if (parentCollections[0].parentID == null) {
return {
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: [],
},
];
}
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: [],
};
}
return {
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: findChildren(parentCollections[0].parentID),
};
path: findChildren(parentCollections[0].parentID),
},
];
}
return null;
return <CollectionSearchNode[]>[];
}
/**

View File

@@ -988,6 +988,7 @@
"workspace": {
"change": "Change workspace",
"personal": "Personal Workspace",
"personal_workspace": "{name}'s Workspace",
"other_workspaces": "My Workspaces",
"team": "Workspace",
"title": "Workspaces"

View File

@@ -1,214 +1,214 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import "@vue/runtime-core"
export {}
declare module "@vue/runtime-core" {
declare module 'vue' {
export interface GlobalComponents {
AppActionHandler: (typeof import("./components/app/ActionHandler.vue"))["default"]
AppBanner: (typeof import("./components/app/Banner.vue"))["default"]
AppContextMenu: (typeof import("./components/app/ContextMenu.vue"))["default"]
AppDeveloperOptions: (typeof import("./components/app/DeveloperOptions.vue"))["default"]
AppFooter: (typeof import("./components/app/Footer.vue"))["default"]
AppGitHubStarButton: (typeof import("./components/app/GitHubStarButton.vue"))["default"]
AppHeader: (typeof import("./components/app/Header.vue"))["default"]
AppInspection: (typeof import("./components/app/Inspection.vue"))["default"]
AppInterceptor: (typeof import("./components/app/Interceptor.vue"))["default"]
AppLogo: (typeof import("./components/app/Logo.vue"))["default"]
AppOptions: (typeof import("./components/app/Options.vue"))["default"]
AppPaneLayout: (typeof import("./components/app/PaneLayout.vue"))["default"]
AppShare: (typeof import("./components/app/Share.vue"))["default"]
AppShortcuts: (typeof import("./components/app/Shortcuts.vue"))["default"]
AppShortcutsEntry: (typeof import("./components/app/ShortcutsEntry.vue"))["default"]
AppShortcutsPrompt: (typeof import("./components/app/ShortcutsPrompt.vue"))["default"]
AppSidenav: (typeof import("./components/app/Sidenav.vue"))["default"]
AppSpotlight: (typeof import("./components/app/spotlight/index.vue"))["default"]
AppSpotlightEntry: (typeof import("./components/app/spotlight/Entry.vue"))["default"]
AppSpotlightEntryGQLHistory: (typeof import("./components/app/spotlight/entry/GQLHistory.vue"))["default"]
AppSpotlightEntryGQLRequest: (typeof import("./components/app/spotlight/entry/GQLRequest.vue"))["default"]
AppSpotlightEntryIconSelected: (typeof import("./components/app/spotlight/entry/IconSelected.vue"))["default"]
AppSpotlightEntryRESTHistory: (typeof import("./components/app/spotlight/entry/RESTHistory.vue"))["default"]
AppSpotlightEntryRESTRequest: (typeof import("./components/app/spotlight/entry/RESTRequest.vue"))["default"]
AppSupport: (typeof import("./components/app/Support.vue"))["default"]
Collections: (typeof import("./components/collections/index.vue"))["default"]
CollectionsAdd: (typeof import("./components/collections/Add.vue"))["default"]
CollectionsAddFolder: (typeof import("./components/collections/AddFolder.vue"))["default"]
CollectionsAddRequest: (typeof import("./components/collections/AddRequest.vue"))["default"]
CollectionsCollection: (typeof import("./components/collections/Collection.vue"))["default"]
CollectionsEdit: (typeof import("./components/collections/Edit.vue"))["default"]
CollectionsEditFolder: (typeof import("./components/collections/EditFolder.vue"))["default"]
CollectionsEditRequest: (typeof import("./components/collections/EditRequest.vue"))["default"]
CollectionsGraphql: (typeof import("./components/collections/graphql/index.vue"))["default"]
CollectionsGraphqlAdd: (typeof import("./components/collections/graphql/Add.vue"))["default"]
CollectionsGraphqlAddFolder: (typeof import("./components/collections/graphql/AddFolder.vue"))["default"]
CollectionsGraphqlAddRequest: (typeof import("./components/collections/graphql/AddRequest.vue"))["default"]
CollectionsGraphqlCollection: (typeof import("./components/collections/graphql/Collection.vue"))["default"]
CollectionsGraphqlEdit: (typeof import("./components/collections/graphql/Edit.vue"))["default"]
CollectionsGraphqlEditFolder: (typeof import("./components/collections/graphql/EditFolder.vue"))["default"]
CollectionsGraphqlEditRequest: (typeof import("./components/collections/graphql/EditRequest.vue"))["default"]
CollectionsGraphqlFolder: (typeof import("./components/collections/graphql/Folder.vue"))["default"]
CollectionsGraphqlImportExport: (typeof import("./components/collections/graphql/ImportExport.vue"))["default"]
CollectionsGraphqlRequest: (typeof import("./components/collections/graphql/Request.vue"))["default"]
CollectionsImportExport: (typeof import("./components/collections/ImportExport.vue"))["default"]
CollectionsMyCollections: (typeof import("./components/collections/MyCollections.vue"))["default"]
CollectionsProperties: (typeof import("./components/collections/Properties.vue"))["default"]
CollectionsRequest: (typeof import("./components/collections/Request.vue"))["default"]
CollectionsSaveRequest: (typeof import("./components/collections/SaveRequest.vue"))["default"]
CollectionsTeamCollections: (typeof import("./components/collections/TeamCollections.vue"))["default"]
CookiesAllModal: (typeof import("./components/cookies/AllModal.vue"))["default"]
CookiesEditCookie: (typeof import("./components/cookies/EditCookie.vue"))["default"]
Embeds: (typeof import("./components/embeds/index.vue"))["default"]
Environments: (typeof import("./components/environments/index.vue"))["default"]
EnvironmentsAdd: (typeof import("./components/environments/Add.vue"))["default"]
EnvironmentsImportExport: (typeof import("./components/environments/ImportExport.vue"))["default"]
EnvironmentsMy: (typeof import("./components/environments/my/index.vue"))["default"]
EnvironmentsMyDetails: (typeof import("./components/environments/my/Details.vue"))["default"]
EnvironmentsMyEnvironment: (typeof import("./components/environments/my/Environment.vue"))["default"]
EnvironmentsSelector: (typeof import("./components/environments/Selector.vue"))["default"]
EnvironmentsTeams: (typeof import("./components/environments/teams/index.vue"))["default"]
EnvironmentsTeamsDetails: (typeof import("./components/environments/teams/Details.vue"))["default"]
EnvironmentsTeamsEnvironment: (typeof import("./components/environments/teams/Environment.vue"))["default"]
FirebaseLogin: (typeof import("./components/firebase/Login.vue"))["default"]
FirebaseLogout: (typeof import("./components/firebase/Logout.vue"))["default"]
GraphqlAuthorization: (typeof import("./components/graphql/Authorization.vue"))["default"]
GraphqlField: (typeof import("./components/graphql/Field.vue"))["default"]
GraphqlHeaders: (typeof import("./components/graphql/Headers.vue"))["default"]
GraphqlQuery: (typeof import("./components/graphql/Query.vue"))["default"]
GraphqlRequest: (typeof import("./components/graphql/Request.vue"))["default"]
GraphqlRequestOptions: (typeof import("./components/graphql/RequestOptions.vue"))["default"]
GraphqlRequestTab: (typeof import("./components/graphql/RequestTab.vue"))["default"]
GraphqlResponse: (typeof import("./components/graphql/Response.vue"))["default"]
GraphqlSidebar: (typeof import("./components/graphql/Sidebar.vue"))["default"]
GraphqlSubscriptionLog: (typeof import("./components/graphql/SubscriptionLog.vue"))["default"]
GraphqlTabHead: (typeof import("./components/graphql/TabHead.vue"))["default"]
GraphqlType: (typeof import("./components/graphql/Type.vue"))["default"]
GraphqlTypeLink: (typeof import("./components/graphql/TypeLink.vue"))["default"]
GraphqlVariable: (typeof import("./components/graphql/Variable.vue"))["default"]
History: (typeof import("./components/history/index.vue"))["default"]
HistoryGraphqlCard: (typeof import("./components/history/graphql/Card.vue"))["default"]
HistoryRestCard: (typeof import("./components/history/rest/Card.vue"))["default"]
HoppButtonPrimary: (typeof import("@hoppscotch/ui"))["HoppButtonPrimary"]
HoppButtonSecondary: (typeof import("@hoppscotch/ui"))["HoppButtonSecondary"]
HoppSmartAnchor: (typeof import("@hoppscotch/ui"))["HoppSmartAnchor"]
HoppSmartCheckbox: (typeof import("@hoppscotch/ui"))["HoppSmartCheckbox"]
HoppSmartConfirmModal: (typeof import("@hoppscotch/ui"))["HoppSmartConfirmModal"]
HoppSmartExpand: (typeof import("@hoppscotch/ui"))["HoppSmartExpand"]
HoppSmartFileChip: (typeof import("@hoppscotch/ui"))["HoppSmartFileChip"]
HoppSmartInput: (typeof import("@hoppscotch/ui"))["HoppSmartInput"]
HoppSmartIntersection: (typeof import("@hoppscotch/ui"))["HoppSmartIntersection"]
HoppSmartItem: (typeof import("@hoppscotch/ui"))["HoppSmartItem"]
HoppSmartLink: (typeof import("@hoppscotch/ui"))["HoppSmartLink"]
HoppSmartModal: (typeof import("@hoppscotch/ui"))["HoppSmartModal"]
HoppSmartPicture: (typeof import("@hoppscotch/ui"))["HoppSmartPicture"]
HoppSmartPlaceholder: (typeof import("@hoppscotch/ui"))["HoppSmartPlaceholder"]
HoppSmartProgressRing: (typeof import("@hoppscotch/ui"))["HoppSmartProgressRing"]
HoppSmartRadio: (typeof import("@hoppscotch/ui"))["HoppSmartRadio"]
HoppSmartRadioGroup: (typeof import("@hoppscotch/ui"))["HoppSmartRadioGroup"]
HoppSmartSelectWrapper: (typeof import("@hoppscotch/ui"))["HoppSmartSelectWrapper"]
HoppSmartSlideOver: (typeof import("@hoppscotch/ui"))["HoppSmartSlideOver"]
HoppSmartSpinner: (typeof import("@hoppscotch/ui"))["HoppSmartSpinner"]
HoppSmartTab: (typeof import("@hoppscotch/ui"))["HoppSmartTab"]
HoppSmartTabs: (typeof import("@hoppscotch/ui"))["HoppSmartTabs"]
HoppSmartToggle: (typeof import("@hoppscotch/ui"))["HoppSmartToggle"]
HoppSmartTree: (typeof import("@hoppscotch/ui"))["HoppSmartTree"]
HoppSmartWindow: (typeof import("@hoppscotch/ui"))["HoppSmartWindow"]
HoppSmartWindows: (typeof import("@hoppscotch/ui"))["HoppSmartWindows"]
HttpAuthorization: (typeof import("./components/http/Authorization.vue"))["default"]
HttpAuthorizationApiKey: (typeof import("./components/http/authorization/ApiKey.vue"))["default"]
HttpAuthorizationBasic: (typeof import("./components/http/authorization/Basic.vue"))["default"]
HttpBody: (typeof import("./components/http/Body.vue"))["default"]
HttpBodyParameters: (typeof import("./components/http/BodyParameters.vue"))["default"]
HttpCodegenModal: (typeof import("./components/http/CodegenModal.vue"))["default"]
HttpHeaders: (typeof import("./components/http/Headers.vue"))["default"]
HttpImportCurl: (typeof import("./components/http/ImportCurl.vue"))["default"]
HttpOAuth2Authorization: (typeof import("./components/http/OAuth2Authorization.vue"))["default"]
HttpParameters: (typeof import("./components/http/Parameters.vue"))["default"]
HttpPreRequestScript: (typeof import("./components/http/PreRequestScript.vue"))["default"]
HttpRawBody: (typeof import("./components/http/RawBody.vue"))["default"]
HttpReqChangeConfirmModal: (typeof import("./components/http/ReqChangeConfirmModal.vue"))["default"]
HttpRequest: (typeof import("./components/http/Request.vue"))["default"]
HttpRequestOptions: (typeof import("./components/http/RequestOptions.vue"))["default"]
HttpRequestTab: (typeof import("./components/http/RequestTab.vue"))["default"]
HttpResponse: (typeof import("./components/http/Response.vue"))["default"]
HttpResponseMeta: (typeof import("./components/http/ResponseMeta.vue"))["default"]
HttpSidebar: (typeof import("./components/http/Sidebar.vue"))["default"]
HttpTabHead: (typeof import("./components/http/TabHead.vue"))["default"]
HttpTestResult: (typeof import("./components/http/TestResult.vue"))["default"]
HttpTestResultEntry: (typeof import("./components/http/TestResultEntry.vue"))["default"]
HttpTestResultEnv: (typeof import("./components/http/TestResultEnv.vue"))["default"]
HttpTestResultReport: (typeof import("./components/http/TestResultReport.vue"))["default"]
HttpTests: (typeof import("./components/http/Tests.vue"))["default"]
HttpURLEncodedParams: (typeof import("./components/http/URLEncodedParams.vue"))["default"]
IconLucideActivity: (typeof import("~icons/lucide/activity"))["default"]
IconLucideAlertTriangle: (typeof import("~icons/lucide/alert-triangle"))["default"]
IconLucideArrowLeft: (typeof import("~icons/lucide/arrow-left"))["default"]
IconLucideArrowUpRight: (typeof import("~icons/lucide/arrow-up-right"))["default"]
IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"]
IconLucideCheckCircle: (typeof import("~icons/lucide/check-circle"))["default"]
IconLucideChevronRight: (typeof import("~icons/lucide/chevron-right"))["default"]
IconLucideGlobe: (typeof import("~icons/lucide/globe"))["default"]
IconLucideHelpCircle: (typeof import("~icons/lucide/help-circle"))["default"]
IconLucideInbox: (typeof import("~icons/lucide/inbox"))["default"]
IconLucideInfo: (typeof import("~icons/lucide/info"))["default"]
IconLucideLayers: (typeof import("~icons/lucide/layers"))["default"]
IconLucideListEnd: (typeof import("~icons/lucide/list-end"))["default"]
IconLucideMinus: (typeof import("~icons/lucide/minus"))["default"]
IconLucideRss: (typeof import("~icons/lucide/rss"))["default"]
IconLucideSearch: (typeof import("~icons/lucide/search"))["default"]
IconLucideUsers: (typeof import("~icons/lucide/users"))["default"]
IconLucideX: (typeof import("~icons/lucide/x"))["default"]
ImportExportBase: (typeof import("./components/importExport/Base.vue"))["default"]
ImportExportImportExportList: (typeof import("./components/importExport/ImportExportList.vue"))["default"]
ImportExportImportExportSourcesList: (typeof import("./components/importExport/ImportExportSourcesList.vue"))["default"]
ImportExportImportExportStepsFileImport: (typeof import("./components/importExport/ImportExportSteps/FileImport.vue"))["default"]
ImportExportImportExportStepsMyCollectionImport: (typeof import("./components/importExport/ImportExportSteps/MyCollectionImport.vue"))["default"]
ImportExportImportExportStepsUrlImport: (typeof import("./components/importExport/ImportExportSteps/UrlImport.vue"))["default"]
InterceptorsErrorPlaceholder: (typeof import("./components/interceptors/ErrorPlaceholder.vue"))["default"]
InterceptorsExtensionSubtitle: (typeof import("./components/interceptors/ExtensionSubtitle.vue"))["default"]
LensesHeadersRenderer: (typeof import("./components/lenses/HeadersRenderer.vue"))["default"]
LensesHeadersRendererEntry: (typeof import("./components/lenses/HeadersRendererEntry.vue"))["default"]
LensesRenderersAudioLensRenderer: (typeof import("./components/lenses/renderers/AudioLensRenderer.vue"))["default"]
LensesRenderersHTMLLensRenderer: (typeof import("./components/lenses/renderers/HTMLLensRenderer.vue"))["default"]
LensesRenderersImageLensRenderer: (typeof import("./components/lenses/renderers/ImageLensRenderer.vue"))["default"]
LensesRenderersJSONLensRenderer: (typeof import("./components/lenses/renderers/JSONLensRenderer.vue"))["default"]
LensesRenderersPDFLensRenderer: (typeof import("./components/lenses/renderers/PDFLensRenderer.vue"))["default"]
LensesRenderersRawLensRenderer: (typeof import("./components/lenses/renderers/RawLensRenderer.vue"))["default"]
LensesRenderersVideoLensRenderer: (typeof import("./components/lenses/renderers/VideoLensRenderer.vue"))["default"]
LensesRenderersXMLLensRenderer: (typeof import("./components/lenses/renderers/XMLLensRenderer.vue"))["default"]
LensesResponseBodyRenderer: (typeof import("./components/lenses/ResponseBodyRenderer.vue"))["default"]
ProfileUserDelete: (typeof import("./components/profile/UserDelete.vue"))["default"]
RealtimeCommunication: (typeof import("./components/realtime/Communication.vue"))["default"]
RealtimeConnectionConfig: (typeof import("./components/realtime/ConnectionConfig.vue"))["default"]
RealtimeLog: (typeof import("./components/realtime/Log.vue"))["default"]
RealtimeLogEntry: (typeof import("./components/realtime/LogEntry.vue"))["default"]
RealtimeSubscription: (typeof import("./components/realtime/Subscription.vue"))["default"]
SettingsExtension: (typeof import("./components/settings/Extension.vue"))["default"]
SettingsProxy: (typeof import("./components/settings/Proxy.vue"))["default"]
Share: (typeof import("./components/share/index.vue"))["default"]
ShareCreateModal: (typeof import("./components/share/CreateModal.vue"))["default"]
ShareCustomizeModal: (typeof import("./components/share/CustomizeModal.vue"))["default"]
ShareModal: (typeof import("./components/share/Modal.vue"))["default"]
ShareRequest: (typeof import("./components/share/Request.vue"))["default"]
ShareTemplatesButton: (typeof import("./components/share/templates/Button.vue"))["default"]
ShareTemplatesEmbeds: (typeof import("./components/share/templates/Embeds.vue"))["default"]
ShareTemplatesLink: (typeof import("./components/share/templates/Link.vue"))["default"]
SmartAccentModePicker: (typeof import("./components/smart/AccentModePicker.vue"))["default"]
SmartChangeLanguage: (typeof import("./components/smart/ChangeLanguage.vue"))["default"]
SmartColorModePicker: (typeof import("./components/smart/ColorModePicker.vue"))["default"]
SmartEnvInput: (typeof import("./components/smart/EnvInput.vue"))["default"]
TabPrimary: (typeof import("./components/tab/Primary.vue"))["default"]
TabSecondary: (typeof import("./components/tab/Secondary.vue"))["default"]
Teams: (typeof import("./components/teams/index.vue"))["default"]
TeamsAdd: (typeof import("./components/teams/Add.vue"))["default"]
TeamsEdit: (typeof import("./components/teams/Edit.vue"))["default"]
TeamsInvite: (typeof import("./components/teams/Invite.vue"))["default"]
TeamsMemberStack: (typeof import("./components/teams/MemberStack.vue"))["default"]
TeamsModal: (typeof import("./components/teams/Modal.vue"))["default"]
TeamsTeam: (typeof import("./components/teams/Team.vue"))["default"]
Tippy: (typeof import("vue-tippy"))["Tippy"]
WorkspaceCurrent: (typeof import("./components/workspace/Current.vue"))["default"]
WorkspaceSelector: (typeof import("./components/workspace/Selector.vue"))["default"]
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppBanner: typeof import('./components/app/Banner.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: typeof import('./components/app/Inspection.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default']
Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
CollectionsGraphqlAddRequest: typeof import('./components/collections/graphql/AddRequest.vue')['default']
CollectionsGraphqlCollection: typeof import('./components/collections/graphql/Collection.vue')['default']
CollectionsGraphqlEdit: typeof import('./components/collections/graphql/Edit.vue')['default']
CollectionsGraphqlEditFolder: typeof import('./components/collections/graphql/EditFolder.vue')['default']
CollectionsGraphqlEditRequest: typeof import('./components/collections/graphql/EditRequest.vue')['default']
CollectionsGraphqlFolder: typeof import('./components/collections/graphql/Folder.vue')['default']
CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
FirebaseLogin: typeof import('./components/firebase/Login.vue')['default']
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
HttpBody: typeof import('./components/http/Body.vue')['default']
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
HttpHeaders: typeof import('./components/http/Headers.vue')['default']
HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
HttpOAuth2Authorization: typeof import('./components/http/OAuth2Authorization.vue')['default']
HttpParameters: typeof import('./components/http/Parameters.vue')['default']
HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
HttpRequest: typeof import('./components/http/Request.vue')['default']
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideInfo: typeof import('~icons/lucide/info')['default']
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
Teams: typeof import('./components/teams/index.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
TeamsModal: typeof import('./components/teams/Modal.vue')['default']
TeamsTeam: typeof import('./components/teams/Team.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy']
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
}
}

View File

@@ -331,9 +331,14 @@ const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const workspace = workspaceService.currentWorkspace
const workspaceName = computed(() => {
return workspace.value.type === "personal"
? t("workspace.personal")
: workspace.value.teamName
if (workspace.value.type === "personal") {
return currentUser.value?.displayName
? t("workspace.personal_workspace", {
name: currentUser.value.displayName,
})
: t("workspace.personal")
}
return workspace.value.teamName
})
const refetchTeams = () => {

View File

@@ -165,7 +165,7 @@ import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { uniqueID } from "~/helpers/utils/uniqueID"
import { uniqueId } from "lodash-es"
type EnvironmentVariable = {
id: number
@@ -277,7 +277,7 @@ const workingEnv = computed(() => {
} as Environment
} else if (props.action === "new") {
return {
id: uniqueID(),
id: uniqueId(),
name: "",
variables: props.envVars(),
}
@@ -331,7 +331,7 @@ watch(
: "variables"
if (props.editingEnvironmentIndex !== "Global") {
editingID.value = workingEnv.value?.id || uniqueID()
editingID.value = workingEnv.value?.id ?? uniqueId()
}
vars.value = pipe(
workingEnv.value?.variables ?? [],
@@ -416,12 +416,14 @@ const saveEnvironment = () => {
const variables = pipe(
filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
)
const environmentUpdated: Environment = {
v: 1,
id: uniqueID(),
id: uniqueId(),
name: editingName.value,
variables,
}

View File

@@ -360,7 +360,7 @@ const saveEnvironment = async () => {
return
}
const filteredVariables = pipe(
const filterdVariables = pipe(
vars.value,
A.filterMap(
flow(
@@ -371,15 +371,17 @@ const saveEnvironment = async () => {
)
const secretVariables = pipe(
filteredVariables,
filterdVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
)
)
const variables = pipe(
filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
filterdVariables,
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
)
const environmentUpdated: Environment = {

View File

@@ -15,6 +15,8 @@ import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform"
const props = defineProps<{
section?: string
@@ -26,11 +28,23 @@ const t = useI18n()
const workspaceService = useService(WorkspaceService)
const workspace = workspaceService.currentWorkspace
const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(),
platform.auth.getProbableUser()
)
const currentWorkspace = computed(() => {
if (props.isOnlyPersonal || workspace.value.type === "personal") {
return t("workspace.personal")
const personalWorkspaceName = currentUser.value?.displayName
? t("workspace.personal_workspace", { name: currentUser.value.displayName })
: t("workspace.personal")
if (props.isOnlyPersonal) {
return personalWorkspaceName
}
return teamWorkspaceName.value
if (workspace.value.type === "team") {
return teamWorkspaceName.value
}
return personalWorkspaceName
})
const teamWorkspaceName = computed(() => {

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col">
<div class="flex flex-col">
<HoppSmartItem
:label="t('workspace.personal')"
:label="personalWorkspaceName"
:icon="IconUser"
:info-icon="workspace.type === 'personal' ? IconDone : undefined"
:active-info-icon="workspace.type === 'personal'"
@@ -96,6 +96,12 @@ const currentUser = useReadonlyStream(
platform.auth.getProbableUser()
)
const personalWorkspaceName = computed(() =>
currentUser.value?.displayName
? t("workspace.personal_workspace", { name: currentUser.value.displayName })
: t("workspace.personal")
)
const workspaceService = useService(WorkspaceService)
const teamListadapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListadapter.teamList$, [])

View File

@@ -4,12 +4,11 @@ import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as A from "fp-ts/Array"
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
import { entityReference } from "verzod"
import { z } from "zod"
export const hoppRESTImporter = (content: string[]) =>
pipe(
@@ -27,14 +26,26 @@ export const hoppRESTImporter = (content: string[]) =>
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
/**
* checks if a value is a plain object
*/
const isPlainObject = (value: any): value is object => _isPlainObject(value)
/**
* checks if a collection matches the schema for a hoppscotch collection.
* here 2 is the latest version of the schema.
*/
const isValidCollection = (collection: unknown): collection is HoppCollection =>
isPlainObject(collection) && "v" in collection && collection.v === 2
/**
* checks if a collection is a valid hoppscotch collection.
* else translate it into one.
*/
const validateCollection = (collection: unknown) => {
const result = entityReference(HoppCollection).safeParse(collection)
if (result.success) return O.some(result.data)
if (isValidCollection(collection)) {
return O.some(collection)
}
return O.some(translateToNewRESTCollection(collection))
}
@@ -64,9 +75,8 @@ export const hoppGQLImporter = (content: string) =>
* @returns the collection if it is valid, else a translated version of the collection
*/
export const validateGQLCollection = (collection: unknown) => {
const result = z.array(entityReference(HoppCollection)).safeParse(collection)
if (result.success) return O.some(result.data)
if (isValidCollection(collection)) {
return O.some(collection)
}
return O.some(translateToNewGQLCollection(collection))
}

View File

@@ -6,7 +6,7 @@ import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { z } from "zod"
import { NonSecretEnvironment } from "@hoppscotch/data"
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
import { uniqueID } from "~/helpers/utils/uniqueID"
import { uniqueId } from "lodash-es"
const insomniaResourcesSchema = z.object({
resources: z.array(
@@ -67,7 +67,7 @@ export const insomniaEnvImporter = (contents: string[]) => {
if (parsedInsomniaEnv.success) {
const environment: NonSecretEnvironment = {
id: uniqueID(),
id: uniqueId(),
v: 1,
name: parsedInsomniaEnv.data.name,
variables: Object.entries(parsedInsomniaEnv.data.data).map(

View File

@@ -1,11 +1,11 @@
import { Environment } from "@hoppscotch/data"
import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import { uniqueId } from "lodash-es"
import { z } from "zod"
import { safeParseJSON } from "~/helpers/functional/json"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { uniqueID } from "~/helpers/utils/uniqueID"
const postmanEnvSchema = z.object({
name: z.string(),
@@ -49,7 +49,7 @@ export const postmanEnvImporter = (contents: string[]) => {
// Convert `values` to `variables` to match the format expected by the system
const environments: Environment[] = validationResult.data.map(
({ name, values }) => ({
id: uniqueID(),
id: uniqueId(),
v: 1,
name,
variables: values.map((entires) => ({ ...entires, secret: false })),

View File

@@ -1,3 +0,0 @@
export const uniqueID = (length = 16) => {
return Math.random().toString(36).substring(2, length)
}

View File

@@ -1,8 +1,7 @@
import { Environment } from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es"
import { cloneDeep, isEqual, uniqueId } from "lodash-es"
import { combineLatest, Observable } from "rxjs"
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
import { uniqueID } from "~/helpers/utils/uniqueID"
import { getService } from "~/modules/dioc"
import DispatchingStore, {
defineDispatchers,
@@ -23,7 +22,7 @@ const defaultEnvironmentsState = {
environments: [
{
v: 1,
id: uniqueID(),
id: uniqueId(),
name: "My Environment Variables",
variables: [],
},
@@ -101,7 +100,7 @@ const dispatchers = defineDispatchers({
}
: {
v: 1,
id: uniqueID(),
id: "",
name,
variables,
},
@@ -124,7 +123,7 @@ const dispatchers = defineDispatchers({
...environments,
{
...cloneDeep(newEnvironment),
id: uniqueID(),
id: uniqueId(),
name: `${newEnvironment.name} - Duplicate`,
},
],

View File

@@ -52,6 +52,8 @@ export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
* @returns The proper new collection format
*/
export function translateToNewRESTCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewRESTCollection)
@@ -79,6 +81,8 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
* @returns The proper new collection format
*/
export function translateToNewGQLCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewGQLCollection)

View File

@@ -5,7 +5,7 @@ import { InferredEntity, createVersionedEntity } from "verzod"
import { z } from "zod"
import V0_VERSION from "./v/0"
import V1_VERSION, { uniqueID } from "./v/1"
import V1_VERSION from "./v/1"
const versionedObject = z.object({
v: z.number(),
@@ -165,7 +165,7 @@ export const translateToNewEnvironment = (x: any): Environment => {
if (x.v && x.v === EnvironmentSchemaVersion) return x
// Legacy
const id = x.id || uniqueID()
const id = x.id ?? ""
const name = x.name ?? "Untitled"
const variables = (x.variables ?? []).map(translateToNewEnvironmentVariables)

View File

@@ -2,8 +2,6 @@ import { z } from "zod"
import { defineVersion } from "verzod"
import { V0_SCHEMA } from "./0"
export const uniqueID = () => Math.random().toString(36).substring(2, 16)
export const V1_SCHEMA = z.object({
v: z.literal(1),
id: z.string(),
@@ -30,7 +28,7 @@ export default defineVersion({
const result: z.infer<typeof V1_SCHEMA> = {
...old,
v: 1,
id: old.id || uniqueID(),
id: old.id ?? "",
variables: old.variables.map((variable) => {
return {
...variable,

View File

@@ -18,8 +18,22 @@ export const HoppRESTRequestVariables = z.array(
export type HoppRESTRequestVariables = z.infer<typeof HoppRESTRequestVariables>
const V2_SCHEMA = V1_SCHEMA.extend({
const V2_SCHEMA = z.object({
v: z.literal("2"),
id: z.optional(z.string()), // Firebase Firestore ID
name: z.string(),
method: z.string(),
endpoint: z.string(),
params: HoppRESTParams,
headers: HoppRESTHeaders,
preRequestScript: z.string().catch(""),
testScript: z.string().catch(""),
auth: HoppRESTAuth,
body: HoppRESTReqBody,
requestVariables: HoppRESTRequestVariables,
})

View File

@@ -112,15 +112,10 @@ function exportedCollectionToHoppCollection(
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
),
requests: restCollection.requests.map((request) => {
const requestParsedResult = HoppRESTRequest.safeParse(request)
if (requestParsedResult.type === "ok") {
return requestParsedResult.value
}
const {
v,
requests: restCollection.requests.map(
({
id,
v,
auth,
body,
endpoint,
@@ -130,23 +125,20 @@ function exportedCollectionToHoppCollection(
params,
preRequestScript,
testScript,
requestVariables,
} = request
return {
v,
}) => ({
id,
name,
endpoint,
method,
params,
requestVariables: requestVariables,
v,
auth,
headers,
body,
endpoint,
headers,
method,
name,
params,
preRequestScript,
testScript,
}
}),
})
),
}
} else {
const gqlCollection = collection as ExportedUserCollectionGQL

View File

@@ -120,15 +120,10 @@ function exportedCollectionToHoppCollection(
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
),
requests: restCollection.requests.map((request) => {
const requestParsedResult = HoppRESTRequest.safeParse(request)
if (requestParsedResult.type === "ok") {
return requestParsedResult.value
}
const {
v,
requests: restCollection.requests.map(
({
id,
v,
auth,
body,
endpoint,
@@ -139,22 +134,21 @@ function exportedCollectionToHoppCollection(
preRequestScript,
testScript,
requestVariables,
} = request
return {
v,
}) => ({
id,
name,
endpoint,
method,
params,
requestVariables: requestVariables,
v,
auth,
headers,
body,
endpoint,
headers,
method,
name,
params,
preRequestScript,
testScript,
}
}),
requestVariables,
})
),
auth: data.auth,
headers: data.headers,
}

View File

@@ -106,18 +106,8 @@
"admin_failure": "Failed to make user an admin!!",
"admin_success": "User is now an admin!!",
"and": "and",
"clear_selection": "Clear Selection",
"configure_auth": "Check out the documentation to configure auth providers.",
"confirm_admin_to_user": "Do you want to remove admin status from this user?",
"confirm_admins_to_users": "Do you want to remove admin status from selected users?",
"confirm_delete_invite": "Do you want to revoke the selected invite?",
"confirm_delete_invites": "Do you want to revoke selected invites?",
"confirm_user_deletion": "Confirm user deletion?",
"confirm_users_deletion": "Do you want to delete selected users?",
"confirm_user_to_admin": "Do you want to make this user into an admin?",
"confirm_users_to_admin": "Do you want to make selected users into admins?",
"confirm_logout": "Confirm Logout",
"created_on": "Created On",
"continue_email": "Continue with Email",
"continue_github": "Continue with Github",
"continue_google": "Continue with Google",
@@ -126,21 +116,12 @@
"create_team_failure": "Failed to create workspace!!",
"create_team_success": "Workspace created successfully!!",
"data_sharing_failure": "Failed to update data sharing settings",
"delete_invite_failure": "Failed to delete invite!!",
"delete_invites_failure": "Failed to delete selected invites!!",
"delete_invite_success": "Invite deleted successfully!!",
"delete_invites_success": "Selected invites deleted successfully!!",
"delete_request_failure": "Shared Request deletion failed!!",
"delete_request_success": "Shared Request deleted successfully!!",
"delete_team_failure": "Workspace deletion failed!!",
"delete_team_success": "Workspace deleted successfully!!",
"delete_some_users_failure": "Number of Users Not Deleted: {count}",
"delete_some_users_success": "Number of Users Deleted: {count}",
"delete_user_failed_only_one_admin": "Failed to delete user. There should be atleast one admin!!",
"delete_user_failure": "User deletion failed!!",
"delete_users_failure": "Failed to delete selected users!!",
"delete_user_success": "User deleted successfully!!",
"delete_users_success": "Selected users deleted successfully!!",
"email": "Email",
"email_failure": "Failed to send invitation",
"email_signin_failure": "Failed to login with Email",
@@ -165,22 +146,16 @@
"reenter_email": "Re-enter email",
"remove_admin_failure": "Failed to remove admin status!!",
"remove_admin_success": "Admin status removed!!",
"remove_admin_from_users_failure": "Failed to remove admin status from selected users!!",
"remove_admin_from_users_success": "Admin status removed from selected users!!",
"remove_admin_to_delete_user": "Remove admin privilege to delete the user!!",
"remove_admin_for_deletion": "Remove admin status before attempting deletion!!",
"remove_invitee_failure": "Removal of invitee failed!!",
"remove_invitee_success": "Removal of invitee is successfull!!",
"remove_member_failure": "Member couldn't be removed!!",
"remove_member_success": "Member removed successfully!!",
"rename_team_failure": "Failed to rename workspace!!",
"rename_team_success": "Workspace renamed successfully!",
"rename_user_failure": "Failed to rename user!!",
"rename_user_success": "User renamed successfully!!",
"require_auth_provider": "You need to set atleast one authentication provider to log in.",
"role_update_failed": "Roles updation has failed!!",
"role_update_success": "Roles updated successfully!!",
"selected": "{count} selected",
"self_host_docs": "Self Host Documentation",
"send_magic_link": "Send magic link",
"setup_failure": "Setup has failed!!",
@@ -189,11 +164,7 @@
"sign_in_options": "All sign in option",
"sign_out": "Sign out",
"team_name_too_short": "Workspace name should be atleast 6 characters long!!",
"team_name_long": "Workspace name should be atleast 6 characters long!!",
"user_already_invited": "Failed to send invite. User is already invited!!",
"user_not_found": "User not found in the infra!!",
"users_to_admin_success": "Selected users are elevated to admin status!!",
"users_to_admin_failure": "Failed to elevate selected users to admin status!!"
"user_not_found": "User not found in the infra!!"
},
"teams": {
"add_member": "Add Member",
@@ -230,7 +201,7 @@
"name": "Workspace Name",
"no_members": "No members in this workspace. Add members to this workspace to collaborate",
"no_pending_invites": "No pending invites",
"no_teams": "No workspaces found..",
"no_teams": "No workspaces found",
"pending_invites": "Pending invites",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
@@ -255,17 +226,16 @@
"admin": "Admin",
"admin_email": "Admin Email",
"admin_id": "Admin ID",
"cancel": "Cancel",
"confirm_admin_to_user": "Do you want to remove admin status from this user?",
"confirm_user_deletion": "Confirm user deletion?",
"confirm_user_to_admin": "Do you want to make this user into an admin?",
"created_on": "Created On",
"date": "Date",
"delete": "Delete",
"delete_user": "Delete User",
"delete_users": "Delete Users",
"details": "Details",
"edit": "Edit",
"email": "Email",
"email_address": "Email Address",
"empty_name": "Name cannot be empty!!",
"id": "User ID",
"invalid_user": "Invalid User",
"invite_load_list_error": "Unable to Load Invited Users List",
@@ -275,18 +245,14 @@
"invitee_email": "Invitee Email",
"load_info_error": "Unable to load user info",
"load_list_error": "Unable to Load Users List",
"make_admin": "Make Admin",
"make_admin": "Make admin",
"name": "Name",
"no_invite": "No pending invites found",
"no_invite": "No invited users found",
"no_shared_requests": "No shared requests created by the user",
"no_users": "No users found",
"not_found": "User not found",
"pending_invites": "Pending Invites",
"remove_admin_privilege": "Remove Admin Privilege",
"remove_admin_status": "Remove Admin Status",
"rename": "Rename",
"revoke_invitation": "Revoke Invitation",
"searchbar_placeholder": "Search by name or email..",
"send_invite": "Send Invite",
"show_more": "Show more",
"uid": "UID",

View File

@@ -17,7 +17,7 @@
"@fontsource-variable/material-symbols-rounded": "5.0.5",
"@fontsource-variable/roboto-mono": "5.0.6",
"@graphql-typed-document-node/core": "3.1.1",
"@hoppscotch/ui": "0.1.2",
"@hoppscotch/ui": "0.1.0",
"@hoppscotch/vue-toasted": "0.1.0",
"@intlify/unplugin-vue-i18n": "1.2.0",
"@types/cors": "2.8.13",
@@ -78,4 +78,4 @@
"singleQuote": true,
"semi": true
}
}
}

View File

@@ -29,11 +29,8 @@ declare module '@vue/runtime-core' {
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
SettingsDataSharing: typeof import('./components/settings/DataSharing.vue')['default']
@@ -51,7 +48,6 @@ declare module '@vue/runtime-core' {
UsersDetails: typeof import('./components/users/Details.vue')['default']
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
UsersTable: typeof import('./components/users/Table.vue')['default']
}
}

View File

@@ -45,6 +45,7 @@
:type="
isMasked(provider.name, field.key) ? 'password' : 'text'
"
:disabled="isMasked(provider.name, field.key)"
:autofocus="false"
class="!my-2 !bg-primaryLight flex-1"
/>

View File

@@ -21,12 +21,13 @@
</HoppSmartToggle>
</div>
<!-- TODO: Update the link below -->
<HoppButtonSecondary
outline
filled
:icon="IconShieldQuestion"
:label="t('configs.data_sharing.see_shared')"
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/telemetry"
to="http://docs.hoppscotch.io"
blank
class="w-min my-2"
/>

View File

@@ -37,6 +37,7 @@
<HoppSmartInput
v-model="smtpConfigs.fields[field.key]"
:type="isMasked(field.key) ? 'password' : 'text'"
:disabled="isMasked(field.key)"
:autofocus="false"
class="!my-2 !bg-primaryLight flex-1"
/>

View File

@@ -28,9 +28,10 @@
>
{{ t('data_sharing.toggle_description') }}
</HoppSmartToggle>
<!-- TODO: Update link -->
<HoppSmartAnchor
blank
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/telemetry"
to="http://docs.hoppscotch.io"
:label="t('data_sharing.see_shared')"
class="underline"
/>

View File

@@ -24,40 +24,11 @@
</div>
<template v-for="(info, key) in userInfo" :key="key">
<div v-if="key === 'displayName'" class="flex flex-col space-y-3">
<label class="text-accentContrast" for="teamname"
>{{ t('users.name') }}
</label>
<div
class="flex bg-divider rounded-md items-stretch flex-1 border border-divider"
:class="{
'!border-accent': isNameBeingEdited,
}"
>
<HoppSmartInput
v-model="updatedUserName"
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
:placeholder="t('users.name')"
:disabled="!isNameBeingEdited"
>
<template #button>
<HoppButtonPrimary
class="!rounded-l-none"
filled
:icon="isNameBeingEdited ? IconSave : IconEdit"
:label="
isNameBeingEdited ? t('users.rename') : t('users.edit')
"
@click="handleNameEdit"
/>
</template>
</HoppSmartInput>
</div>
</div>
<div v-else-if="info.condition">
<div v-if="info.condition">
<label class="text-secondaryDark" :for="key">{{ info.label }}</label>
<div class="w-full p-3 mt-2 bg-divider border-gray-600 rounded-md">
<div
class="w-full p-3 mt-2 bg-divider border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
>
<span>{{ info.value }}</span>
</div>
</div>
@@ -99,17 +70,10 @@
</template>
<script setup lang="ts">
import { useMutation } from '@urql/vue';
import { format } from 'date-fns';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import {
UpdateUserDisplayNameByAdminDocument,
UserInfoQuery,
} from '~/helpers/backend/graphql';
import IconEdit from '~icons/lucide/edit';
import IconSave from '~icons/lucide/save';
import { UserInfoQuery } from '~/helpers/backend/graphql';
import IconTrash from '~icons/lucide/trash';
import IconUserCheck from '~icons/lucide/user-check';
import IconUserMinus from '~icons/lucide/user-minus';
@@ -125,7 +89,6 @@ const emit = defineEmits<{
(event: 'delete-user', userID: string): void;
(event: 'make-admin', userID: string): void;
(event: 'remove-admin', userID: string): void;
(event: 'update-user-name', newName: string): void;
}>();
// Get Proper Date Formats
@@ -157,62 +120,4 @@ const userInfo = {
value: getCreatedDateAndTime(createdOn),
},
};
// Contains the actual user name
const userName = computed({
get: () => props.user.displayName,
set: (value) => {
return value;
},
});
// Contains the stored user name from the actual name before being edited
const currentUserName = ref('');
// Set the current user name to the actual user name
onMounted(() => {
if (displayName) currentUserName.value = displayName;
});
// Contains the user name that is being edited
const updatedUserName = computed({
get: () => currentUserName.value,
set: (value) => {
currentUserName.value = value;
},
});
// Rename the user
const isNameBeingEdited = ref(false);
const userRename = useMutation(UpdateUserDisplayNameByAdminDocument);
const handleNameEdit = () => {
if (isNameBeingEdited.value) {
// If the name is not changed, then return control
if (userName.value !== updatedUserName.value) {
renameUserName();
} else isNameBeingEdited.value = false;
} else {
isNameBeingEdited.value = true;
}
};
const renameUserName = async () => {
if (updatedUserName.value?.trim() === '') {
toast.error(t('users.empty_name'));
return;
}
const variables = { userUID: uid, name: updatedUserName.value as string };
const result = await userRename.executeMutation(variables);
if (result.error) {
toast.error(t('state.rename_user_failure'));
} else {
isNameBeingEdited.value = false;
toast.success(t('state.rename_user_success'));
emit('update-user-name', updatedUserName.value as string);
userName.value = updatedUserName.value;
}
};
</script>

View File

@@ -10,7 +10,6 @@
v-model="email"
:label="t('users.email_address')"
input-styles="floating-input"
@submit="sendInvite"
/>
</template>
<template #footer>
@@ -19,12 +18,7 @@
:label="t('users.send_invite')"
@click="sendInvite"
/>
<HoppButtonSecondary
:label="t('users.cancel')"
outline
filled
@click="hideModal"
/>
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
</span>
</template>
</HoppSmartModal>

View File

@@ -1,68 +1,78 @@
<template>
<div class="px-4 mt-7">
<div class="px-4">
<div v-if="fetching" class="flex justify-center">
<HoppSmartSpinner />
</div>
<div v-else-if="error">{{ t('shared_requests.load_list_error') }}</div>
<div v-else-if="sharedRequests.length === 0">
<div v-else-if="sharedRequests.length === 0" class="mt-5">
{{ t('users.no_shared_requests') }}
</div>
<HoppSmartTable v-else :headings="headings" :list="sharedRequests">
<HoppSmartTable v-else class="mt-8" :list="sharedRequests">
<template #head>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">
{{ t('shared_requests.action') }}
</th>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">
{{ t('shared_requests.action') }}
</th>
</tr>
</template>
<template #body="{ row: sharedRequest }">
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ sharedRequest.id }}
</span>
</td>
<template #body="{ list: sharedRequests }">
<tr
v-for="request in sharedRequests"
:key="request.id"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
>
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ request.id }}
</span>
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(sharedRequest.request) }}
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(request.request) }}
</td>
<td class="py-2 px-7">
{{ getCreatedDate(sharedRequest.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(sharedRequest.createdOn) }}
</div>
</td>
<td class="py-2 px-7">
{{ getCreatedDate(request.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(request.createdOn) }}
</div>
</td>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${sharedRequest.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${request.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(sharedRequest.id)"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(request.id)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(sharedRequest.id)"
/>
</td>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(request.id)"
/>
</td>
</tr>
</template>
</HoppSmartTable>
@@ -126,18 +136,11 @@ const {
} = usePagedQuery(
SharedRequestsDocument,
(x) => x.infra.allShortcodes,
(x) => x.id,
sharedRequestsPerPage,
{ cursor: undefined, take: sharedRequestsPerPage, email: props.email },
(x) => x.id
{ cursor: undefined, take: sharedRequestsPerPage, email: props.email }
);
const headings = [
{ key: 'id', label: t('shared_requests.id') },
{ key: 'request', label: t('shared_requests.url') },
{ key: 'createdOn', label: t('shared_requests.created_on') },
{ key: 'action', label: t('shared_requests.action') },
];
// Return request endpoint from the request object
const sharedRequestURL = (request: string) => {
const parsedRequest = JSON.parse(request);
@@ -171,17 +174,17 @@ const deleteSharedRequestMutation = async (id: string | null) => {
return;
}
const variables = { codeID: id };
const result = await sharedRequestDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_request_failure'));
} else {
sharedRequests.value = sharedRequests.value.filter(
(request) => request.id !== id
);
refetch();
toast.success(t('state.delete_request_success'));
}
await sharedRequestDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(t('state.delete_request_failure'));
} else {
sharedRequests.value = sharedRequests.value.filter(
(request) => request.id !== id
);
refetch();
toast.success(t('state.delete_request_success'));
}
});
confirmDeletion.value = false;
deleteSharedRequestID.value = null;
};

View File

@@ -1,4 +1,4 @@
import { onMounted, ref, Ref } from 'vue';
import { Ref, onMounted, ref } from 'vue';
import { DocumentNode } from 'graphql';
import { TypedDocumentNode, useClientHandle } from '@urql/vue';
@@ -9,9 +9,9 @@ export function usePagedQuery<
>(
query: string | TypedDocumentNode<Result, Vars> | DocumentNode,
getList: (result: Result) => ListItem[],
getCursor: (value: ListItem) => string,
itemsPerPage: number,
baseVariables: Vars,
getCursor?: (value: ListItem) => string
variables: Vars
) {
const { client } = useClientHandle();
const fetching = ref(true);
@@ -20,25 +20,21 @@ export function usePagedQuery<
const currentPage = ref(0);
const hasNextPage = ref(true);
const fetchNextPage = async (additionalVariables?: Vars) => {
let variables = { ...baseVariables };
const fetchNextPage = async () => {
fetching.value = true;
// Cursor based pagination
if (getCursor) {
const cursor =
list.value.length > 0
? getCursor(list.value.at(-1) as ListItem)
: undefined;
variables = { ...variables, cursor };
}
// Offset based pagination
else if (additionalVariables) {
variables = { ...variables, ...additionalVariables };
}
const cursor =
list.value.length > 0 ? getCursor(list.value.at(-1)!) : undefined;
const variablesForPagination = {
...variables,
take: itemsPerPage,
cursor,
};
const result = await client
.query(query, variablesForPagination)
.toPromise();
const result = await client.query(query, variables).toPromise();
if (result.error) {
error.value = true;
fetching.value = false;
@@ -67,14 +63,11 @@ export function usePagedQuery<
}
};
const refetch = async (variables?: Vars) => {
const refetch = async () => {
currentPage.value = 0;
hasNextPage.value = true;
list.value = [];
if (hasNextPage.value) {
variables ? await fetchNextPage(variables) : await fetchNextPage();
}
await fetchNextPage();
};
return {

View File

@@ -1,3 +0,0 @@
mutation DemoteUsersByAdmin($userUIDs: [ID!]!) {
demoteUsersByAdmin(userUIDs: $userUIDs)
}

View File

@@ -0,0 +1,3 @@
mutation MakeUserAdmin($uid: ID!) {
makeUserAdmin(userUID: $uid)
}

View File

@@ -1,3 +0,0 @@
mutation MakeUsersAdmin($userUIDs: [ID!]!) {
makeUsersAdmin(userUIDs: $userUIDs)
}

View File

@@ -0,0 +1,3 @@
mutation RemoveUserAsAdmin($uid: ID!) {
removeUserAsAdmin(userUID: $uid)
}

View File

@@ -0,0 +1,3 @@
mutation RemoveUserByAdmin($uid: ID!) {
removeUserByAdmin(userUID: $uid)
}

View File

@@ -1,7 +0,0 @@
mutation RemoveUsersByAdmin($userUIDs: [ID!]!) {
removeUsersByAdmin(userUIDs: $userUIDs) {
userUID
isDeleted
errorMessage
}
}

View File

@@ -1,3 +0,0 @@
mutation RevokeUserInvitationsByAdmin($inviteeEmails: [String!]!) {
revokeUserInvitationsByAdmin(inviteeEmails: $inviteeEmails)
}

View File

@@ -1,3 +0,0 @@
mutation UpdateUserDisplayNameByAdmin($userUID: ID!, $name: String!) {
updateUserDisplayNameByAdmin(userUID: $userUID, displayName: $name)
}

View File

@@ -1,12 +0,0 @@
query UsersListV2($searchString: String, $skip: Int, $take: Int) {
infra {
allUsersV2(searchString: $searchString, skip: $skip, take: $take) {
uid
displayName
email
isAdmin
photoURL
createdOn
}
}
}

View File

@@ -7,13 +7,3 @@ export const UNAUTHORIZED = 'Unauthorized' as const;
// Sometimes the backend returns Unauthorized error message as follows:
export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const;
export const DELETE_USER_FAILED_ONLY_ONE_ADMIN =
'admin/only_one_admin_account_found' as const;
export const ADMIN_CANNOT_BE_DELETED =
'admin/admin_can_not_be_deleted' as const;
// When trying to invite a user that is already invited
export const USER_ALREADY_INVITED =
'[GraphQL] admin/user_already_invited' as const;

View File

@@ -18,83 +18,89 @@
<div v-else-if="error">{{ t('teams.load_list_error') }}</div>
<HoppSmartTable
v-else-if="teamsList.length"
:headings="headings"
:list="teamsList"
@onRowClicked="goToTeamDetails"
>
<HoppSmartTable v-else-if="teamsList.length" :list="teamsList">
<template #head>
<th class="px-6 py-2">{{ t('teams.id') }}</th>
<th class="px-6 py-2">{{ t('teams.name') }}</th>
<th class="px-6 py-2">{{ t('teams.members') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2"></th>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('teams.id') }}</th>
<th class="px-6 py-2">{{ t('teams.name') }}</th>
<th class="px-6 py-2">{{ t('teams.members') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2"></th>
</tr>
</template>
<template #body="{ row: team }">
<td class="flex py-4 px-7 max-w-[16rem]">
<span class="truncate">
{{ team.id }}
</span>
</td>
<template #body="{ list }">
<tr
v-for="team in list"
:key="team.id"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
@click="goToTeamDetails(team.id)"
>
<td class="flex py-4 px-7 max-w-[16rem]">
<span class="truncate">
{{ team.id }}
</span>
</td>
<td class="py-4 px-7 min-w-[20rem]">
<span
class="flex items-center truncate"
:class="{ truncate: team.name }"
>
{{ team.name ?? t('teams.unnamed') }}
</span>
</td>
<td class="py-4 px-7 min-w-[20rem]">
<span
class="flex items-center truncate"
:class="{ truncate: team.name }"
>
{{ team.name ?? t('teams.unnamed') }}
</span>
</td>
<td class="py-4 px-8">
{{ team.members?.length }}
</td>
<td class="py-4 px-7">
{{ team.members?.length }}
</td>
<td @click.stop class="flex justify-end mr-10">
<div class="relative">
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="IconTrash"
:label="t('teams.delete_team')"
class="!hover:bg-red-600 w-full"
@click="
() => {
deleteTeam(team.id);
hide();
}
"
/>
</div>
</template>
</tippy>
</div>
</td>
<td @click.stop>
<div class="relative">
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="IconTrash"
:label="t('teams.delete_team')"
class="!hover:bg-red-600 w-full"
@click="
() => {
deleteTeam(team.id);
hide();
}
"
/>
</div>
</template>
</tippy>
</div>
</td>
</tr>
</template>
</HoppSmartTable>
<div v-else class="px-2">
<div v-else class="px-2 text-lg">
{{ t('teams.no_teams') }}
</div>
<div
v-if="hasNextPage && teamsList.length >= teamsPerPage"
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextTeams"
>
<span>{{ t('teams.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2" />
<icon-lucide-chevron-down class="ml-2 text-lg" />
</div>
</div>
</div>
@@ -129,7 +135,6 @@ import {
CreateTeamDocument,
MetricsDocument,
RemoveTeamDocument,
TeamInfoQuery,
TeamListDocument,
UsersListDocument,
} from '../../helpers/backend/graphql';
@@ -144,9 +149,9 @@ const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
const { list: usersList } = usePagedQuery(
UsersListDocument,
(x) => x.infra.allUsers,
(x) => x.uid,
usersPerPage.value,
{ cursor: undefined, take: usersPerPage.value },
(x) => x.uid
{ cursor: undefined, take: usersPerPage.value }
);
const allUsersEmail = computed(() => usersList.value.map((user) => user.email));
@@ -163,19 +168,11 @@ const {
} = usePagedQuery(
TeamListDocument,
(x) => x.infra.allTeams,
(x) => x.id,
teamsPerPage,
{ cursor: undefined, take: teamsPerPage },
(x) => x.id
{ cursor: undefined, take: teamsPerPage }
);
// Table Headings
const headings = [
{ key: 'id', label: t('teams.id') },
{ key: 'name', label: t('teams.name') },
{ key: 'members', label: t('teams.members') },
{ key: 'actions', label: '' },
];
// Create Team
const showCreateTeamModal = ref(false);
const createTeamLoading = ref(false);
@@ -215,8 +212,7 @@ const createTeam = async (newTeamName: string, ownerEmail: string) => {
// Go To Individual Team Details Page
const router = useRouter();
const goToTeamDetails = (team: TeamInfoQuery['infra']['teamInfo']) =>
router.push('/teams/' + team.id);
const goToTeamDetails = (teamId: string) => router.push('/teams/' + teamId);
// Team Deletion
const confirmDeletion = ref(false);

View File

@@ -12,7 +12,7 @@
<div class="flex items-center space-x-3">
<h1 class="text-lg text-accentContrast">
{{ userName }}
{{ user.displayName }}
</h1>
<span>/</span>
<h2 class="text-lg text-accentContrast">
@@ -29,7 +29,6 @@
@delete-user="deleteUser"
@make-admin="makeUserAdmin"
@remove-admin="makeAdminToUser"
@update-user-name="(name: string) => (userName = name)"
class="py-8 px-4"
/>
</HoppSmartTab>
@@ -41,19 +40,19 @@
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="t('state.confirm_user_deletion')"
:title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)"
/>
<HoppSmartConfirmModal
:show="confirmUserToAdmin"
:title="t('state.confirm_user_to_admin')"
:title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)"
/>
<HoppSmartConfirmModal
:show="confirmAdminToUser"
:title="t('state.confirm_admin_to_user')"
:title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)"
/>
@@ -68,12 +67,11 @@ import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useClientHandler } from '~/composables/useClientHandler';
import {
DemoteUsersByAdminDocument,
MakeUsersAdminDocument,
RemoveUsersByAdminDocument,
MakeUserAdminDocument,
RemoveUserAsAdminDocument,
RemoveUserByAdminDocument,
UserInfoDocument,
} from '~/helpers/backend/graphql';
import { ADMIN_CANNOT_BE_DELETED } from '~/helpers/errors';
const t = useI18n();
const toast = useToast();
@@ -106,15 +104,6 @@ onMounted(async () => {
await fetchData();
});
const userName = computed({
get: () => data.value?.infra.userInfo.displayName,
set: (value) => {
if (value) {
data.value!.infra.userInfo.displayName = value;
}
},
});
const user = computed({
get: () => data.value?.infra.userInfo,
set: (value) => {
@@ -124,69 +113,9 @@ const user = computed({
},
});
const confirmUserToAdmin = ref(false);
const userToAdminUID = ref<string | null>(null);
const usersToAdmin = useMutation(MakeUsersAdminDocument);
const makeUserAdmin = (id: string | null) => {
confirmUserToAdmin.value = true;
userToAdminUID.value = id;
};
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error(t('state.admin_failure'));
return;
}
const userUIDs = [id];
const variables = { userUIDs };
const result = await usersToAdmin.executeMutation(variables);
if (result.error) {
toast.error(t('state.admin_failure'));
} else {
toast.success(t('state.admin_success'));
user.value!.isAdmin = true;
}
confirmUserToAdmin.value = false;
userToAdminUID.value = null;
};
// Remove Admin Status from a current admin user
const adminToUser = useMutation(DemoteUsersByAdminDocument);
const confirmAdminToUser = ref(false);
const adminToUserUID = ref<string | null>(null);
const makeAdminToUser = (id: string) => {
confirmAdminToUser.value = true;
adminToUserUID.value = id;
};
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error(t('state.remove_admin_failure'));
return;
}
const userUIDs = [id];
const variables = { userUIDs };
const result = await adminToUser.executeMutation(variables);
if (result.error) {
toast.error(t('state.remove_admin_failure'));
} else {
toast.success(t('state.remove_admin_success'));
user.value!.isAdmin = false;
}
confirmAdminToUser.value = false;
adminToUserUID.value = null;
};
// User Deletion
const router = useRouter();
const userDeletion = useMutation(RemoveUsersByAdminDocument);
const userDeletion = useMutation(RemoveUserByAdminDocument);
const confirmDeletion = ref(false);
const deleteUserUID = ref<string | null>(null);
@@ -201,25 +130,73 @@ const deleteUserMutation = async (id: string | null) => {
toast.error(t('state.delete_user_failure'));
return;
}
const userUIDs = [id];
const variables = { userUIDs };
const variables = { uid: id };
const result = await userDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_user_failure'));
} else {
const deletedUsers = result.data?.removeUsersByAdmin || [];
const isAdminError = deletedUsers.some(
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
);
isAdminError
? toast.error(t('state.delete_user_failed_only_one_admin'))
: toast.success(t('state.delete_user_success'));
toast.success(t('state.delete_user_success'));
}
confirmDeletion.value = false;
deleteUserUID.value = null;
router.push('/users');
};
// Make User Admin
const userToAdmin = useMutation(MakeUserAdminDocument);
const confirmUserToAdmin = ref(false);
const userToAdminUID = ref<string | null>(null);
const makeUserAdmin = (id: string) => {
confirmUserToAdmin.value = true;
userToAdminUID.value = id;
};
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error(t('state.admin_failure'));
return;
}
const variables = { uid: id };
const result = await userToAdmin.executeMutation(variables);
if (result.error) {
toast.error(t('state.admin_failure'));
} else {
user.value!.isAdmin = true;
toast.success(t('state.admin_success'));
}
confirmUserToAdmin.value = false;
userToAdminUID.value = null;
};
// Remove Admin Status from a current admin user
const adminToUser = useMutation(RemoveUserAsAdminDocument);
const confirmAdminToUser = ref(false);
const adminToUserUID = ref<string | null>(null);
const makeAdminToUser = (id: string) => {
confirmAdminToUser.value = true;
adminToUserUID.value = id;
};
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error(t('state.remove_admin_failure'));
return;
}
const variables = { uid: id };
const result = await adminToUser.executeMutation(variables);
if (result.error) {
toast.error(t('state.remove_admin_failure'));
} else {
user.value!.isAdmin = false;
toast.error(t('state.remove_admin_success'));
}
confirmAdminToUser.value = false;
adminToUserUID.value = null;
};
</script>

View File

@@ -5,7 +5,7 @@
<h1 class="text-lg font-bold text-secondaryDark">
{{ t('users.users') }}
</h1>
<div class="flex items-center space-x-4 mt-10 mb-5">
<div class="flex items-center space-x-4 py-10">
<HoppButtonPrimary
:label="t('users.invite_user')"
@click="showInviteUserModal = true"
@@ -15,189 +15,132 @@
<HoppButtonSecondary
outline
filled
:label="t('users.pending_invites')"
:label="t('users.invited_users')"
:to="'/users/invited'"
/>
</div>
</div>
<div class="overflow-x-auto">
<div class="mb-3 flex items-center justify-end">
<HoppButtonSecondary
outline
filled
:icon="IconLeft"
:disabled="page === 1"
@click="changePage(PageDirection.Previous)"
/>
<div class="flex h-full w-10 items-center justify-center">
<span>{{ page }}</span>
</div>
<HoppButtonSecondary
outline
filled
:icon="IconRight"
:disabled="page >= totalPages"
@click="changePage(PageDirection.Next)"
/>
<div v-if="fetching" class="flex justify-center">
<HoppSmartSpinner />
</div>
<HoppSmartTable
v-model:list="finalUsersList"
v-model:selected-rows="selectedRows"
:headings="headings"
:loading="showSpinner"
@onRowClicked="goToUserDetails"
>
<template #extension>
<div class="flex w-full items-center bg-primary">
<icon-lucide-search class="mx-3 text-xs" />
<HoppSmartInput
v-model="query"
styles="w-full bg-primary py-1"
input-styles="h-full border-none"
:placeholder="t('users.searchbar_placeholder')"
/>
</div>
</template>
<div v-else-if="error">{{ t('users.load_list_error') }}</div>
<HoppSmartTable v-else-if="usersList.length" :list="usersList">
<template #head>
<th class="px-6 py-2">{{ t('users.id') }}</th>
<th class="px-6 py-2">{{ t('users.name') }}</th>
<th class="px-6 py-2">{{ t('users.email') }}</th>
<th class="px-6 py-2">{{ t('users.date') }}</th>
<!-- Empty header for Action Button -->
<th class="w-20 px-6 py-2"></th>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('users.id') }}</th>
<th class="px-6 py-2">{{ t('users.name') }}</th>
<th class="px-6 py-2">{{ t('users.email') }}</th>
<th class="px-6 py-2">{{ t('users.date') }}</th>
<!-- Empty header for Action Button -->
<th class="px-6 py-2"></th>
</tr>
</template>
<template #empty-state>
<td colspan="6">
<span class="flex justify-center p-3">
{{ error ? t('users.load_list_error') : t('users.no_users') }}
</span>
</td>
</template>
<template #body="{ list }">
<tr
v-for="user in list"
:key="user.uid"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
@click="goToUserDetails(user.uid)"
>
<td class="py-2 px-7 max-w-[8rem] truncate">
{{ user.uid }}
</td>
<template #body="{ row: user }">
<td class="py-2 px-7 max-w-[8rem] truncate">
{{ user.uid }}
</td>
<td class="py-2 px-7">
<div class="flex items-center space-x-2">
<span>
{{ user.displayName ?? t('users.unnamed') }}
</span>
<td class="py-2 px-7">
<div class="flex items-center space-x-2">
<span>
{{ user.displayName ?? t('users.unnamed') }}
</span>
<span
v-if="user.isAdmin"
class="text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
{{ t('users.admin') }}
</span>
</div>
</td>
<span
v-if="user.isAdmin"
class="text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
{{ t('users.admin') }}
</span>
</div>
</td>
<td class="py-2 px-7">
{{ user.email }}
</td>
<td class="py-2 px-7 truncate">
{{ user.email }}
</td>
<td class="py-2 px-7">
{{ getCreatedDate(user.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(user.createdOn) }}
</div>
</td>
<td class="py-2 px-7">
{{ getCreatedDate(user.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(user.createdOn) }}
</div>
</td>
<td @click.stop class="flex justify-end w-20">
<div class="mt-2 mr-5">
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="user.isAdmin ? IconUserMinus : IconUserCheck"
:label="
user.isAdmin
? t('users.remove_admin_status')
: t('users.make_admin')
"
class="!hover:bg-emerald-600"
@click="
() => {
<td @click.stop>
<div class="relative">
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconMoreHorizontal"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="user.isAdmin ? IconUserMinus : IconUserCheck"
:label="
user.isAdmin
? confirmAdminToUser(user.uid)
: confirmUserToAdmin(user.uid);
hide();
}
"
/>
<HoppSmartItem
v-if="!user.isAdmin"
:icon="IconTrash"
:label="t('users.delete_user')"
class="!hover:bg-red-600"
@click="
() => {
confirmUserDeletion(user.uid);
hide();
}
"
/>
</div>
</template>
</tippy>
</div>
</td>
? t('users.remove_admin_status')
: t('users.make_admin')
"
class="!hover:bg-emerald-600"
@click="
() => {
user.isAdmin
? makeAdminToUser(user.uid)
: makeUserAdmin(user.uid);
hide();
}
"
/>
<HoppSmartItem
v-if="!user.isAdmin"
:icon="IconTrash"
:label="t('users.delete_user')"
class="!hover:bg-red-600"
@click="
() => {
deleteUser(user.uid);
hide();
}
"
/>
</div>
</template>
</tippy>
</div>
</td>
</tr>
</template>
</HoppSmartTable>
<!-- Actions for Selected Rows -->
<div v-else-if="usersList.length === 0" class="flex justify-center">
{{ t('users.no_users') }}
</div>
<div
v-if="selectedRows.length"
class="fixed m-2 bottom-0 left-40 right-0 w-min mx-auto shadow-2xl"
v-if="hasNextPage && usersList.length >= usersPerPage"
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextUsers"
>
<div
class="flex justify-center items-end bg-primaryLight border border-divider rounded-md mb-5"
>
<HoppButtonSecondary
:icon="IconCheck"
:label="t('state.selected', { count: selectedRows.length })"
class="py-4 border-divider rounded-r-none bg-emerald-800 text-secondaryDark"
/>
<HoppButtonSecondary
:icon="IconUserCheck"
:label="t('users.make_admin')"
class="py-4 border-divider border-r-1 rounded-none hover:bg-emerald-600"
@click="confirmUsersToAdmin = true"
/>
<HoppButtonSecondary
:icon="IconUserMinus"
:label="t('users.remove_admin_status')"
class="py-4 border-divider border-r-1 rounded-none hover:bg-orange-500"
@click="confirmAdminsToUsers = true"
/>
<HoppButtonSecondary
:icon="IconTrash"
:label="t('users.delete_users')"
class="py-4 border-divider rounded-none hover:bg-red-500"
@click="confirmUsersDeletion = true"
/>
<HoppButtonSecondary
:icon="IconX"
:label="t('state.clear_selection')"
class="py-4 border-divider rounded-l-none text-secondaryDark bg-red-600 hover:bg-red-500"
@click="selectedRows.splice(0, selectedRows.length)"
/>
</div>
<span>{{ t('users.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2 text-lg" />
</div>
</div>
</div>
@@ -208,70 +151,46 @@
@send-invite="sendInvite"
/>
<HoppSmartConfirmModal
:show="confirmUsersToAdmin"
:title="
AreMultipleUsersSelected
? t('state.confirm_users_to_admin')
: t('state.confirm_user_to_admin')
"
@hide-modal="resetConfirmUserToAdmin"
@resolve="makeUsersToAdmin(usersToAdminUID)"
:show="confirmDeletion"
:title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)"
/>
<HoppSmartConfirmModal
:show="confirmAdminsToUsers"
:title="
AreMultipleUsersSelectedToAdmin
? t('state.confirm_admins_to_users')
: t('state.confirm_admin_to_user')
"
@hide-modal="resetConfirmAdminToUser"
@resolve="makeAdminsToUsers(adminsToUserUID)"
:show="confirmUserToAdmin"
:title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)"
/>
<HoppSmartConfirmModal
:show="confirmUsersDeletion"
:title="
AreMultipleUsersSelectedForDeletion
? t('state.confirm_users_deletion')
: t('state.confirm_user_deletion')
"
@hide-modal="resetConfirmUserDeletion"
@resolve="deleteUsers(deleteUserUID)"
:show="confirmAdminToUser"
:title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)"
/>
</div>
</template>
<script setup lang="ts">
import { useMutation, useQuery } from '@urql/vue';
import { useMutation } from '@urql/vue';
import { format } from 'date-fns';
import { computed, onUnmounted, ref, watch } from 'vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import {
DemoteUsersByAdminDocument,
InviteNewUserDocument,
MakeUsersAdminDocument,
MetricsDocument,
RemoveUsersByAdminDocument,
UserInfoQuery,
UsersListQuery,
UsersListV2Document,
} from '~/helpers/backend/graphql';
import {
ADMIN_CANNOT_BE_DELETED,
DELETE_USER_FAILED_ONLY_ONE_ADMIN,
USER_ALREADY_INVITED,
} from '~/helpers/errors';
import IconCheck from '~icons/lucide/check';
import IconLeft from '~icons/lucide/chevron-left';
import IconRight from '~icons/lucide/chevron-right';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import IconTrash from '~icons/lucide/trash';
import IconUserCheck from '~icons/lucide/user-check';
import IconUserMinus from '~icons/lucide/user-minus';
import IconAddUser from '~icons/lucide/user-plus';
import IconX from '~icons/lucide/x';
import {
InviteNewUserDocument,
MakeUserAdminDocument,
RemoveUserAsAdminDocument,
RemoveUserByAdminDocument,
UsersListDocument,
} from '~/helpers/backend/graphql';
// Get Proper Date Formats
const t = useI18n();
@@ -280,165 +199,25 @@ const toast = useToast();
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MM-yyyy');
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
// Table Headings
const headings = [
{ key: 'uid', label: t('users.id') },
{ key: 'displayName', label: t('users.name') },
{ key: 'email', label: t('users.email') },
{ key: 'createdOn', label: t('users.date') },
{ key: '', label: '' },
];
// Get Paginated Results of all the users in the infra
const usersPerPage = 20;
const {
fetching,
error,
refetch,
goToNextPage: fetchNextUsers,
list: usersList,
hasNextPage,
} = usePagedQuery(
UsersListV2Document,
(x) => x.infra.allUsersV2,
UsersListDocument,
(x) => x.infra.allUsers,
(x) => x.uid,
usersPerPage,
{ searchString: '', take: usersPerPage, skip: 0 }
{ cursor: undefined, take: usersPerPage }
);
// Selected Rows
const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
// Ensure this variable is declared outside the debounce function
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
let toastTimeout: ReturnType<typeof setTimeout> | null = null;
onUnmounted(() => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
if (toastTimeout) {
clearTimeout(toastTimeout);
}
});
// Debounce Function
const debounce = (func: () => void, delay: number) => {
if (debounceTimeout) clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(func, delay);
};
// Search
const query = ref('');
// Query which is sent to the backend after debouncing
const searchQuery = ref('');
const handleSearch = async (input: string) => {
searchQuery.value = input;
if (input.length === 0) {
await refetch({
searchString: '',
take: usersPerPage,
skip: (page.value - 1) * usersPerPage,
});
} else {
// If search query is present, fetch all the users filtered by the search query
await refetch({ searchString: input, take: usersCount.value!, skip: 0 });
}
// Reset the page to 1 when the search query changes
page.value = 1;
};
watch(query, () => {
if (query.value.length === 0) {
handleSearch(query.value);
} else {
debounce(() => {
handleSearch(query.value);
}, 500);
}
});
// Final Users List after Search and Pagination operations
const finalUsersList = computed(() =>
// If search query is present, filter the list based on the search query and return the paginated results
// Else just return the paginated results directly
searchQuery.value.length > 0
? usersList.value.slice(
(page.value - 1) * usersPerPage,
page.value * usersPerPage
)
: usersList.value
);
// Spinner
const showSpinner = ref(false);
watch(fetching, (fetching) => {
if (fetching) {
showSpinner.value = true;
debounce(() => {
showSpinner.value = false;
}, 500);
}
});
// Pagination
enum PageDirection {
Previous,
Next,
}
const page = ref(1);
const { data } = useQuery({ query: MetricsDocument });
const usersCount = computed(() => data?.value?.infra.usersCount);
const changePage = (direction: PageDirection) => {
const isPrevious = direction === PageDirection.Previous;
const isValidPreviousAction = isPrevious && page.value > 1;
const isValidNextAction = !isPrevious && page.value < totalPages.value;
if (isValidNextAction || isValidPreviousAction) {
page.value += isPrevious ? -1 : 1;
}
};
const totalPages = computed(() => {
if (!usersCount.value) return 0;
if (query.value.length > 0) {
return Math.ceil(usersList.value.length / usersPerPage);
}
return Math.ceil(usersCount.value / usersPerPage);
});
watch(page, async () => {
if (page.value < 1 || page.value > totalPages.value) {
return;
}
// Show spinner when moving to a different page when search query is present
else if (query.value.length > 0) {
showSpinner.value = true;
debounce(() => (showSpinner.value = false), 500);
} else {
await refetch({
searchString: '',
take: usersPerPage,
skip: (page.value - 1) * usersPerPage,
});
}
});
// Go to Individual User Details Page
const router = useRouter();
const goToUserDetails = (user: UserInfoQuery['infra']['userInfo']) =>
router.push('/users/' + user.uid);
// Send Invitation through Email
const showInviteUserModal = ref(false);
const sendInvitation = useMutation(InviteNewUserDocument);
const showInviteUserModal = ref(false);
const sendInvite = async (email: string) => {
if (!email.trim()) {
@@ -448,172 +227,104 @@ const sendInvite = async (email: string) => {
const variables = { inviteeEmail: email.trim() };
const result = await sendInvitation.executeMutation(variables);
if (result.error) {
if (result.error.message === USER_ALREADY_INVITED)
toast.error(t('state.user_already_invited'));
else toast.error(t('state.email_failure'));
toast.error(t('state.email_failure'));
} else {
toast.success(t('state.email_success'));
showInviteUserModal.value = false;
}
};
// Make Multiple Users Admin
const confirmUsersToAdmin = ref(false);
const usersToAdminUID = ref<string | null>(null);
const usersToAdmin = useMutation(MakeUsersAdminDocument);
// Go to Individual User Details Page
const router = useRouter();
const goToUserDetails = (uid: string) => router.push('/users/' + uid);
const AreMultipleUsersSelected = computed(() => selectedRows.value.length > 1);
const confirmUserToAdmin = (id: string | null) => {
confirmUsersToAdmin.value = true;
usersToAdminUID.value = id;
};
// Resets variables if user cancels the confirmation
const resetConfirmUserToAdmin = () => {
confirmUsersToAdmin.value = false;
usersToAdminUID.value = null;
};
const makeUsersToAdmin = async (id: string | null) => {
const userUIDs = id ? [id] : selectedRows.value.map((user) => user.uid);
const variables = { userUIDs };
const result = await usersToAdmin.executeMutation(variables);
if (result.error) {
toast.error(
id ? t('state.admin_failure') : t('state.users_to_admin_failure')
);
} else {
toast.success(
id ? t('state.admin_success') : t('state.users_to_admin_success')
);
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: userUIDs.includes(user.uid) ? true : user.isAdmin,
}));
selectedRows.value.splice(0, selectedRows.value.length);
}
confirmUsersToAdmin.value = false;
usersToAdminUID.value = null;
};
// Remove Admin Status from Multiple Users
const confirmAdminsToUsers = ref(false);
const adminsToUserUID = ref<string | null>(null);
const adminsToUser = useMutation(DemoteUsersByAdminDocument);
const confirmAdminToUser = (id: string | null) => {
confirmAdminsToUsers.value = true;
adminsToUserUID.value = id;
};
// Resets variables if user cancels the confirmation
const resetConfirmAdminToUser = () => {
confirmAdminsToUsers.value = false;
adminsToUserUID.value = null;
};
const AreMultipleUsersSelectedToAdmin = computed(
() => selectedRows.value.length > 1
);
const makeAdminsToUsers = async (id: string | null) => {
const userUIDs = id ? [id] : selectedRows.value.map((user) => user.uid);
const variables = { userUIDs };
const result = await adminsToUser.executeMutation(variables);
if (result.error) {
toast.error(
id
? t('state.remove_admin_failure')
: t('state.remove_admin_from_users_failure')
);
} else {
toast.success(
id
? t('state.remove_admin_success')
: t('state.remove_admin_from_users_success')
);
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: userUIDs.includes(user.uid) ? false : user.isAdmin,
}));
selectedRows.value.splice(0, selectedRows.value.length);
}
confirmAdminsToUsers.value = false;
adminsToUserUID.value = null;
};
// Delete Multiple Users
const confirmUsersDeletion = ref(false);
// User Deletion
const userDeletion = useMutation(RemoveUserByAdminDocument);
const confirmDeletion = ref(false);
const deleteUserUID = ref<string | null>(null);
const usersDeletion = useMutation(RemoveUsersByAdminDocument);
const confirmUserDeletion = (id: string | null) => {
confirmUsersDeletion.value = true;
const deleteUserMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(t('state.delete_user_failure'));
return;
}
const variables = { uid: id };
const result = await userDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_user_failure'));
} else {
toast.success(t('state.delete_user_success'));
usersList.value = usersList.value.filter((user) => user.uid !== id);
}
confirmDeletion.value = false;
deleteUserUID.value = null;
};
// Make User Admin
const userToAdmin = useMutation(MakeUserAdminDocument);
const confirmUserToAdmin = ref(false);
const userToAdminUID = ref<string | null>(null);
const makeUserAdmin = (id: string) => {
confirmUserToAdmin.value = true;
userToAdminUID.value = id;
};
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error(t('state.admin_failure'));
return;
}
const variables = { uid: id };
const result = await userToAdmin.executeMutation(variables);
if (result.error) {
toast.error(t('state.admin_failure'));
} else {
toast.success(t('state.admin_success'));
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: user.uid === id ? true : user.isAdmin,
}));
}
confirmUserToAdmin.value = false;
userToAdminUID.value = null;
};
// Remove Admin Status from a current Admin
const adminToUser = useMutation(RemoveUserAsAdminDocument);
const confirmAdminToUser = ref(false);
const adminToUserUID = ref<string | null>(null);
const makeAdminToUser = (id: string) => {
confirmAdminToUser.value = true;
adminToUserUID.value = id;
};
const deleteUser = (id: string) => {
confirmDeletion.value = true;
deleteUserUID.value = id;
};
// Resets variables if user cancels the confirmation
const resetConfirmUserDeletion = () => {
confirmUsersDeletion.value = false;
deleteUserUID.value = null;
};
const AreMultipleUsersSelectedForDeletion = computed(
() => selectedRows.value.length > 1
);
const deleteUsers = async (id: string | null) => {
const userUIDs = id ? [id] : selectedRows.value.map((user) => user.uid);
const variables = { userUIDs };
const result = await usersDeletion.executeMutation(variables);
if (result.error) {
const errorMessage =
result.error.message === DELETE_USER_FAILED_ONLY_ONE_ADMIN
? t('state.delete_user_failed_only_one_admin')
: id
? t('state.delete_user_failure')
: t('state.delete_users_failure');
toast.error(errorMessage);
} else {
const deletedUsers = result.data?.removeUsersByAdmin || [];
const deletedIDs = deletedUsers
.filter((user) => user.isDeleted)
.map((user) => user.userUID);
const isAdminError = deletedUsers.some(
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
);
usersList.value = usersList.value.filter(
(user) => !deletedIDs.includes(user.uid)
);
if (isAdminError) {
toast.success(
t('state.delete_some_users_success', { count: deletedIDs.length })
);
toast.error(
t('state.delete_some_users_failure', {
count: deletedUsers.length - deletedIDs.length,
})
);
toastTimeout = setTimeout(() => {
toast.error(t('state.remove_admin_for_deletion'));
}, 2000);
} else {
toast.success(
id ? t('state.delete_user_success') : t('state.delete_users_success')
);
}
selectedRows.value.splice(0, selectedRows.value.length);
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error(t('state.remove_admin_failure'));
return;
}
confirmUsersDeletion.value = false;
deleteUserUID.value = null;
const variables = { uid: id };
const result = await adminToUser.executeMutation(variables);
if (result.error) {
toast.error(t('state.remove_admin_failure'));
} else {
toast.success(t('state.remove_admin_success'));
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: user.uid === id ? false : user.isAdmin,
}));
}
confirmAdminToUser.value = false;
adminToUserUID.value = null;
};
</script>

View File

@@ -6,29 +6,24 @@
</button>
</div>
<h3 class="text-lg font-bold text-accentContrast pt-6 pb-4">
{{ t('users.pending_invites') }}
<h3 class="text-lg font-bold text-accentContrast py-6">
{{ t('users.invited_users') }}
</h3>
<div class="flex flex-col">
<div class="relative py-2 overflow-x-auto">
<div class="py-2 overflow-x-auto">
<div v-if="fetching" class="flex justify-center">
<HoppSmartSpinner />
</div>
<div v-else-if="error">
<div v-else-if="error" class="text-xl">
{{ t('users.invite_load_list_error') }}
</div>
<div v-else-if="pendingInvites?.length === 0">
{{ t('users.no_invite') }}
</div>
<HoppSmartTable
v-else
v-else-if="invitedUsers?.length"
:list="invitedUsers"
:headings="headings"
:list="pendingInvites"
:selected-rows="selectedRows"
>
<template #invitedOn="{ item }">
<div v-if="item" class="pr-2 truncate">
@@ -42,91 +37,32 @@
</div>
<span v-else> - </span>
</template>
<template #action="{ item }">
<div v-if="item" class="my-1 mr-2">
<HoppButtonSecondary
v-if="xlAndLarger"
:icon="IconTrash"
:label="t('users.revoke_invitation')"
class="text-secondaryDark bg-red-500 hover:bg-red-600"
@click="confirmInviteDeletion(item.inviteeEmail)"
/>
<HoppButtonSecondary
v-else
v-tippy="{ theme: 'tooltip' }"
:icon="IconTrash"
:title="t('users.revoke_invitation')"
class="ml-2 !text-red-500"
@click="confirmInviteDeletion(item.inviteeEmail)"
/>
</div>
</template>
</HoppSmartTable>
<div
v-if="selectedRows.length"
class="fixed m-2 bottom-0 left-40 right-0 w-min mx-auto shadow-2xl"
>
<div
class="flex justify-center items-end bg-primaryLight border border-divider rounded-md mb-5"
>
<HoppButtonSecondary
:label="t('state.selected', { count: selectedRows.length })"
class="py-4 border-divider rounded-r-none bg-emerald-800 text-secondaryDark"
/>
<HoppButtonSecondary
:icon="IconTrash"
:label="t('users.revoke_invitation')"
class="py-4 border-divider rounded-l-none hover:bg-red-500"
@click="confirmDeletion = true"
/>
</div>
</div>
<div v-else class="text-lg">{{ t('users.no_invite') }}</div>
</div>
</div>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="
selectedRows.length > 0
? t('state.confirm_delete_invites')
: t('state.confirm_delete_invite')
"
@hide-modal="confirmDeletion = false"
@resolve="deleteInvitation(inviteToBeDeleted)"
/>
</div>
</template>
<script setup lang="ts">
import { useMutation, useQuery } from '@urql/vue';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
import { useQuery } from '@urql/vue';
import { format } from 'date-fns';
import { computed, ref } from 'vue';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import IconTrash from '~icons/lucide/trash';
import {
InvitedUsersDocument,
InvitedUsersQuery,
RevokeUserInvitationsByAdminDocument,
} from '../../helpers/backend/graphql';
import { InvitedUsersDocument } from '~/helpers/backend/graphql';
const t = useI18n();
const toast = useToast();
const router = useRouter();
const breakpoints = useBreakpoints(breakpointsTailwind);
const xlAndLarger = breakpoints.greater('xl');
// Get Proper Date Formats
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MM-yyyy');
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
// Get Invited Users
const { fetching, error, data } = useQuery({ query: InvitedUsersDocument });
const invitedUsers = computed(() => data?.value?.infra.invitedUsers);
// Table Headings
const headings = [
@@ -134,56 +70,5 @@ const headings = [
{ key: 'adminEmail', label: t('users.admin_email') },
{ key: 'inviteeEmail', label: t('users.invitee_email') },
{ key: 'invitedOn', label: t('users.invited_on') },
{ key: 'action', label: 'Action' },
];
// Selected Rows
const selectedRows = ref<InvitedUsersQuery['infra']['invitedUsers']>([]);
// Invited Users
const pendingInvites = computed({
get: () => data.value?.infra.invitedUsers,
set: (value) => {
if (!value) return;
data.value!.infra.invitedUsers = value;
},
});
// Delete Invite
const confirmDeletion = ref(false);
const inviteToBeDeleted = ref<string | null>(null);
const deleteInvitationMutation = useMutation(
RevokeUserInvitationsByAdminDocument
);
const confirmInviteDeletion = (inviteeEmail: string | null) => {
confirmDeletion.value = true;
inviteToBeDeleted.value = inviteeEmail;
};
const deleteInvitation = async (email: string | null) => {
const inviteeEmails = email
? [email]
: selectedRows.value.map((row) => row.inviteeEmail);
const variables = { inviteeEmails };
const result = await deleteInvitationMutation.executeMutation(variables);
if (result.error) {
email
? toast.error(t('state.delete_invite_failure'))
: toast.error(t('state.delete_invites_failure'));
} else {
pendingInvites.value = pendingInvites.value?.filter(
(user) => !inviteeEmails.includes(user.inviteeEmail)
);
selectedRows.value.splice(0, selectedRows.value.length);
email
? toast.success(t('state.delete_invite_success'))
: toast.success(t('state.delete_invites_success'));
}
confirmDeletion.value = false;
inviteToBeDeleted.value = null;
};
</script>

2999
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff