Compare commits

...

10 Commits

Author SHA1 Message Date
Andrew Bastin
1cce117b0a chore: bump version to 2023.4.4 2023-06-02 11:06:51 -04:00
Liyas Thomas
abc7b4b6f3 chore: improve mobile responsiveness on environment selector (#3100) 2023-06-02 10:56:18 -04:00
Ankit Sridhar
05e32ef9e4 fix: update team invitation link to domain specified in .env [HBE-202] (#3096) 2023-05-31 10:36:34 -04:00
Nivedin
f0a1fc319c fix: sync popup firing multiple times (#3063) 2023-05-30 23:36:37 -04:00
Allen Zhang
385cabc6aa fix: update package.json script (#3083) 2023-05-30 17:50:47 -04:00
Liyas Thomas
397b26a9f3 chore: environment selector with new ux (#3052)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-05-30 17:47:37 -04:00
Nivedin
9a40058329 fix: set team environment from test (#3059) 2023-05-30 17:38:28 -04:00
Akash K
7ec2380ed5 chore: update wss url to ws in .env.example (#3081) 2023-05-29 20:23:02 -04:00
安正超
3d4825305d chore(i18n): Update zh-CN translations (#3068) 2023-05-29 20:19:43 -04:00
Nivedin
26e564288b feat: prettify XML response (#3079) 2023-05-29 20:18:19 -04:00
22 changed files with 813 additions and 373 deletions

View File

@@ -51,7 +51,7 @@ VITE_ADMIN_URL=http://localhost:3100
# Backend URLs # Backend URLs
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
VITE_BACKEND_WS_URL=wss://localhost:3170/graphql VITE_BACKEND_WS_URL=ws://localhost:3170/graphql
VITE_BACKEND_API_URL=http://localhost:3170/v1 VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional) # Terms Of Service And Privacy Policy Links (Optional)

View File

@@ -11,7 +11,7 @@
"dev": "pnpm -r do-dev", "dev": "pnpm -r do-dev",
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl", "gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
"generate": "pnpm -r do-build-prod", "generate": "pnpm -r do-build-prod",
"start": "http-server packages/hoppscotch-web/dist -p 3000", "start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
"lint": "pnpm -r do-lint", "lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck", "typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix", "lintfix": "pnpm -r do-lintfix",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2023.4.3", "version": "2023.4.4",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -126,7 +126,7 @@ export class TeamInvitationService {
template: 'team-invitation', template: 'team-invitation',
variables: { variables: {
invitee: creator.displayName ?? 'A Hoppscotch User', invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`, action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
invite_team_name: team.name, invite_team_name: team.name,
}, },
}), }),

View File

@@ -19,7 +19,7 @@
"edit": "编辑", "edit": "编辑",
"filter": "过滤", "filter": "过滤",
"go_back": "返回", "go_back": "返回",
"go_forward": "Go forward", "go_forward": "前进",
"group_by": "分组方式", "group_by": "分组方式",
"label": "标签", "label": "标签",
"learn_more": "了解更多", "learn_more": "了解更多",
@@ -40,9 +40,9 @@
"start": "开始", "start": "开始",
"starting": "正在开始", "starting": "正在开始",
"stop": "停止", "stop": "停止",
"to_close": "关闭", "to_close": "关闭",
"to_navigate": "定位", "to_navigate": "定位",
"to_select": "选择", "to_select": "选择",
"turn_off": "关闭", "turn_off": "关闭",
"turn_on": "开启", "turn_on": "开启",
"undo": "撤消", "undo": "撤消",
@@ -118,16 +118,16 @@
}, },
"collection": { "collection": {
"created": "集合已创建", "created": "集合已创建",
"different_parent": "Cannot reorder collection with different parent", "different_parent": "不能用不同的父类来重新排序集合",
"edit": "编辑集合", "edit": "编辑集合",
"invalid_name": "请提供有效的集合名称", "invalid_name": "请提供有效的集合名称",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "该集合已经在根级了",
"moved": "Moved Successfully", "moved": "移动完成",
"my_collections": "我的集合", "my_collections": "我的集合",
"name": "我的新集合", "name": "我的新集合",
"name_length_insufficient": "集合名字至少需要 3 个字符", "name_length_insufficient": "集合名字至少需要 3 个字符",
"new": "新建集合", "new": "新建集合",
"order_changed": "Collection Order Updated", "order_changed": "集合顺序已更新",
"renamed": "集合已更名", "renamed": "集合已更名",
"request_in_use": "请求正在使用中", "request_in_use": "请求正在使用中",
"save_as": "另存为", "save_as": "另存为",
@@ -147,7 +147,7 @@
"remove_team": "你确定要删除该团队吗?", "remove_team": "你确定要删除该团队吗?",
"remove_telemetry": "你确定要退出遥测服务吗?", "remove_telemetry": "你确定要退出遥测服务吗?",
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。", "request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "你想保存在此标签页中所作的修改吗?",
"sync": "您确定要同步该工作区吗?" "sync": "您确定要同步该工作区吗?"
}, },
"count": { "count": {
@@ -177,7 +177,7 @@
"members": "团队为空", "members": "团队为空",
"parameters": "该请求没有任何参数", "parameters": "该请求没有任何参数",
"pending_invites": "此团队无待办邀请", "pending_invites": "此团队无待办邀请",
"profile": "登录以查看你的个人档案", "profile": "登录以查看你的个人资料",
"protocols": "协议为空", "protocols": "协议为空",
"schema": "连接至 GraphQL 端点", "schema": "连接至 GraphQL 端点",
"shortcodes": "Shortcodes 为空", "shortcodes": "Shortcodes 为空",
@@ -209,7 +209,7 @@
"browser_support_sse": "该浏览器似乎不支持 SSE。", "browser_support_sse": "该浏览器似乎不支持 SSE。",
"check_console_details": "检查控制台日志以获悉详情", "check_console_details": "检查控制台日志以获悉详情",
"curl_invalid_format": "cURL 格式不正确", "curl_invalid_format": "cURL 格式不正确",
"danger_zone": "Danger zone", "danger_zone": "危险区域",
"delete_account": "您的帐号目前为这些团队的拥有者:", "delete_account": "您的帐号目前为这些团队的拥有者:",
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。", "delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
"empty_req_name": "空请求名称", "empty_req_name": "空请求名称",
@@ -219,7 +219,7 @@
"incorrect_email": "电子邮箱错误", "incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接", "invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。", "invalid_link_description": "你点击的链接无效或已过期。",
"json_parsing_failed": "Invalid JSON", "json_parsing_failed": "不合法的 JSON",
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试", "json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
"network_error": "好像发生了网络错误,请重试。", "network_error": "好像发生了网络错误,请重试。",
"network_fail": "无法发送请求", "network_fail": "无法发送请求",
@@ -316,14 +316,14 @@
"zen_mode": "ZEN 模式" "zen_mode": "ZEN 模式"
}, },
"modal": { "modal": {
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "有未保存的变更",
"collections": "集合", "collections": "集合",
"confirm": "确认", "confirm": "确认",
"edit_request": "编辑请求", "edit_request": "编辑请求",
"import_export": "导入/导出" "import_export": "导入/导出"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "您已经订阅了此主。", "already_subscribed": "您已经订阅了此主。",
"clean_session": "清除会话", "clean_session": "清除会话",
"clear_input": "清除输入", "clear_input": "清除输入",
"clear_input_on_send": "发送后清除输入", "clear_input_on_send": "发送后清除输入",
@@ -355,7 +355,7 @@
"navigation": { "navigation": {
"doc": "文档", "doc": "文档",
"graphql": "GraphQL", "graphql": "GraphQL",
"profile": "个人档案", "profile": "个人资料",
"realtime": "实时", "realtime": "实时",
"rest": "REST", "rest": "REST",
"settings": "设置" "settings": "设置"
@@ -377,7 +377,7 @@
"owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。", "owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。",
"roles": "角色", "roles": "角色",
"roles_description": "角色用以控制共享集合的访问权限。", "roles_description": "角色用以控制共享集合的访问权限。",
"updated": "档案已更新", "updated": "已更新",
"viewer": "查看者", "viewer": "查看者",
"viewer_description": "查看者只可查看与使用请求。" "viewer_description": "查看者只可查看与使用请求。"
}, },
@@ -396,8 +396,8 @@
"text": "文字" "text": "文字"
}, },
"copy_link": "复制链接", "copy_link": "复制链接",
"different_collection": "Cannot reorder requests from different collections", "different_collection": "不能对来自不同集合的请求进行重新排序",
"duplicated": "Request duplicated", "duplicated": "重复的请求",
"duration": "持续时间", "duration": "持续时间",
"enter_curl": "输入 cURL", "enter_curl": "输入 cURL",
"generate_code": "生成代码", "generate_code": "生成代码",
@@ -405,10 +405,10 @@
"header_list": "请求头列表", "header_list": "请求头列表",
"invalid_name": "请提供请求名称", "invalid_name": "请提供请求名称",
"method": "方法", "method": "方法",
"moved": "Request moved", "moved": "请求移动完成",
"name": "请求名称", "name": "请求名称",
"new": "新请求", "new": "新请求",
"order_changed": "Request Order Updated", "order_changed": "请求顺序更新完成",
"override": "覆盖", "override": "覆盖",
"override_help": "设置 <kbd>Content-Type</kbd> 头", "override_help": "设置 <kbd>Content-Type</kbd> 头",
"overriden": "覆盖", "overriden": "覆盖",
@@ -479,10 +479,10 @@
"language": "语言", "language": "语言",
"light_mode": "亮色", "light_mode": "亮色",
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。", "official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
"profile": "个人档案", "profile": "个人资料",
"profile_description": "更新你的档案详情", "profile_description": "更新你的资料",
"profile_email": "电子邮箱地址", "profile_email": "电子邮箱地址",
"profile_name": "档案名称", "profile_name": "名称",
"proxy": "网络代理", "proxy": "网络代理",
"proxy_url": "代理网址", "proxy_url": "代理网址",
"proxy_use_toggle": "使用代理中间件发送请求", "proxy_use_toggle": "使用代理中间件发送请求",
@@ -532,7 +532,7 @@
"documentation": "前往文档页面", "documentation": "前往文档页面",
"forward": "前往下一页面", "forward": "前往下一页面",
"graphql": "前往 GraphQL 页面", "graphql": "前往 GraphQL 页面",
"profile": "前往个人档案页面", "profile": "前往个人资料页面",
"realtime": "前往实时页面", "realtime": "前往实时页面",
"rest": "前往 REST 页面", "rest": "前往 REST 页面",
"settings": "前往设置页面", "settings": "前往设置页面",
@@ -574,7 +574,7 @@
}, },
"socketio": { "socketio": {
"communication": "通讯", "communication": "通讯",
"connection_not_authorized": "此SocketIO连接未使用任何验证。", "connection_not_authorized": "此 SocketIO 连接未使用任何验证。",
"event_name": "事件名称", "event_name": "事件名称",
"events": "事件", "events": "事件",
"log": "日志", "log": "日志",
@@ -614,12 +614,12 @@
"none": "无", "none": "无",
"nothing_found": "没有找到", "nothing_found": "没有找到",
"published_error": "将信息:{topic}发布至主题:{message}时发生错误", "published_error": "将信息:{topic}发布至主题:{message}时发生错误",
"published_message": "已将此信息:{message}发布至主题:{topic}", "published_message": "已将此信息:{message} 发布至主题:{topic}",
"reconnection_error": "重连失败", "reconnection_error": "重连失败",
"subscribed_failed": "无法订阅此主{topic}", "subscribed_failed": "无法订阅此主{topic}",
"subscribed_success": "成功订阅此主{topic}", "subscribed_success": "成功订阅此主{topic}",
"unsubscribed_failed": "无法取消订阅此主{topic}", "unsubscribed_failed": "无法取消订阅此主{topic}",
"unsubscribed_success": "成功取消订阅此主{topic}", "unsubscribed_success": "成功取消订阅此主{topic}",
"waiting_send_request": "等待发送请求" "waiting_send_request": "等待发送请求"
}, },
"support": { "support": {
@@ -639,7 +639,7 @@
"body": "请求体", "body": "请求体",
"collections": "集合", "collections": "集合",
"documentation": "帮助文档", "documentation": "帮助文档",
"environments": "Environments", "environments": "环境",
"headers": "请求头", "headers": "请求头",
"history": "历史记录", "history": "历史记录",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -664,7 +664,7 @@
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。", "email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
"exit": "退出团队", "exit": "退出团队",
"exit_disabled": "团队所有者无法退出团队", "exit_disabled": "团队所有者无法退出团队",
"invalid_coll_id": "Invalid collection ID", "invalid_coll_id": "无效的集合 ID",
"invalid_email_format": "电子邮箱格式无效", "invalid_email_format": "电子邮箱格式无效",
"invalid_id": "无效的团队 ID请联系你的团队者。", "invalid_id": "无效的团队 ID请联系你的团队者。",
"invalid_invite_link": "无效的邀请链接", "invalid_invite_link": "无效的邀请链接",
@@ -688,7 +688,7 @@
"member_removed": "用户已移除", "member_removed": "用户已移除",
"member_role_updated": "用户角色已更新", "member_role_updated": "用户角色已更新",
"members": "成员", "members": "成员",
"more_members": "+{count} more", "more_members": "+{count} 更多",
"name_length_insufficient": "团队名称至少为 6 个字符", "name_length_insufficient": "团队名称至少为 6 个字符",
"name_updated": "团队名称已更新", "name_updated": "团队名称已更新",
"new": "新团队", "new": "新团队",
@@ -696,13 +696,13 @@
"new_name": "我的新团队", "new_name": "我的新团队",
"no_access": "你没有编辑集合的权限", "no_access": "你没有编辑集合的权限",
"no_invite_found": "未找到邀请。请联系你的团队者。", "no_invite_found": "未找到邀请。请联系你的团队者。",
"no_request_found": "Request not found.", "no_request_found": "请求不存在",
"not_found": "没有找到团队,请联系您的团队所有者。", "not_found": "没有找到团队,请联系您的团队所有者。",
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。", "not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
"parent_coll_move": "Cannot move collection to a child collection", "parent_coll_move": "不能将集合移动到一个子集合",
"pending_invites": "待办邀请", "pending_invites": "待办邀请",
"permissions": "权限", "permissions": "权限",
"same_target_destination": "Same target and destination", "same_target_destination": "目标相同",
"saved": "团队已保存", "saved": "团队已保存",
"select_a_team": "选择团队", "select_a_team": "选择团队",
"title": "团队", "title": "团队",
@@ -732,9 +732,9 @@
"url": "URL" "url": "URL"
}, },
"workspace": { "workspace": {
"change": "Change workspace", "change": "切换工作空间",
"personal": "My Workspace", "personal": "我的工作空间",
"team": "Team Workspace", "team": "团队工作空间",
"title": "Workspaces" "title": "工作空间"
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.4.3", "version": "2023.4.4",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"dev:vite": "vite", "dev:vite": "vite",
@@ -92,6 +92,7 @@
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^4.0.15", "wonka": "^4.0.15",
"workbox-window": "^6.5.4", "workbox-window": "^6.5.4",
"xml-formatter": "^3.4.1",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -126,7 +126,7 @@
/> />
<CollectionsEditRequest <CollectionsEditRequest
:show="showModalEditRequest" :show="showModalEditRequest"
v-bind:model-value="editingRequest ? editingRequest.name : ''" :model-value="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState" :loading-state="modalLoadingState"
@submit="updateEditingRequest" @submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)" @hide-modal="displayModalEditRequest(false)"

View File

@@ -11,13 +11,14 @@
class="bg-transparent border-b border-dividerLight select-wrapper" class="bg-transparent border-b border-dividerLight select-wrapper"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="selectedEnv.type !== 'NO_ENV_SELECTED'" :icon="IconLayers"
:label="selectedEnv.name" :label="
class="flex-1 !justify-start pr-8 rounded-none" mdAndLarger
/> ? selectedEnv.type !== 'NO_ENV_SELECTED'
<HoppButtonSecondary ? selectedEnv.name
v-else : `${t('environment.select')}`
:label="`${t('environment.select')}`" : ''
"
class="flex-1 !justify-start pr-8 rounded-none" class="flex-1 !justify-start pr-8 rounded-none"
/> />
</span> </span>
@@ -46,93 +47,145 @@
} }
" "
/> />
<div v-if="environmentType === 'my-environments'" class="flex flex-col"> <HoppSmartTabs
<hr v-if="myEnvironments.length > 0" /> v-model="selectedEnvTab"
<HoppSmartItem styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary"
v-for="(gen, index) in myEnvironments" render-inactive-tabs
:key="`gen-${index}`" >
:label="gen.name" <HoppSmartTab
:info-icon="index === selectedEnv.index ? IconCheck : undefined" :id="'my-environments'"
:active-info-icon="index === selectedEnv.index" :label="`${t('environment.my_environments')}`"
@click="
() => {
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
hide()
}
"
/>
</div>
<div v-else class="flex flex-col">
<div
v-if="teamEnvLoading"
class="flex flex-col items-center justify-center p-4"
> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<hr v-if="teamEnvironmentList.length > 0" />
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="(gen, index) in myEnvironments"
:key="`gen-team-${index}`" :key="`gen-${index}`"
:label="gen.environment.name" :icon="IconLayers"
:info-icon=" :label="gen.name"
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined :info-icon="index === selectedEnv.index ? IconCheck : undefined"
" :active-info-icon="index === selectedEnv.index"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click=" @click="
() => { () => {
selectedEnvironmentIndex = { selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide() hide()
} }
" "
/> />
</div> <div
<div v-if="myEnvironments.length === 0"
v-if="!teamEnvLoading && isAdapterError" class="flex flex-col items-center justify-center text-secondaryLight"
class="flex flex-col items-center py-4" >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
> >
<icon-lucide-help-circle class="mb-4 svg-icons" /> <div
{{ errorMessage }} v-if="teamListLoading"
</div> class="flex flex-col items-center justify-center p-4"
</div> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
<div
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</template> </template>
</tippy> </tippy>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue" import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { Environment } from "@hoppscotch/data" import { useReadonlyStream, useStream } from "~/composables/stream"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useStream } from "~/composables/stream"
import { import {
environments$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const t = useI18n() const t = useI18n()
const colorMode = useColorMode()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
const props = defineProps<{ const myEnvironments = useReadonlyStream(environments$, [])
environmentType: EnvironmentType
myEnvironments: Environment[] const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
teamEnvironmentList: TeamEnvironment[]
teamEnvLoading: boolean const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
isAdapterError: boolean const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
errorMessage: GQLError<string> const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
isTeamSelected: boolean const teamEnvironmentList = useReadonlyStream(
}>() teamEnvListAdapter.teamEnvironmentList$,
[]
)
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
@@ -140,15 +193,35 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex setSelectedEnvironmentIndex
) )
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: props.myEnvironments[selectedEnvironmentIndex.value.index].name, name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = props.teamEnvironmentList.find( const teamEnv = teamEnvironmentList.value.find(
(env) => (env) =>
env.id === env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" && (selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
@@ -170,4 +243,17 @@ const selectedEnv = computed(() => {
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -4,15 +4,6 @@
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<WorkspaceCurrent :section="t('tab.environments')" /> <WorkspaceCurrent :section="t('tab.environments')" />
<EnvironmentsSelector
:environment-type="environmentType.type"
:my-environments="myEnvironments"
:team-env-loading="loading"
:team-environment-list="teamEnvironmentList"
:is-adapter-error="adapterError !== null"
:error-message="adapterError ? getErrorMessage(adapterError) : ''"
:is-team-selected="environmentType.selectedTeam !== undefined"
/>
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment
environment-index="Global" environment-index="Global"
:environment="globalEnvironment" :environment="globalEnvironment"
@@ -46,13 +37,11 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { import {
environments$,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace" import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -147,24 +136,19 @@ onLoggedIn(() => {
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Used to switch environment type and team when user switch workspace in the global workspace switcher // Switch to my environments if workspace is personal and to team environments if workspace is team
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team // also resets selected environment if workspace is personal and the previous selected environment was a team environment
// If there is no teamID, switch to my environment
watch(workspace, (newWorkspace) => { watch(workspace, (newWorkspace) => {
if (newWorkspace.type === "personal") { if (newWorkspace.type === "personal") {
switchToMyEnvironments() switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") { if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED", type: "NO_ENV_SELECTED",
}) })
} }
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
} }
}) })
@@ -207,8 +191,6 @@ defineActionHandler(
} }
) )
const myEnvironments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" }, { type: "NO_ENV_SELECTED" },
@@ -251,17 +233,4 @@ watch(
}, },
{ deep: true } { deep: true }
) )
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -165,8 +165,8 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironmentIndex: number | "Global" | null editingEnvironmentIndex?: number | "Global" | null
editingVariableName: string | null editingVariableName?: string | null
envVars?: () => Environment["variables"] envVars?: () => Environment["variables"]
}>(), }>(),
{ {

View File

@@ -140,7 +140,7 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function" import { flow, pipe } from "fp-ts/function"
import { parseTemplateStringE } from "@hoppscotch/data" import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
@@ -173,16 +173,20 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironment: TeamEnvironment | null editingEnvironment?: TeamEnvironment | null
editingTeamId: string | undefined editingTeamId: string | undefined
editingVariableName: string | null editingVariableName?: string | null
isViewer: boolean isViewer?: boolean
envVars?: () => Environment["variables"]
}>(), }>(),
{ {
show: false, show: false,
action: "edit", action: "edit",
editingEnvironment: null, editingEnvironment: null,
editingTeamId: "", editingTeamId: "",
editingVariableName: null,
isViewer: false,
envVars: () => [],
} }
) )
@@ -226,10 +230,16 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
if (props.editingEnvironment === null) { if (props.action === "new") {
name.value = null name.value = null
vars.value = [] vars.value = pipe(
} else { props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({
id: idTicker.value++,
env: clone(e),
}))
)
} else if (props.editingEnvironment !== null) {
name.value = props.editingEnvironment.environment.name ?? null name.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe( vars.value = pipe(
props.editingEnvironment.environment.variables ?? [], props.editingEnvironment.environment.variables ?? [],

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"

View File

@@ -84,6 +84,7 @@ import { useToast } from "@composables/toast"
import { isJSONContentType } from "~/helpers/utils/contenttypes" import { isJSONContentType } from "~/helpers/utils/contenttypes"
import jsonLinter from "~/helpers/editor/linting/json" import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files" import { readFileAsText } from "~/helpers/functional/files"
import xmlFormat from "xml-formatter"
type PossibleContentTypes = Exclude< type PossibleContentTypes = Exclude<
ValidContentTypes, ValidContentTypes,
@@ -197,26 +198,10 @@ const prettifyRequestBody = () => {
} }
const prettifyXML = (xml: string) => { const prettifyXML = (xml: string) => {
const PADDING = " ".repeat(2) // set desired indent size here return xmlFormat(xml, {
const reg = /(>)(<)(\/*)/g indentation: " ",
let pad = 0 collapseContent: true,
xml = xml.replace(reg, "$1\r\n$2$3") lineSeparator: "\n",
return xml })
.split("\r\n")
.map((node) => {
let indent = 0
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0
} else if (node.match(/^<\/\w/) && pad > 0) {
pad -= 1
} else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
indent = 1
} else {
indent = 0
}
pad += indent
return PADDING.repeat(pad - indent) + node
})
.join("\r\n")
} }
</script> </script>

View File

@@ -197,11 +197,20 @@
/> />
</div> </div>
<EnvironmentsMyDetails <EnvironmentsMyDetails
:show="showModalDetails" :show="showMyEnvironmentDetailsModal"
action="new" action="new"
:env-vars="getAdditionVars" :env-vars="getAdditionVars"
@hide-modal="displayModalAdd(false)" @hide-modal="displayModalAdd(false)"
/> />
<EnvironmentsTeamsDetails
:show="showTeamEnvironmentDetailsModal"
action="new"
:env-vars="getAdditionVars"
:editing-team-id="
workspace.type === 'team' ? workspace.teamID : undefined
"
@hide-modal="displayModalAdd(false)"
/>
</div> </div>
</template> </template>
@@ -225,6 +234,7 @@ import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { workspaceStatus$ } from "~/newstore/workspace"
const props = defineProps<{ const props = defineProps<{
modelValue: HoppTestResult | null | undefined modelValue: HoppTestResult | null | undefined
@@ -239,10 +249,15 @@ const testResults = useVModel(props, "modelValue", emit)
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const showModalDetails = ref(false) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const showMyEnvironmentDetailsModal = ref(false)
const showTeamEnvironmentDetailsModal = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
showModalDetails.value = shouldDisplay if (workspace.value.type === "personal")
showMyEnvironmentDetailsModal.value = shouldDisplay
else showTeamEnvironmentDetailsModal.value = shouldDisplay
} }
/** /**

View File

@@ -38,6 +38,7 @@ import {
baseHighlightStyle, baseHighlightStyle,
} from "@helpers/editor/themes/baseTheme" } from "@helpers/editor/themes/baseTheme"
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment" import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
import xmlFormat from "xml-formatter"
import { platform } from "~/platform" import { platform } from "~/platform"
// TODO: Migrate from legacy mode // TODO: Migrate from legacy mode
@@ -152,6 +153,27 @@ const getLanguage = (langMime: string): Language | null => {
return null return null
} }
/**
* Uses xml-formatter to format the XML document
* @param doc Document to parse
* @param langMime Language mime type
* @returns Parsed document if mime type is xml, else returns the original document
*/
const parseDoc = (
doc: string | undefined,
langMime: string
): string | undefined => {
if (langMime === "application/xml" && doc) {
return xmlFormat(doc, {
indentation: " ",
collapseContent: true,
lineSeparator: "\n",
})
} else {
return doc
}
}
const getEditorLanguage = ( const getEditorLanguage = (
langMime: string, langMime: string,
linter: LinterDefinition | undefined, linter: LinterDefinition | undefined,
@@ -261,7 +283,7 @@ export function useCodemirror(
view.value = new EditorView({ view.value = new EditorView({
parent: el, parent: el,
state: EditorState.create({ state: EditorState.create({
doc: value.value, doc: parseDoc(value.value, options.extendedEditorConfig.mode ?? ""),
extensions, extensions,
}), }),
}) })

View File

@@ -6,6 +6,7 @@ import { pipe, flow } from "fp-ts/function"
import { tupleToRecord } from "~/helpers/functional/record" import { tupleToRecord } from "~/helpers/functional/record"
import { safeParseJSON } from "~/helpers/functional/json" import { safeParseJSON } from "~/helpers/functional/json"
import { optionChoose } from "~/helpers/functional/option" import { optionChoose } from "~/helpers/functional/option"
import xmlFormat from "xml-formatter"
const isJSON = flow(safeParseJSON, O.isSome) const isJSON = flow(safeParseJSON, O.isSome)
@@ -213,45 +214,18 @@ export function parseBody(
*/ */
/** /**
* Prettifies XML string * Prettifies XML string using xml-formatter
* @param sourceXml The string to format * @param sourceXml The string to format
* @returns Indented XML string (uses spaces) * @returns Indented XML string (uses spaces)
*/ */
function prettifyXml(sourceXml: string) { function prettifyXml(sourceXml: string) {
return pipe( return pipe(
O.tryCatch(() => { O.tryCatch(() => {
const xmlDoc = new DOMParser().parseFromString( return xmlFormat(sourceXml, {
sourceXml, indentation: " ",
"application/xml" collapseContent: true,
) lineSeparator: "\n",
})
if (xmlDoc.querySelector("parsererror")) {
throw new Error("Unstructured Body")
}
const xsltDoc = new DOMParser().parseFromString(
[
// describes how we want to modify the XML - indent everything
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
' <xsl:strip-space elements="*"/>',
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
' <xsl:value-of select="normalize-space(.)"/>',
" </xsl:template>",
' <xsl:template match="node()|@*">',
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
" </xsl:template>",
' <xsl:output indent="yes"/>',
"</xsl:stylesheet>",
].join("\n"),
"application/xml"
)
const xsltProcessor = new XSLTProcessor()
xsltProcessor.importStylesheet(xsltDoc)
const resultDoc = xsltProcessor.transformToDocument(xmlDoc)
const resultXml = new XMLSerializer().serializeToString(resultDoc)
return resultXml
}) })
) )
} }

View File

@@ -0,0 +1,40 @@
import { distinctUntilChanged, pluck } from "rxjs"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
type SyncState = {
isInitialSync: boolean
shouldSync: boolean
}
type CurrentSyncingState = {
currentSyncingItem: SyncState
}
const initialState: CurrentSyncingState = {
currentSyncingItem: {
isInitialSync: false,
shouldSync: false,
},
}
const dispatchers = defineDispatchers({
changeCurrentSyncStatus(_, { syncItem }: { syncItem: SyncState }) {
return {
currentSyncingItem: syncItem,
}
},
})
export const currentSyncStore = new DispatchingStore(initialState, dispatchers)
export const currentSyncingStatus$ = currentSyncStore.subject$.pipe(
pluck("currentSyncingItem"),
distinctUntilChanged()
)
export function changeCurrentSyncStatus(syncItem: SyncState) {
currentSyncStore.dispatch({
dispatcher: "changeCurrentSyncStatus",
payload: { syncItem },
})
}

View File

@@ -56,6 +56,9 @@
@update:model-value="onTabUpdate" @update:model-value="onTabUpdate"
/> />
</HoppSmartWindow> </HoppSmartWindow>
<template #actions>
<EnvironmentsSelector class="h-full" />
</template>
</HoppSmartWindows> </HoppSmartWindows>
</template> </template>
<template #sidebar> <template #sidebar>
@@ -63,8 +66,8 @@
</template> </template>
</AppPaneLayout> </AppPaneLayout>
<CollectionsEditRequest <CollectionsEditRequest
:show="showRenamingReqNameModal"
v-model="reqName" v-model="reqName"
:show="showRenamingReqNameModal"
@submit="renameReqName" @submit="renameReqName"
@hide-modal="showRenamingReqNameModal = false" @hide-modal="showRenamingReqNameModal = false"
/> />
@@ -84,7 +87,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, onBeforeMount } from "vue" import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data" import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams" import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router" import { useRoute } from "vue-router"
@@ -120,6 +123,11 @@ import { useToast } from "~/composables/toast"
import { PersistableRESTTabState } from "~/helpers/rest/tab" import { PersistableRESTTabState } from "~/helpers/rest/tab"
import { watchDebounced } from "@vueuse/core" import { watchDebounced } from "@vueuse/core"
import { oauthRedirect } from "~/helpers/oauth" import { oauthRedirect } from "~/helpers/oauth"
import { useReadonlyStream } from "~/composables/stream"
import {
changeCurrentSyncStatus,
currentSyncingStatus$,
} from "~/newstore/syncing"
const savingRequest = ref(false) const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null) const confirmingCloseForTabID = ref<string | null>(null)
@@ -131,7 +139,10 @@ const toast = useToast()
const tabs = getActiveTabs() const tabs = getActiveTabs()
const confirmSync = ref(false) const confirmSync = useReadonlyStream(currentSyncingStatus$, {
isInitialSync: false,
shouldSync: true,
})
const tabStateForSync = ref<PersistableRESTTabState | null>(null) const tabStateForSync = ref<PersistableRESTTabState | null>(null)
function bindRequestToURLParams() { function bindRequestToURLParams() {
@@ -226,29 +237,6 @@ const onSaveModalClose = () => {
} }
} }
watch(confirmSync, (newValue) => {
if (newValue) {
toast.show(t("confirm.sync"), {
duration: 0,
action: [
{
text: `${t("action.yes")}`,
onClick: (_, toastObject) => {
syncTabState()
toastObject.goAway(0)
},
},
{
text: `${t("action.no")}`,
onClick: (_, toastObject) => {
toastObject.goAway(0)
},
},
],
})
}
})
const syncTabState = () => { const syncTabState = () => {
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value) if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
} }
@@ -287,6 +275,35 @@ function startTabStateSync(): Subscription {
return sub return sub
} }
const showSyncToast = () => {
toast.show(t("confirm.sync"), {
duration: 0,
action: [
{
text: `${t("action.yes")}`,
onClick: (_, toastObject) => {
syncTabState()
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: true,
})
toastObject.goAway(0)
},
},
{
text: `${t("action.no")}`,
onClick: (_, toastObject) => {
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: false,
})
toastObject.goAway(0)
},
},
],
})
}
function setupTabStateSync() { function setupTabStateSync() {
const route = useRoute() const route = useRoute()
@@ -302,9 +319,15 @@ function setupTabStateSync() {
const tabStateFromSync = const tabStateFromSync =
await platform.sync.tabState.loadTabStateFromSync() await platform.sync.tabState.loadTabStateFromSync()
if (tabStateFromSync) { if (tabStateFromSync && !confirmSync.value.isInitialSync) {
tabStateForSync.value = tabStateFromSync tabStateForSync.value = tabStateFromSync
confirmSync.value = true showSyncToast()
// Have to set isInitialSync to true here because the toast is shown
// and the user does not click on any of the actions
changeCurrentSyncStatus({
isInitialSync: true,
shouldSync: false,
})
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/selfhost-web", "name": "@hoppscotch/selfhost-web",
"private": true, "private": true,
"version": "2023.4.3", "version": "2023.4.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev:vite": "vite", "dev:vite": "vite",

View File

@@ -1,7 +1,7 @@
{ {
"name": "hoppscotch-sh-admin", "name": "hoppscotch-sh-admin",
"private": true, "private": true,
"version": "2023.4.3", "version": "2023.4.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",

View File

@@ -100,7 +100,9 @@
</div> </div>
</div> </div>
<slot name="actions" /> <div v-if="hasActions" :class="mdAndLarger ? 'w-64' : 'w-16'">
<slot name="actions" />
</div>
<input <input
type="range" type="range"
@@ -111,10 +113,10 @@
:class="{ :class="{
'!block': scrollThumb.show, '!block': scrollThumb.show,
}" }"
:style="{ :style="[
'--thumb-width': scrollThumb.width + 'px', `--thumb-width: ${scrollThumb.width}px`,
}" `width: calc(100% - ${hasActions ? mdAndLarger ? '19rem' : '7rem' : '3rem'})`,
style="width: calc(100% - 3rem)" ]"
id="myRange" id="myRange"
/> />
</div> </div>
@@ -140,8 +142,9 @@ import {
inject, inject,
watch, watch,
nextTick, nextTick,
useSlots,
} from "vue" } from "vue"
import { useElementSize } from "@vueuse/core" import { breakpointsTailwind, useBreakpoints, useElementSize } from "@vueuse/core"
import type { Slot } from "vue" import type { Slot } from "vue"
import draggable from "vuedraggable-es" import draggable from "vuedraggable-es"
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index" import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
@@ -164,6 +167,9 @@ export type TabProvider = {
removeTabEntry: (tabID: string) => void removeTabEntry: (tabID: string) => void
} }
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {} const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = withDefaults( const props = withDefaults(
@@ -191,6 +197,12 @@ const emit = defineEmits<{
(e: "addTab"): void (e: "addTab"): void
}>() }>()
const slots = useSlots()
const hasActions = computed(() => {
return !!slots.actions
})
const throwError = (message: string): never => { const throwError = (message: string): never => {
throw new Error(message) throw new Error(message)
} }

541
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff