From 125e54e1349869e767e12e26833f70412e463a40 Mon Sep 17 00:00:00 2001 From: Aofei Sheng Date: Tue, 10 Sep 2024 15:26:53 +0800 Subject: [PATCH 1/7] refactor: improve project sharing UX (#882) Fixes #847 Fixes #848 Signed-off-by: Aofei Sheng --- .../project/ProjectSharingLinkModal.vue | 71 ++++++++++++ spx-gui/src/components/project/index.ts | 108 ++++++------------ .../project/runner/RunnerContainer.vue | 8 +- spx-gui/src/components/top-nav/TopNav.vue | 4 +- spx-gui/src/components/ui/UITextInput.vue | 2 + spx-gui/src/components/ui/dialog/UIDialog.vue | 2 +- .../src/components/ui/modal/UIFormModal.vue | 32 ++++-- spx-gui/src/components/ui/modal/UIModal.vue | 10 +- 8 files changed, 142 insertions(+), 95 deletions(-) create mode 100644 spx-gui/src/components/project/ProjectSharingLinkModal.vue diff --git a/spx-gui/src/components/project/ProjectSharingLinkModal.vue b/spx-gui/src/components/project/ProjectSharingLinkModal.vue new file mode 100644 index 000000000..47e561e3c --- /dev/null +++ b/spx-gui/src/components/project/ProjectSharingLinkModal.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/spx-gui/src/components/project/index.ts b/spx-gui/src/components/project/index.ts index 6498ce6fe..0cc81a5b3 100644 --- a/spx-gui/src/components/project/index.ts +++ b/spx-gui/src/components/project/index.ts @@ -1,11 +1,12 @@ import { useRouter } from 'vue-router' -import { useModal, useConfirmDialog, useMessage } from '@/components/ui' +import { useModal, useConfirmDialog } from '@/components/ui' import { IsPublic, deleteProject } from '@/apis/project' import ProjectCreateModal from './ProjectCreateModal.vue' import ProjectOpenModal from './ProjectOpenModal.vue' -import { useI18n, type LocaleMessage } from '@/utils/i18n' +import ProjectSharingLinkModal from './ProjectSharingLinkModal.vue' +import { useI18n } from '@/utils/i18n' import type { Project } from '@/models/project' -import { getProjectEditorRoute, getProjectShareRoute } from '@/router' +import { getProjectEditorRoute } from '@/router' export function useCreateProject() { const modal = useModal(ProjectCreateModal) @@ -26,8 +27,8 @@ export function useOpenProject() { } export function useRemoveProject() { - const withConfirm = useConfirmDialog() const { t } = useI18n() + const withConfirm = useConfirmDialog() return async function removeProject(owner: string, name: string) { return withConfirm({ @@ -41,94 +42,61 @@ export function useRemoveProject() { } } -/** Copy sharing link for given project */ -export function useShareProject() { - const m = useMessage() - const { t } = useI18n() - return async function shareProject(project: Project) { - const { owner, name } = project - // TODO: the check should be unnecessary - if (owner == null || name == null) throw new Error(`owner (${owner}), name (${name}) required`) - await navigator.clipboard.writeText(`${location.origin}${getProjectShareRoute(owner, name)}`) - m.success(t({ en: 'Link copied to clipboard', zh: '分享链接已复制到剪贴板' })) +export function useCreateProjectSharingLink() { + const modal = useModal(ProjectSharingLinkModal) + + return async function createProjectSharingLink(project: Project) { + await modal({ project }) } } /** - * Save and share given project - * - save current project state + * Share given project * - make project public * - copy sharing link */ -export function useSaveAndShareProject() { +export function useShareProject() { const { t } = useI18n() const withConfirm = useConfirmDialog() - const shareProject = useShareProject() + const createProjectSharingLink = useCreateProjectSharingLink() - async function saveAndShare(project: Project) { - if (project.isPublic !== IsPublic.public || project.hasUnsyncedChanges) { - project.setPublic(IsPublic.public) - await project.saveToCloud() - } - await shareProject(project) - } - - async function saveAndShareWithConfirm(project: Project, confirmMessage: LocaleMessage) { - await withConfirm({ - title: t({ en: 'Share project', zh: '分享项目' }), - content: t(confirmMessage), - confirmHandler: () => saveAndShare(project) - }) + async function makePublic(project: Project) { + project.setPublic(IsPublic.public) + await project.saveToCloud() } - return async function saveAndShareProject(project: Project) { - const { isPublic, hasUnsyncedChanges } = project - if (isPublic == null) throw new Error('isPublic required') - - if (isPublic === IsPublic.public) { - if (!hasUnsyncedChanges) return saveAndShare(project) - else - return saveAndShareWithConfirm(project, { - en: "To share the project, we will save the project's current state to cloud", - zh: '分享操作会将当前项目状态保存到云端' - }) - } else { - if (!hasUnsyncedChanges) - return saveAndShareWithConfirm(project, { - en: 'To share the project, we will make the project public', - zh: '分享操作会将当前项目设置为公开' - }) - else - return saveAndShareWithConfirm(project, { - en: "To share the project, we will save the project's current state to cloud & make it public", - zh: '分享操作会将当前项目状态保存到云端,并将项目设置为公开' - }) + return async function shareProject(project: Project) { + if (project.isPublic !== IsPublic.public) { + await withConfirm({ + title: t({ en: 'Share project', zh: '分享项目' }), + content: t({ + en: 'To share the current project, it will be made public. Would you like to proceed?', + zh: '为了分享当前项目,它将被设置为公开。确认继续吗?' + }), + confirmHandler: () => makePublic(project) + }) } + return createProjectSharingLink(project) } } export function useStopSharingProject() { - const withConfirm = useConfirmDialog() const { t } = useI18n() + const withConfirm = useConfirmDialog() + + async function makePersonal(project: Project) { + project.setPublic(IsPublic.personal) + await project.saveToCloud() + } return async function stopSharingProject(project: Project) { - let confirmMessage = { - en: 'If sharing stopped, others will no longer have permission to access the project, and all project-sharing links will expire', - zh: '停止分享后,其他人不再可以访问项目,所有的项目分享链接也将失效' - } - if (project.hasUnsyncedChanges) { - confirmMessage = { - en: `The project's current state will be saved to cloud. ${confirmMessage.en}`, - zh: `当前项目状态将被保存到云端;${confirmMessage.zh}` - } - } return withConfirm({ title: t({ en: 'Stop sharing project', zh: '停止分享项目' }), - content: t(confirmMessage), - confirmHandler: async () => { - project.setPublic(IsPublic.personal) - await project.saveToCloud() - } + content: t({ + en: 'If sharing is stopped, others will no longer have access to the current project, and its sharing links will expire. Would you like to proceed?', + zh: '如果停止分享,其他人将无法访问当前项目,且分享链接将会失效。确认继续吗?' + }), + confirmHandler: () => makePersonal(project) }) } } diff --git a/spx-gui/src/components/project/runner/RunnerContainer.vue b/spx-gui/src/components/project/runner/RunnerContainer.vue index cf6a840e2..8a52ee1eb 100644 --- a/spx-gui/src/components/project/runner/RunnerContainer.vue +++ b/spx-gui/src/components/project/runner/RunnerContainer.vue @@ -61,7 +61,7 @@ import { onMounted, ref, type CSSProperties, watch, nextTick } from 'vue' import dayjs from 'dayjs' import type { Project } from '@/models/project' import ProjectRunner from './ProjectRunner.vue' -import { useSaveAndShareProject, useShareProject } from '@/components/project' +import { useShareProject, useCreateProjectSharingLink } from '@/components/project' import { UIButton, UIIcon, UIModalClose } from '@/components/ui' import { useMessageHandle } from '@/utils/exception' @@ -122,14 +122,14 @@ const handleRerun = () => { consoleMessages.value = [] } -const saveAndShareProject = useSaveAndShareProject() const shareProject = useShareProject() +const createProjectSharingLink = useCreateProjectSharingLink() const handleShare = useMessageHandle( () => { if (props.mode === 'debug') { - return saveAndShareProject(props.project) - } else { return shareProject(props.project) + } else { + return createProjectSharingLink(props.project) } }, { en: 'Failed to share project', zh: '分享项目失败' } diff --git a/spx-gui/src/components/top-nav/TopNav.vue b/spx-gui/src/components/top-nav/TopNav.vue index 5f6271f93..0c98b7b8e 100644 --- a/spx-gui/src/components/top-nav/TopNav.vue +++ b/spx-gui/src/components/top-nav/TopNav.vue @@ -159,7 +159,7 @@ import { useCreateProject, useOpenProject, useRemoveProject, - useSaveAndShareProject, + useShareProject, useStopSharingProject } from '@/components/project' import { useLoadFromScratchModal } from '@/components/asset' @@ -240,7 +240,7 @@ const handleImportFromScratch = useMessageHandle(() => loadFromScratchModal(prop zh: '从 Scratch 项目文件导入失败' }).fn -const shareProject = useSaveAndShareProject() +const shareProject = useShareProject() const handleShareProject = useMessageHandle(() => shareProject(props.project!), { en: 'Failed to share project', zh: '分享项目失败' diff --git a/spx-gui/src/components/ui/UITextInput.vue b/spx-gui/src/components/ui/UITextInput.vue index 7858445b6..99684dece 100644 --- a/spx-gui/src/components/ui/UITextInput.vue +++ b/spx-gui/src/components/ui/UITextInput.vue @@ -4,6 +4,7 @@ :placeholder="placeholder || ''" :value="value" :disabled="disabled" + :readonly="readonly" @update:value="(v) => emit('update:value', v)" >