{"version":3,"file":"ProjectSettingsModal.vue_vue_type_script_setup_true_lang-BuG1GlGR.js","sources":["../../../ui/src/stores/project-settings-modal-store.ts","../../../ui/src/components/ProjectThreeDotMenu.vue","../../../ui/src/components/project-settings/ProjectSettingsDetails.vue","../../../ui/src/components/project-settings/ProjectSettingsVersions.vue","../../../ui/src/components/project-settings/ProjectSettingsAuth.vue","../../../ui/src/components/project-settings/ProjectSettingsStats.vue","../../../ui/src/components/project-settings/ProjectSettingsApiProxies.vue","../../../ui/src/components/project-settings/ProjectSettingsChannels.vue","../../../ui/src/components/project-settings/ProjectSettingsModalTabs.vue","../../../ui/src/components/project-settings/ProjectSettingsModal.vue"],"sourcesContent":["import { defineStore } from 'pinia';\nimport type { ApiProxy, Channel, Project } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\n\n/**\n * Central store for the ProjectSettingsModal component.\n *\n * NOTE: Since this is a single store, there should only ever be 1 ProjectSettingsModal\n * component loaded on a page. Otherwise, all modals on the page would appear at once\n * when the modal is shown.\n */\n\ninterface State {\n show: boolean;\n project: Project | null;\n selectedTabIndex: number;\n apiProxies: ApiProxy[];\n channels: Channel[];\n}\n\nexport const useProjectSettingsModalStore = defineStore('project-settings-modal', {\n state: (): State => ({\n show: false,\n project: null,\n selectedTabIndex: 0,\n apiProxies: [],\n channels: [],\n }),\n\n actions: {\n async open(project: Project, tabIndex = 0) {\n this.project = project;\n this.selectedTabIndex = tabIndex;\n this.show = true;\n this.apiProxies = await pyscriptApi.listApiProxies();\n this.channels = await pyscriptApi.listChannels();\n },\n\n close() {\n // Reset the state back to the defaults\n this.$reset();\n },\n },\n});\n","<script setup lang=\"ts\">\nimport { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { computed, reactive } from 'vue';\nimport type { Project } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { useUserStore } from '~/stores/user-store';\nimport Spinner from '~/components/Spinner.vue';\nimport { toProjectPage } from '~/utilities/to-project-page';\nimport { useShareModalStore } from '~/stores/share-modal-store';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport { useCollectionsStore } from '~/stores/collections-store';\nimport IconTrashCan from '~icons/carbon/trash-can';\nimport IconMenuVertical from '~icons/carbon/overflow-menu-vertical';\nimport IconFork from '~icons/carbon/fork';\nimport IconSettings from '~icons/carbon/settings';\nimport IconShare from '~icons/carbon/share';\nimport IconExternalLink from '~icons/carbon/launch';\nimport IconAddToCollections from '~icons/ph/plus-circle';\nimport IconRemoveFromCollections from '~icons/ph/minus-circle';\nimport IconVersions from '~icons/system-uicons/versions';\nimport { useProjectStore } from '~/stores/project-store';\n\nconst props = defineProps<{\n project: Project;\n canDelete?: boolean;\n version?: string;\n menuBtnClassList?: string;\n}>();\n\nconst emit = defineEmits<{\n (e: 'projectDeleted', value: Project): void;\n (e: 'projectUpdated', value: Project): void;\n}>();\n\nconst router = useRouter();\nconst route = useRoute();\nconst userStore = useUserStore();\nconst shareModalStore = useShareModalStore();\nconst projectStore = useProjectStore();\nconst projectSettingsModalStore = useProjectSettingsModalStore();\nconst collectionsStore = useCollectionsStore();\n\nconst isOwner = computed(() => props.project.user_id === userStore.user?.id);\n\nconst isCollectionProjectsPage = computed(\n () => route.name === 'collection-projects' && route.params.collectionId,\n);\n\nconst state = reactive({\n isForkingProject: false,\n});\n\nasync function onFork() {\n state.isForkingProject = true;\n const forkedProject = await pyscriptApi.forkProject(props.project.id, { version: props.version });\n await router.push(toProjectPage(forkedProject));\n state.isForkingProject = false;\n}\n\nasync function onDelete(projectId: string, projectName: string) {\n // eslint-disable-next-line no-alert\n const result = confirm(`Are you sure you want to delete \"${projectName}\"?`);\n\n if (result) {\n try {\n await pyscriptApi.deleteProject(projectId);\n\n if (projectId === projectStore.project?.id) {\n projectStore.$reset();\n }\n\n // Emit the deleted project to parent\n emit('projectDeleted', props.project);\n } catch (error) {}\n }\n}\n\nfunction onShowProjectSettings(index: number) {\n projectSettingsModalStore.open(props.project, index);\n}\n\nfunction onShowCollections() {\n collectionsStore.openAddProjectToCollectionModal(props.project);\n}\n\nasync function removeProjectFromCollection() {\n // eslint-disable-next-line no-alert\n const confirmResult = confirm(\n `Are you sure you want to remove this project from the collection?`,\n );\n\n if (!confirmResult) return;\n\n await collectionsStore.updateProjectsInCollection(\n route.params.collectionId as string,\n [],\n [props.project.id],\n );\n\n emit('projectUpdated', props.project);\n}\n</script>\n\n<template>\n <Menu as=\"div\" class=\"three-dot-menu\">\n <MenuButton\n aria-label=\"Project Menu\"\n class=\"three-dot-menu__btn outline-none\"\n :class=\"menuBtnClassList\"\n >\n <IconMenuVertical aria-hidden=\"true\" class=\"text-xl\" />\n </MenuButton>\n\n <Transition name=\"animate-menu-panel\">\n <MenuItems class=\"three-dot-menu__panel --right min-w-[8rem]\">\n <MenuItem>\n <a\n class=\"three-dot-menu__item-btn\"\n :href=\"project.latest.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n :title=\"$t('buttons.view_site')\"\n >\n <IconExternalLink class=\"text-sm\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.view_site')\"\n v-text=\"$t('buttons.view_site')\"\n />\n </a>\n </MenuItem>\n\n <MenuItem v-if=\"isOwner\">\n <button class=\"three-dot-menu__item-btn\" @click=\"onShowProjectSettings(0)\">\n <IconSettings class=\"text-sm\" aria-hidden=\"true\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.project_settings')\"\n v-text=\"$t('buttons.project_settings')\"\n />\n </button>\n </MenuItem>\n\n <MenuItem v-if=\"isOwner\">\n <button\n v-if=\"isCollectionProjectsPage\"\n class=\"three-dot-menu__item-btn\"\n @click=\"removeProjectFromCollection\"\n >\n <IconRemoveFromCollections class=\"text-sm\" aria-hidden=\"true\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.remove_from_collection')\"\n v-text=\"$t('buttons.remove_from_collection')\"\n />\n </button>\n\n <button v-else class=\"three-dot-menu__item-btn\" @click=\"onShowCollections()\">\n <IconAddToCollections class=\"text-sm\" aria-hidden=\"true\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.add_to_collection')\"\n v-text=\"$t('buttons.add_to_collection')\"\n />\n </button>\n </MenuItem>\n\n <MenuItem v-if=\"isOwner\">\n <button class=\"three-dot-menu__item-btn\" @click=\"onShowProjectSettings(1)\">\n <IconVersions class=\"text-sm\" aria-hidden=\"true\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.versions')\"\n v-text=\"$t('buttons.versions')\"\n />\n </button>\n </MenuItem>\n\n <MenuItem>\n <button\n class=\"three-dot-menu__item-btn\"\n @click=\"shareModalStore.open(project, version, version === 'latest')\"\n >\n <IconShare class=\"text-sm\" aria-hidden=\"true\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.share')\"\n v-text=\"$t('buttons.share')\"\n />\n </button>\n </MenuItem>\n\n <MenuItem>\n <button class=\"three-dot-menu__item-btn\" @click=\"onFork\">\n <div class=\"flex w-4 items-center justify-center\">\n <Spinner v-if=\"state.isForkingProject\" class=\"h-4 w-4 flex-none\" />\n <IconFork v-else class=\"-rotate-90 text-sm\" />\n </div>\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.fork')\"\n v-text=\"$t('buttons.fork')\"\n />\n </button>\n </MenuItem>\n\n <!-- Disable delete option from Collection Projects page to prevent users from accidentally deleting their projects. -->\n <MenuItem v-if=\"!isCollectionProjectsPage && canDelete\">\n <button\n class=\"three-dot-menu__item-btn hover:!bg-error-600 hover:text-white\"\n @click=\"() => onDelete(project.id, project.name)\"\n >\n <IconTrashCan class=\"text-sm\" />\n <span\n class=\"three-dot-menu__item-text\"\n :data-content=\"$t('buttons.delete')\"\n v-text=\"$t('buttons.delete')\"\n />\n </button>\n </MenuItem>\n </MenuItems>\n </Transition>\n </Menu>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, reactive } from 'vue';\nimport { type Project } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { getErrorMessage } from '~/utilities/error-message';\nimport IconUpload from '~icons/carbon/upload';\n\ninterface Props {\n project: Project;\n}\nconst props = defineProps<Props>();\n\nconst emit = defineEmits<{\n (e: 'projectUpdated', value: Project): void;\n (e: 'projectDeleted', value: Project): void;\n}>();\n\nconst descriptionMaxLength = 160;\n\nconst state = reactive({\n isSubmitting: false,\n isDeleting: false,\n name: props.project.name,\n description: props.project.description,\n confirmDelete: false,\n deleteProjectError: '',\n updateProjectError: '',\n uploadedPicture: null as File | null,\n newProjectIcon: props.project.icon,\n tags: props.project.tags.join(', '),\n stats: {\n viewed: '-' as string | number,\n forked: '-' as string | number,\n shared: '-' as string | number,\n },\n});\n\nconst isDeleting = computed(() => state.isDeleting || state.confirmDelete);\nconst hasInvalidProjectName = computed(() => state.name.trim().length < 3);\n\nasync function onSubmit() {\n state.isSubmitting = true;\n\n try {\n state.updateProjectError = '';\n\n const tagsList = state.tags\n .split(',')\n .map((tag) => tag.trim())\n .filter((tag) => tag !== '');\n\n if (tagsList.length > 5) {\n state.updateProjectError = 'You can only have up to 5 tags.';\n return;\n }\n\n if (state.uploadedPicture) {\n const formData = new FormData();\n formData.append('icon', state.uploadedPicture, state.uploadedPicture.name);\n\n try {\n await pyscriptApi.uploadProjectIcon(props.project.id, formData);\n } catch (error) {\n state.updateProjectError = getErrorMessage(error);\n state.isSubmitting = false;\n return;\n }\n }\n\n const resp = await pyscriptApi.updateProject(props.project.id, {\n name: state.name.trim(),\n description: state.description.trim(),\n tags: tagsList,\n });\n\n emit('projectUpdated', resp);\n } catch (error) {\n state.updateProjectError = getErrorMessage(error);\n } finally {\n state.isSubmitting = false;\n }\n}\n\nasync function confirmDelete() {\n state.confirmDelete = true;\n}\n\nasync function deleteProject() {\n if (state.isDeleting) return;\n\n state.isDeleting = true;\n\n try {\n state.deleteProjectError = '';\n await pyscriptApi.deleteProject(props.project.id);\n emit('projectDeleted', props.project);\n } catch (error) {\n state.deleteProjectError = getErrorMessage(error);\n } finally {\n state.isDeleting = false;\n }\n}\n\nasync function cancelDelete() {\n state.confirmDelete = false;\n}\n\n/**\n * Ingests an image and converts it to a File object.\n *\n * This is used to convert the uploaded image into a .PNG file with\n * the correct dimensions, which is then uploaded to the server.\n *\n * This is done so we can be sure that the image is a PNG and also\n * so we don't upload the original file as it could contain metadata\n * and other things we don't want to expose.\n *\n * @param img The image to ingest\n * @param file The original file\n */\nfunction ingestImage(img: HTMLImageElement, file: File) {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n\n // Make the canvas has the right size\n canvas.width = img.width;\n canvas.height = img.height;\n\n if (!ctx) {\n return null;\n }\n\n ctx.drawImage(img, 0, 0);\n\n canvas?.toBlob((blob) => {\n if (!blob) {\n return;\n }\n state.uploadedPicture = new File([blob], file.name, { type: blob.type });\n state.newProjectIcon = URL.createObjectURL(state.uploadedPicture);\n }, 'image/png');\n}\n\nfunction onFileChange(e: Event) {\n if (e.target instanceof HTMLInputElement && e.target.files) {\n const file = e.target.files[0];\n // If we don't have a file, it's likely the user cancelled\n if (!file) {\n return;\n }\n\n // Only allow images to be uploaded\n if (!file.type.startsWith('image/')) {\n state.updateProjectError = 'You can only upload images for your project icon.';\n return;\n }\n\n const reader = new FileReader();\n reader.readAsDataURL(file);\n\n reader.onload = (event) => {\n const img = new Image();\n img.onload = () => {\n ingestImage(img, file);\n };\n\n if (!event.target) {\n return;\n }\n\n img.src = event.target.result as string;\n\n state.newProjectIcon = img.src;\n };\n }\n}\n\nconst classes = {\n h2: 'text-lg font-semibold leading-tight mb-5 pb-1.5 border-b border-b-new-gray-100',\n label: 'block text-sm leading-5 font-medium mb-1',\n grid: 'grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-6',\n input:\n 'ui-input dark:text-white dark:bg-transparent mt-1 w-full border-new-gray-200 dark:border-new-gray-500 dark:focus:border-new-gray-200',\n};\n</script>\n\n<template>\n <section class=\"flex max-w-2xl flex-col gap-16 sm:text-sm\">\n <form @submit.prevent=\"onSubmit\">\n <h2 :class=\"classes.h2\">General</h2>\n\n <div :class=\"classes.grid\">\n <div class=\"sm:col-span-4\">\n <label class=\"inline-block\">\n <span :class=\"classes.label\">Project Icon</span>\n\n <div\n class=\"relative mt-2 h-20 w-20 cursor-pointer rounded border border-new-gray-200 dark:border-new-gray-500 dark:bg-transparent dark:text-white dark:focus:border-new-gray-200\"\n >\n <img :src=\"state.newProjectIcon\" class=\"h-full w-full rounded object-cover\" />\n\n <div class=\"absolute inset-0 flex items-center justify-center\">\n <div class=\"rounded-full bg-black bg-opacity-50 p-2\">\n <IconUpload class=\"h-6 w-6 text-white\" />\n </div>\n </div>\n\n <input\n hidden\n type=\"file\"\n name=\"picture\"\n accept=\"image/*\"\n aria-label=\"Your project icon\"\n @change=\"onFileChange\"\n />\n </div>\n </label>\n </div>\n\n <div class=\"sm:col-span-3\">\n <label class=\"block\">\n <span :class=\"classes.label\">Project name</span>\n <input\n v-model=\"state.name\"\n type=\"text\"\n name=\"name\"\n aria-label=\"project name\"\n :class=\"classes.input\"\n />\n </label>\n\n <div v-if=\"hasInvalidProjectName\" class=\"text-xs text-error-600\">\n Must be at least 3 characters.\n </div>\n </div>\n\n <div class=\"sm:col-span-4\">\n <label class=\"block\">\n <span :class=\"classes.label\">Description</span>\n <textarea\n v-model=\"state.description\"\n type=\"text\"\n name=\"description\"\n aria-label=\"description\"\n :class=\"classes.input\"\n class=\"block h-28\"\n :maxlength=\"descriptionMaxLength\"\n />\n </label>\n\n <div class=\"text-xs\">\n Max {{ descriptionMaxLength }} characters.\n {{ descriptionMaxLength - state.description.length }} remaining.\n </div>\n </div>\n\n <div class=\"sm:col-span-4\">\n <label class=\"block\">\n <span :classes=\"classes.label\">Tags</span>\n <input\n v-model=\"state.tags\"\n type=\"text\"\n name=\"tags\"\n aria-label=\"tags\"\n :class=\"classes.input\"\n placeholder=\"pyscript-is-fun, python\"\n />\n\n <div class=\"text-xs\"> Project tags, separated by commas. </div>\n </label>\n </div>\n\n <div class=\"col-span-full flex\">\n <button\n :disabled=\"state.isSubmitting || hasInvalidProjectName\"\n class=\"btn --space-sm --primary px-4 text-sm font-medium\"\n >\n {{ state.isSubmitting ? 'Updating...' : 'Update' }}\n </button>\n </div>\n\n <div v-if=\"state.updateProjectError\" class=\"col-span-full\">\n <p class=\"text-sm text-error-600\">\n {{ state.updateProjectError }}\n </p>\n </div>\n </div>\n </form>\n\n <section>\n <h2 :class=\"classes.h2\" class=\"text-error-600\">Danger Zone</h2>\n\n <div v-if=\"!isDeleting\">\n <p class=\"mb-4\">Once you delete your project, there is no going back. Please be certain.</p>\n\n <button\n class=\"btn --space-sm --danger mt-4 whitespace-nowrap text-sm sm:block\"\n @click=\"confirmDelete\"\n >\n Delete Project\n </button>\n </div>\n\n <div v-else>\n <p>Are you sure? This action cannot be undone.</p>\n\n <div class=\"flex gap-4\">\n <button\n class=\"btn --space-sm --white mt-4 whitespace-nowrap text-sm sm:block\"\n @click=\"cancelDelete\"\n >\n Cancel\n </button>\n\n <button\n class=\"btn --space-sm --danger mt-4 whitespace-nowrap text-sm sm:block\"\n @click=\"deleteProject\"\n >\n Confirm Delete\n </button>\n </div>\n </div>\n\n <p v-if=\"state.deleteProjectError\" class=\"mt-4 text-sm text-error-600\">\n {{ state.deleteProjectError }}\n </p>\n </section>\n </section>\n</template>\n","<script setup lang=\"ts\">\nimport { onBeforeMount, reactive, ref } from 'vue';\nimport { formatTimeAgo, useDateFormat } from '@vueuse/core';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute } from 'vue-router';\nimport { type Project, type Version } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport InfoTooltip from '~/components/InfoTooltip.vue';\nimport Spinner from '~/components/Spinner.vue';\nimport { toProjectPage } from '~/utilities/to-project-page';\nimport { useShareModalStore } from '~/stores/share-modal-store';\nimport IconTrashCan from '~icons/mdi/trash-can';\nimport IconExternalLink from '~icons/ooui/link-external-ltr';\nimport IconShare from '~icons/ph/share-network-fill';\nimport IconVersion from '~icons/system-uicons/version';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport { getErrorMessage } from '~/utilities/error-message';\n\ninterface Props {\n project: Project;\n}\nconst props = defineProps<Props>();\n\nconst emit = defineEmits<{\n (e: 'defaultVersionChanged', value: Project): void;\n}>();\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst shareModalStore = useShareModalStore();\nconst projectSettingModalStore = useProjectSettingsModalStore();\nconst tableWrapperEl = ref<HTMLElement>();\n\nconst state = reactive({\n versions: [] as Version[],\n selectedDefaultVersion: props.project.default_version,\n loading: false,\n isCreatingVersion: false,\n isDeletingVersion: false,\n errorMessage: '',\n});\n\nonBeforeMount(async () => {\n try {\n await fetchVersions();\n } catch (err) {}\n});\n\nasync function fetchVersions() {\n state.loading = true;\n const versions = await pyscriptApi.listVersions(props.project.id);\n state.versions = versions.sort((a, b) => {\n if (a.version !== 'latest' && b.version === 'latest') return 1;\n if (a.version === 'latest' && b.version !== 'latest') return -1;\n return Date.parse(b.created_at) - Date.parse(a.created_at);\n });\n state.loading = false;\n}\n\nasync function deleteVersion(versionToDelete: Version) {\n // eslint-disable-next-line no-alert\n const result = confirm(\n `Are you sure you want to delete the version \"${versionToDelete.version}\"?`,\n );\n\n if (result) {\n state.isDeletingVersion = true;\n state.errorMessage = '';\n\n // Keeping a backup of our versions in case deletion fails\n const versionsStateBackup = state.versions;\n\n // Filter out the version immediately from state to provide perceived quickness to user\n state.versions = state.versions.filter((v) => v.version !== versionToDelete.version);\n\n try {\n await pyscriptApi.deleteVersion(props.project.id, versionToDelete.id);\n await fetchVersions();\n } catch (error) {\n // Reset versions to our backup\n state.versions = versionsStateBackup;\n\n state.errorMessage = getErrorMessage(error);\n } finally {\n state.isDeletingVersion = false;\n }\n }\n}\n\nasync function createVersion() {\n if (state.isCreatingVersion === true) return;\n\n state.isCreatingVersion = true;\n state.errorMessage = '';\n\n try {\n await pyscriptApi.createVersion(props.project.id);\n tableWrapperEl.value?.scrollTo({\n top: 0,\n behavior: 'smooth',\n });\n await fetchVersions();\n } catch (error) {\n state.errorMessage = getErrorMessage(error);\n } finally {\n state.isCreatingVersion = false;\n }\n}\n\nfunction deleteButtonTooltipText(version: Version) {\n if (version.version === 'latest') {\n return t('project_settings.versions.delete_disabled.latest');\n }\n\n if (version.version === props.project.default_version) {\n return t('project_settings.versions.delete_disabled.default_version');\n }\n\n if (version.version === route.params.version) {\n return t('project_settings.versions.delete_disabled.current_route');\n }\n\n return t('project_settings.versions.delete');\n}\n\nfunction formatDate(sourceDate: string) {\n return formatTimeAgo(new Date(sourceDate), {\n max: 86400000, // 1 day in milliseconds\n fullDateFormatter(date) {\n const d = useDateFormat(date, 'MMM D, YYYY', { locales: 'en-US' }).value;\n const t = useDateFormat(date, 'hh:mmA', { locales: 'en-US' }).value;\n return isYesterday(date) ? `yesterday at ${t}` : `${d} at ${t}`;\n },\n });\n}\n\nfunction isYesterday(date: Date) {\n const yesterday = new Date();\n yesterday.setDate(yesterday.getDate() - 1);\n return yesterday.toDateString() === date.toDateString();\n}\n\nasync function confirmDefaultVersionChange(event: Event) {\n if (event.target instanceof HTMLInputElement) {\n const version = event.target.value;\n\n // Don't confirm if already selected.\n if (version === state.selectedDefaultVersion) {\n return;\n }\n\n // eslint-disable-next-line no-alert\n const answer = confirm(\n `Change the default version from \"${state.selectedDefaultVersion}\" to \"${version}\"?`,\n );\n if (answer) {\n state.selectedDefaultVersion = version;\n const updatedProject = await pyscriptApi.updateProject(props.project.id, {\n default_version: version,\n });\n projectSettingModalStore.project = updatedProject;\n emit('defaultVersionChanged', updatedProject);\n } else {\n event.preventDefault();\n }\n }\n}\n\nasync function confirmPublishChange(event: Event) {\n if (event.target instanceof HTMLInputElement) {\n const version = event.target.value;\n\n // eslint-disable-next-line no-alert\n const answer = confirm(`${event.target.checked ? 'Publish' : 'Unpublish'} \"${version}\"?`);\n\n if (answer) {\n await pyscriptApi.updateVersion(props.project.id, version, {\n published: event.target.checked,\n });\n await fetchVersions();\n } else {\n event.preventDefault();\n }\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6 sm:h-full\">\n <div\n v-if=\"state.loading && !state.versions.length\"\n class=\"flex h-full items-center justify-center\"\n >\n <div class=\"dot-flashing mx-auto\" />\n </div>\n\n <template v-else>\n <section\n ref=\"tableWrapperEl\"\n class=\"scrollbar max-h-full overflow-auto rounded-xl border border-new-gray-100 text-new-gray-500 dark:border-new-gray-400 dark:text-new-gray-200\"\n >\n <table class=\"w-full text-left\">\n <thead class=\"text-sm tracking-wide\">\n <tr>\n <th scope=\"col\">{{ t('project_settings.versions.version') }}</th>\n\n <th scope=\"col\">{{ t('project_settings.versions.created') }}</th>\n\n <th scope=\"col\">\n {{ t('project_settings.versions.last_edited') }}\n </th>\n\n <th scope=\"col\">\n <div class=\"flex items-center justify-center gap-1\">\n <div>{{ t('project_settings.versions.published') }}</div>\n\n <InfoTooltip class=\"text-xs\">\n A published version allows the version to be viewable on the project's site.\n </InfoTooltip>\n </div>\n </th>\n\n <th scope=\"col\">\n <div class=\"flex items-center justify-center gap-1\">\n <div>{{ t('project_settings.versions.default') }}</div>\n\n <InfoTooltip class=\"text-xs\">\n {{ t('project_settings.versions.tooltip_info_default_1') }}\n {{ t('project_settings.versions.tooltip_info_default_2') }}\n <span class=\"break-all italic text-new-gray-200\">\n {{\n `https://${project.username || project.user_id}.pyscriptapps.com/${\n project.slug\n }`\n }}\n </span>\n {{ t('project_settings.versions.tooltip_info_default_3') }}\n </InfoTooltip>\n </div>\n </th>\n\n <th scope=\"col\">{{ t('project_settings.versions.actions') }}</th>\n </tr>\n </thead>\n\n <tbody class=\"text-sm font-medium\">\n <TransitionGroup name=\"slide\">\n <tr v-for=\"version in state.versions\" :key=\"version.id\">\n <td scope=\"row\" class=\"whitespace-nowrap\">\n <span v-if=\"version.version === route.params.version\" class=\"align-middle\">\n {{ version.version }}\n </span>\n\n <router-link\n v-else\n v-tooltip=\"{\n content: t('buttons.view_code'),\n }\"\n :to=\"toProjectPage(project, version.version)\"\n class=\"align-middle underline hover:text-new-green-600\"\n @click=\"projectSettingModalStore.close()\"\n >\n <span>{{ version.version }}</span>\n </router-link>\n\n <InfoTooltip\n v-if=\"version.version === 'latest'\"\n class=\"ml-1 inline-block align-middle text-xs\"\n >\n The <strong>latest</strong> version represents the current state of your\n project. All the most recent changes to your project directly affect it.\n </InfoTooltip>\n </td>\n\n <td>{{ formatDate(version.created_at) }}</td>\n\n <td>{{ formatDate(version.updated_at) }}</td>\n\n <td class=\"text-center\">\n <input\n type=\"checkbox\"\n :checked=\"version.published\"\n :name=\"`${version}-published`\"\n :value=\"version.version\"\n @click=\"confirmPublishChange($event)\"\n />\n </td>\n\n <td class=\"text-center\">\n <input\n v-model.lazy=\"state.selectedDefaultVersion\"\n type=\"radio\"\n name=\"default-version\"\n :value=\"version.version\"\n @click=\"confirmDefaultVersionChange($event)\"\n />\n </td>\n\n <td>\n <div\n class=\"flex gap-2 text-new-gray-600 hover:text-new-gray-900 dark:text-new-gray-100\"\n >\n <a\n v-tooltip=\"{\n content: t('buttons.view_site'),\n }\"\n class=\"circle-btn\"\n :href=\"version.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <IconExternalLink class=\"text-[11px]\" />\n </a>\n\n <button\n v-tooltip=\"{\n content: t('buttons.share'),\n }\"\n class=\"circle-btn\"\n @click=\"shareModalStore.open(project, version.version)\"\n >\n <IconShare />\n </button>\n\n <button\n v-tooltip=\"{\n content: deleteButtonTooltipText(version),\n }\"\n class=\"circle-btn\"\n :disabled=\"\n state.isDeletingVersion ||\n version.version === 'latest' ||\n version.version === project.default_version ||\n version.version === route.params.version\n \"\n @click=\"deleteVersion(version)\"\n >\n <IconTrashCan />\n </button>\n </div>\n </td>\n </tr>\n </TransitionGroup>\n </tbody>\n </table>\n </section>\n\n <section class=\"flex flex-row items-center justify-end gap-2\">\n <div class=\"mr-auto text-sm text-error-600\">\n {{ state.errorMessage }}\n </div>\n\n <button\n class=\"btn --primary flex items-center justify-center gap-1.5 whitespace-nowrap px-3 py-1.5 text-sm font-semibold\"\n :disabled=\"state.isCreatingVersion\"\n @click=\"createVersion\"\n >\n <div>{{ t('project_settings.versions.create_version') }}</div>\n <div class=\"w-4\">\n <Spinner v-if=\"state.isCreatingVersion\" class=\"h-4 w-4 flex-none\" />\n <IconVersion v-else class=\"text-base\" />\n </div>\n </button>\n\n <InfoTooltip class=\"text-xs\">\n Create a version to make a snapshot of the current state of your project. Versions can be\n viewed, but cannot be edited. Share links to versions to show off your work and to ensure\n your users aren't affected by any changes you make to your project.\n </InfoTooltip>\n </section>\n </template>\n </div>\n</template>\n\n<style scoped lang=\"postcss\">\n.scrollbar {\n &::-webkit-scrollbar {\n @apply h-3.5 w-3.5;\n }\n\n &::-webkit-scrollbar-thumb {\n @apply h-8 rounded-full border-4 border-solid border-transparent bg-gray-200 bg-clip-padding hover:bg-gray-300 dark:bg-new-gray-300 hover:dark:bg-new-gray-500;\n }\n\n &::-webkit-scrollbar-corner {\n @apply bg-transparent;\n }\n}\n\ntr {\n @apply bg-white dark:bg-new-gray-900;\n}\n\nthead tr {\n @apply sticky top-0;\n /* This shadow hack is needed to replicate a border because borders disappear when we use position: sticky and don't move properly when the row is animated */\n @apply shadow-[inset_0_-1px_0_0] shadow-new-gray-100 dark:shadow-new-gray-400;\n}\n\ntbody tr {\n @apply hover:bg-new-slate-100/60 dark:hover:bg-new-gray-950/70;\n\n &:not(:first-child) {\n /* This shadow hack is needed to replicate a border because borders don't move properly when the row is animated when adding/removing a row */\n @apply shadow-[inset_0_1px_0_0] shadow-new-gray-100 dark:shadow-new-gray-400;\n }\n}\n\ntd {\n @apply px-2 py-4;\n}\n\nth {\n @apply px-2 py-3 font-semibold;\n}\n\nth,\ntd {\n &:first-child {\n @apply pl-6;\n }\n\n &:last-child {\n @apply pr-6;\n }\n}\n\n.circle-btn {\n @apply flex h-6 w-6 items-center justify-center rounded-full border border-black/5 bg-new-gray-50 text-new-gray-600 transition-colors hover:bg-new-gray-100 disabled:opacity-60 dark:bg-new-gray-700 dark:text-new-gray-100 dark:hover:bg-new-gray-600;\n}\n\n.slide-move,\n.slide-enter-active,\n.slide-leave-to {\n @apply transition-all duration-700;\n}\n\n.slide-leave-active,\n.slide-enter-active {\n @apply bg-new-slate-100/60 dark:bg-new-gray-950/70;\n}\n\n.slide-enter-from,\n.slide-leave-to {\n @apply -translate-x-1/3 -translate-y-1/2 opacity-0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, reactive } from 'vue';\nimport type { FormKitNode } from '@formkit/core';\nimport { email } from '@formkit/rules';\nimport { reset } from '@formkit/core';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { getErrorMessage } from '~/utilities/error-message';\nimport type { Project } from '~/utilities/pyscript-api-models';\nimport IconClose from '~icons/mdi/close';\n\nconst props = defineProps<Props>();\n\nconst emit = defineEmits<{\n (e: 'projectUpdated', value: Project): void;\n}>();\n\ninterface Props {\n project: Project;\n}\n\nconst state = reactive({\n authRequired: props.project.auth_required ? 'private' : 'public',\n changingAuthRequired: false,\n authRequiredErrorMessage: '',\n});\n\nconst authRadioOptions = [\n {\n value: 'public',\n label: 'Public',\n help: 'Anyone in the world can access your site.',\n },\n {\n value: 'private',\n label: 'Private',\n help: 'Only you and the users specified below can access your site.',\n },\n];\n\nconst isPrivate = computed(() => props.project.auth_required);\n\nasync function updateAuthUsersAllowed(data: { userEmailOrUsername: string }, node: FormKitNode) {\n try {\n const auth_users_allowed = [data.userEmailOrUsername, ...props.project.auth_users_allowed];\n const updatedProject = await pyscriptApi.updateProject(props.project.id, {\n auth_users_allowed,\n });\n emit('projectUpdated', updatedProject);\n reset('auth-users-allowed-form');\n } catch (error) {\n node.setErrors([getErrorMessage(error)]);\n }\n}\n\n/**\n * Validate if the field is a valid email, or if it starts with a `@` symbol\n * it has a valid email suffix.\n */\nfunction validateEmailOrUsernameField(node: FormKitNode) {\n // If the value starts with an `@` symbol, validate the email suffix.\n // TODO: Uncomment when we allow email domains again.\n /* if (typeof node.value === 'string' && node.value.startsWith('@')) {\n const validateSuffix = /^@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i;\n return validateSuffix.test(String(node.value));\n } */\n\n // If it's a username, validate the username.\n if (typeof node.value === 'string' && !/@/.test(String(node.value))) {\n // Usernames can't be less than 3 characters.\n if (node.value.length < 3) {\n return false;\n }\n // Check if valid usernames\n const usernameRegex = /^[a-z0-9_][a-z0-9-_]*[a-z0-9_]$/;\n return usernameRegex.test(String(node.value));\n }\n\n // Else validate the email.\n return email(node);\n}\n\nfunction validateEmailDomain(node: FormKitNode) {\n // If the value starts with an `@` symbol, validate the email suffix.\n if (typeof node.value === 'string' && node.value.startsWith('@')) {\n const validateSuffix = /^@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i;\n return !validateSuffix.test(String(node.value));\n }\n\n return true;\n}\n\nasync function confirmRemoveUserEmail(email: string) {\n // eslint-disable-next-line no-alert\n const answer = confirm(`Are you sure you want to remove \"${email}\"?`);\n\n if (answer) {\n const auth_users_allowed = props.project.auth_users_allowed.filter(\n (userEmail) => userEmail !== email,\n );\n const updatedProject = await pyscriptApi.updateProject(props.project.id, {\n auth_users_allowed,\n });\n emit('projectUpdated', updatedProject);\n }\n}\n\nasync function confirmAuthRequiredChange(event: Event) {\n if (event.target instanceof HTMLInputElement) {\n const value = event.target.value;\n\n // Don't confirm if already selected.\n if (value === state.authRequired) {\n return;\n }\n\n // eslint-disable-next-line no-alert\n const answer = confirm(`Are you sure you want to make this project \"${value}\"?`);\n\n // Prevent the radio button from changing. We'll control the change via state\n\n if (answer) {\n // Prevent the radio button change until the API call is successful.\n event.preventDefault();\n\n // Update the state to disable the form while submitting\n state.changingAuthRequired = true;\n\n // Reset the error message\n state.authRequiredErrorMessage = '';\n\n try {\n const updatedProject = await pyscriptApi.updateProject(props.project.id, {\n auth_required: value === 'private',\n });\n\n state.authRequired = value;\n emit('projectUpdated', updatedProject);\n } catch (error) {\n state.authRequiredErrorMessage = getErrorMessage(error);\n } finally {\n state.changingAuthRequired = false;\n }\n } else {\n // The user cancelled the change, so prevent the radio button from changing.\n event.preventDefault();\n }\n }\n}\n</script>\n\n<template>\n <div class=\"text-sm\">\n <section>\n <h2 class=\"mb-3 text-lg font-semibold leading-7\">Site Access</h2>\n\n <section class=\"mb-4\">\n <fieldset>\n <ul\n class=\"mb-1 flex flex-col gap-3\"\n :class=\"state.changingAuthRequired ? 'pointer-events-none opacity-50' : ''\"\n >\n <li v-for=\"option in authRadioOptions\" :key=\"option.value\">\n <label class=\"inline-flex items-baseline gap-1.5\">\n <input\n v-model=\"state.authRequired\"\n type=\"radio\"\n name=\"auth-required\"\n :value=\"option.value\"\n @click=\"confirmAuthRequiredChange($event)\"\n />\n <div>\n <span>{{ option.label }}</span>\n\n <div class=\"text-xs text-gray-500 dark:text-new-gray-200\">\n {{ option.help }}\n </div>\n </div>\n </label>\n </li>\n </ul>\n </fieldset>\n\n <div v-if=\"state.authRequiredErrorMessage\" class=\"text-xs text-error-600\">\n {{ state.authRequiredErrorMessage }}\n </div>\n </section>\n </section>\n\n <section\n :class=\"[\n isPrivate\n ? ''\n : 'w-max cursor-not-allowed rounded-lg border border-black/10 bg-new-gray-50/50 p-3 opacity-75 dark:border-white/20 dark:bg-new-gray-50/10',\n ]\"\n >\n <div\n :class=\"[\n isPrivate\n ? ''\n : 'pointer-events-none contrast-75 grayscale dark:contrast-100 dark:grayscale-0',\n ]\"\n >\n <section class=\"mb-4\">\n <FormKit\n id=\"auth-users-allowed-form\"\n v-slot=\"{ state: { valid } }\"\n type=\"form\"\n :actions=\"false\"\n @submit=\"updateAuthUsersAllowed\"\n >\n <h3 class=\"formkit-label mb-1 block text-sm font-medium\">\n Add a user's email or username to grant access\n </h3>\n\n <div class=\"flex max-w-md flex-col gap-1 sm:flex-row\">\n <!-- TODO: Add back `help=\"You can also grant access to everybody in an email domain. E.g. @example.com\"` when we support email domains again. -->\n <FormKit\n type=\"text\"\n name=\"userEmailOrUsername\"\n label=\"User email or username\"\n validation=\"required|validateEmailDomain|validateEmailOrUsernameField\"\n :validation-rules=\"{ validateEmailDomain, validateEmailOrUsernameField }\"\n validation-visibility=\"blur\"\n :validation-messages=\"{\n validateEmailOrUsernameField: 'Please enter a valid email address or username.',\n validateEmailDomain:\n 'Email domains are temporarily disabled. Please enter a valid email address or username.',\n }\"\n placeholder=\"E.g. john.doe@example.com or username\"\n autocomplete=\"off\"\n autocapitalize=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n :classes=\"{\n label: 'hidden',\n wrapper: '',\n outer: '$remove:mb-4 w-full',\n }\"\n />\n\n <FormKit\n type=\"submit\"\n label=\"Add user\"\n :disabled=\"!valid\"\n :classes=\"{\n input: 'whitespace-nowrap h-[38px] -mt-[1px] border border-black/10',\n outer: '$remove:mb-4',\n }\"\n />\n </div>\n </FormKit>\n </section>\n\n <h3 class=\"mb-2 block text-sm font-medium\">People with access</h3>\n\n <template v-if=\"props.project.auth_users_allowed.length === 0\">\n <span class=\"text-new-gray-600\">Only you can access this site.</span>\n </template>\n\n <TransitionGroup\n name=\"list-fade-slide\"\n tag=\"ul\"\n class=\"flex max-w-md flex-col gap-2 break-all sm:w-max\"\n >\n <li\n v-for=\"userEmailOrUsername in props.project.auth_users_allowed\"\n :key=\"userEmailOrUsername\"\n class=\"inline-flex items-center justify-between gap-6 rounded border border-new-gray-200 bg-new-green-200 px-4 py-2 text-new-gray-700 dark:border-new-gray-700 dark:bg-new-gray-800 dark:text-new-gray-50\"\n >\n <span>{{ userEmailOrUsername }}</span>\n\n <button\n :aria-label=\"`Remove ${userEmailOrUsername}`\"\n class=\"rounded border border-transparent p-0.5 transition-colors duration-75 hover:border-black/20 hover:bg-new-green-900/15 dark:hover:bg-white/10\"\n @click=\"confirmRemoveUserEmail(userEmailOrUsername)\"\n >\n <IconClose aria-hidden=\"true\" />\n </button>\n </li>\n </TransitionGroup>\n </div>\n </section>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, reactive, watch } from 'vue';\nimport type { DateTimeFormatOptions } from '@intlify/core-base';\nimport { useUserStore } from '~/stores/user-store';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { getErrorMessage } from '~/utilities/error-message';\nimport type { Project, ProjectStats } from '~/utilities/pyscript-api-models';\nimport IconCopy from '~icons/carbon/copy';\nimport IconView from '~icons/carbon/view';\nimport IconShare from '~icons/carbon/share';\n\nconst props = defineProps<Props>();\n\ninterface Props {\n project: Project;\n}\n\nconst userStore = useUserStore();\n\nconst projectCreatedDate = computed(() => new Date(props.project.created_at));\nconst projectUpdatedDate = computed(() => new Date(props.project.updated_at));\n\nconst dateOptions: DateTimeFormatOptions = {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n};\n\nconst state = reactive({\n loading: false,\n error: '',\n stats: null as ProjectStats | null,\n});\n\nwatch(\n () => userStore.user?.id,\n async () => {\n state.loading = true;\n state.error = '';\n\n try {\n state.stats = await pyscriptApi.getProjectStats(props.project.id);\n } catch (error) {\n state.error = getErrorMessage(error);\n } finally {\n state.loading = false;\n }\n },\n { immediate: true },\n);\n</script>\n\n<template>\n <div class=\"flex max-w-[340px] flex-col gap-5 text-sm\">\n <div class=\"grid grid-cols-6 items-center gap-y-0.5\">\n <div class=\"col-span-2 font-semibold\">Date Created</div>\n <p class=\"col-span-4\">{{ projectCreatedDate.toLocaleDateString('en-US', dateOptions) }}</p>\n\n <div class=\"col-span-2 font-semibold\">Last Edited</div>\n <p class=\"col-span-4\">{{ projectUpdatedDate.toLocaleDateString('en-US', dateOptions) }}</p>\n </div>\n\n <div v-if=\"state.error\" class=\"text-error-600\">\n <span class=\"mr-1.5 font-bold\">Error retrieving stats:</span>\n <span>{{ state.error }}</span>\n </div>\n\n <div class=\"flex flex-col gap-2\">\n <div\n class=\"flex justify-between rounded border border-new-gray-200 p-2 dark:border-new-gray-500\"\n >\n <div class=\"flex items-center\">\n <IconView class=\"mr-2\" />\n <span class=\"font-medium\">Views</span>\n </div>\n\n <div>{{ state.stats?.viewed.toLocaleString() ?? '-' }}</div>\n </div>\n\n <div\n class=\"flex justify-between rounded border border-new-gray-200 p-2 dark:border-new-gray-500\"\n >\n <div class=\"flex items-center\">\n <IconCopy class=\"mr-2\" />\n <span class=\"font-medium\">Copies</span>\n </div>\n\n <div>{{ state.stats?.forked.toLocaleString() ?? '-' }}</div>\n </div>\n\n <div\n class=\"flex justify-between rounded border border-new-gray-200 p-2 dark:border-new-gray-500\"\n >\n <div class=\"flex items-center\">\n <IconShare class=\"mr-2\" />\n <span class=\"font-medium\">Shares</span>\n </div>\n\n <div>{{ state.stats?.shared.toLocaleString() ?? '-' }}</div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped lang=\"postcss\"></style>\n","<script setup lang=\"ts\">\nimport { computed, reactive } from 'vue';\nimport type { FormKitNode } from '@formkit/core';\nimport { reset } from '@formkit/core';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport { type Project } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { getErrorMessage } from '~/utilities/error-message';\nimport IconCheckmark from '~icons/ion/checkmark-circle';\n\nconst emit = defineEmits<{\n (e: 'projectUpdated', value: Project): void;\n}>();\n\nconst modalStore = useProjectSettingsModalStore();\n\nconst state = reactive({\n apiProxiesAllowed: modalStore.project!.api_proxies_allowed,\n showSuccessMessage: false,\n});\n\nconst options = computed(() =>\n modalStore.apiProxies.map((proxy) => ({\n label: proxy.name,\n value: proxy.id,\n help: proxy.description,\n })),\n);\n\nfunction toggleSuccessMessage() {\n state.showSuccessMessage = true;\n setTimeout(() => {\n state.showSuccessMessage = false;\n }, 3000);\n}\n\nasync function submitHandler(data: any, node: FormKitNode) {\n try {\n const updatedProject = await pyscriptApi.updateProject(modalStore.project!.id, {\n api_proxies_allowed: state.apiProxiesAllowed,\n });\n emit('projectUpdated', updatedProject);\n reset('allowed-api-proxies', state);\n\n state.showSuccessMessage = true;\n toggleSuccessMessage();\n } catch (error) {\n node.setErrors([getErrorMessage(error)]);\n }\n}\n</script>\n\n<template>\n <FormKit\n id=\"allowed-api-proxies\"\n v-slot=\"{ state: { dirty } }\"\n type=\"form\"\n :actions=\"false\"\n @submit=\"submitHandler\"\n >\n <FormKit\n v-model=\"state.apiProxiesAllowed\"\n type=\"checkbox\"\n name=\"apiProxiesAllowed\"\n :label=\"$t('project_settings.api_proxies.heading')\"\n :help=\"$t('project_settings.api_proxies.description')\"\n :options=\"options\"\n decorator-icon=\"check\"\n :classes=\"{\n legend: '$remove:text-sm $remove:font-medium font-semibold text-lg',\n wrapper: '$remove:mb-1 mb-0.5',\n help: '$remove:mt-1.5 $remove:text-xs text-sm',\n optionHelp: '$remove:ml-5 ml-[22px]',\n }\"\n />\n\n <div class=\"flex items-center gap-4\">\n <FormKit\n type=\"submit\"\n :disabled=\"!dirty\"\n :classes=\"{ outer: '$remove:mb-4', wrapper: '$remove:mb-1' }\"\n :label=\"$t('project_settings.api_proxies.submit')\"\n />\n\n <div\n aria-hidden=\"true\"\n class=\"flex items-center gap-1 text-xs text-new-gray-700 transition-opacity\"\n :class=\"[state.showSuccessMessage ? 'opacity-1' : 'opacity-0']\"\n >\n <IconCheckmark class=\"shrink-0 text-new-green-800\" />\n <span>{{ $t('project_settings.api_proxies.success_message') }}</span>\n </div>\n </div>\n </FormKit>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, reactive } from 'vue';\nimport type { FormKitNode } from '@formkit/core';\nimport { reset } from '@formkit/core';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport { type Project } from '~/utilities/pyscript-api-models';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport { getErrorMessage } from '~/utilities/error-message';\nimport IconCheckmark from '~icons/ion/checkmark-circle';\n\nconst emit = defineEmits<{\n (e: 'projectUpdated', value: Project): void;\n}>();\n\nconst modalStore = useProjectSettingsModalStore();\n\nconst state = reactive({\n channelsAllowed: modalStore.project!.channels_allowed,\n showSuccessMessage: false,\n});\n\nconst options = computed(() =>\n modalStore.channels.map((proxy) => ({\n label: proxy.name,\n value: proxy.id,\n help: proxy.description,\n })),\n);\n\nfunction toggleSuccessMessage() {\n state.showSuccessMessage = true;\n setTimeout(() => {\n state.showSuccessMessage = false;\n }, 3000);\n}\n\nasync function submitHandler(data: any, node: FormKitNode) {\n try {\n const updatedProject = await pyscriptApi.updateProject(modalStore.project!.id, {\n channels_allowed: state.channelsAllowed,\n });\n emit('projectUpdated', updatedProject);\n reset('allowed-channels-form', state);\n\n state.showSuccessMessage = true;\n toggleSuccessMessage();\n } catch (error) {\n node.setErrors([getErrorMessage(error)]);\n }\n}\n</script>\n\n<template>\n <FormKit\n id=\"allowed-channels-form\"\n v-slot=\"{ state: { dirty } }\"\n type=\"form\"\n :actions=\"false\"\n @submit=\"submitHandler\"\n >\n <FormKit\n v-model=\"state.channelsAllowed\"\n type=\"checkbox\"\n name=\"channelsAllowed\"\n :label=\"$t('project_settings.channels.heading')\"\n :help=\"$t('project_settings.channels.description')\"\n :options=\"options\"\n decorator-icon=\"check\"\n :classes=\"{\n decoratorIcon: '$remove:p-[1px] p-[2px]',\n legend: '$remove:text-sm $remove:font-medium font-semibold text-lg',\n wrapper: '$remove:mb-1 mb-0.5',\n help: '$remove:mt-1.5 $remove:text-xs text-sm',\n optionHelp: '$remove:ml-5 ml-[22px]',\n }\"\n />\n\n <div class=\"flex items-center gap-4\">\n <FormKit\n type=\"submit\"\n :disabled=\"!dirty\"\n :classes=\"{ outer: '$remove:mb-4', wrapper: '$remove:mb-1' }\"\n :label=\"$t('project_settings.channels.submit')\"\n />\n\n <div\n aria-hidden=\"true\"\n class=\"flex items-center gap-1 text-xs text-new-gray-700 transition-opacity\"\n :class=\"[state.showSuccessMessage ? 'opacity-1' : 'opacity-0']\"\n >\n <IconCheckmark class=\"shrink-0 text-new-green-800\" />\n <span>{{ $t('project_settings.channels.success_message') }}</span>\n </div>\n </div>\n </FormKit>\n</template>\n","<script setup lang=\"ts\">\nimport { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/vue';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport type { Project } from '~/utilities/pyscript-api-models';\nimport ProjectSettingsDetails from '~/components/project-settings/ProjectSettingsDetails.vue';\nimport ProjectSettingsVersions from '~/components/project-settings/ProjectSettingsVersions.vue';\nimport ProjectSettingsAuth from '~/components/project-settings/ProjectSettingsAuth.vue';\nimport ProjectSettingsStats from '~/components/project-settings/ProjectSettingsStats.vue';\nimport ProjectSettingsApiProxies from '~/components/project-settings/ProjectSettingsApiProxies.vue';\nimport ProjectSettingsChannels from '~/components/project-settings/ProjectSettingsChannels.vue';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport { useUserStore } from '~/stores/user-store';\n\ndefineEmits<{\n (e: 'projectDeleted', value: Project): void;\n (e: 'projectUpdated', value: Project): void;\n (e: 'projectAuthUpdated', value: Project): void;\n (e: 'defaultVersionChanged', value: Project): void;\n}>();\n\nconst { t } = useI18n();\nconst modalStore = useProjectSettingsModalStore();\nconst userStore = useUserStore();\n\nconst TAB_ID_USER_DETAILS = 'details';\nconst TAB_ID_VERSIONS = 'versions';\nconst TAB_ID_API_STATS = 'stats';\nconst TAB_ID_API_PROXIES = 'api-proxies';\nconst TAB_ID_API_PERMISSIONS = 'permissions';\nconst TAB_ID_CHANNELS = 'channels';\n\nconst tabOrder = [\n TAB_ID_USER_DETAILS,\n TAB_ID_VERSIONS,\n TAB_ID_API_STATS,\n TAB_ID_API_PROXIES,\n TAB_ID_API_PERMISSIONS,\n TAB_ID_CHANNELS,\n];\n\nconst tabList = computed(() => {\n const tabs = [\n { id: TAB_ID_USER_DETAILS, label: t('project_settings.tabs.details') },\n { id: TAB_ID_VERSIONS, label: t('project_settings.tabs.versions') },\n { id: TAB_ID_API_STATS, label: t('project_settings.tabs.stats') },\n { id: TAB_ID_API_PROXIES, label: t('project_settings.tabs.api_proxies') },\n { id: TAB_ID_CHANNELS, label: t('project_settings.tabs.channels') },\n ];\n\n if (userStore.canUsePrivateProjects) {\n tabs.push({ id: TAB_ID_API_PERMISSIONS, label: t('project_settings.tabs.permissions') });\n }\n\n tabs.sort((a, b) => tabOrder.indexOf(a.id) - tabOrder.indexOf(b.id));\n\n return tabs;\n});\n</script>\n\n<template>\n <TabGroup\n v-if=\"modalStore.project\"\n :selected-index=\"modalStore.selectedTabIndex\"\n @change=\"(index) => (modalStore.selectedTabIndex = index)\"\n >\n <TabList\n class=\"custom-scrollbar flex flex-shrink-0 flex-nowrap gap-6 overflow-auto border-b border-t border-new-gray-100/40 bg-new-gray-50/60 px-4 py-2.5 dark:border-transparent dark:bg-new-gray-950/80 sm:px-6\"\n >\n <Tab v-for=\"tabItem in tabList\" v-slot=\"{ selected }\" :key=\"tabItem.id\" as=\"template\">\n <button\n class=\"block text-base font-semibold leading-tight outline-none\"\n :class=\"[\n selected\n ? 'border-y-2 border-b-new-green-500 border-t-transparent text-new-content-900 dark:text-new-content-100'\n : 'border-y-2 border-y-transparent text-gray-500 hover:text-gray-600 dark:text-gray-300',\n ]\"\n >\n <div class=\"py-1\">{{ tabItem.label }}</div>\n </button>\n </Tab>\n </TabList>\n\n <TabPanels class=\"custom-scrollbar mx-2 basis-full overflow-auto py-4 sm:px-4\">\n <template v-for=\"tabItem in tabList\" :key=\"tabItem.id\">\n <TabPanel v-if=\"tabItem.id === TAB_ID_USER_DETAILS\" :unmount=\"false\" class=\"outline-none\">\n <ProjectSettingsDetails\n :project=\"modalStore.project\"\n @project-deleted=\"$emit('projectDeleted', $event)\"\n @project-updated=\"$emit('projectUpdated', $event)\"\n />\n </TabPanel>\n\n <TabPanel\n v-if=\"tabItem.id === TAB_ID_VERSIONS\"\n :unmount=\"false\"\n class=\"h-full outline-none\"\n >\n <ProjectSettingsVersions\n :project=\"modalStore.project\"\n @default-version-changed=\"$emit('defaultVersionChanged', $event)\"\n />\n </TabPanel>\n\n <TabPanel\n v-if=\"tabItem.id === TAB_ID_API_STATS\"\n :unmount=\"false\"\n class=\"h-full outline-none\"\n >\n <ProjectSettingsStats :project=\"modalStore.project\" />\n </TabPanel>\n\n <TabPanel\n v-if=\"tabItem.id === TAB_ID_API_PROXIES\"\n :unmount=\"false\"\n class=\"h-full outline-none\"\n >\n <ProjectSettingsApiProxies @project-updated=\"$emit('projectUpdated', $event)\" />\n </TabPanel>\n\n <TabPanel\n v-if=\"tabItem.id === TAB_ID_API_PERMISSIONS\"\n :unmount=\"false\"\n class=\"h-full outline-none\"\n >\n <ProjectSettingsAuth\n :project=\"modalStore.project\"\n @project-updated=\"$emit('projectAuthUpdated', $event)\"\n />\n </TabPanel>\n\n <TabPanel\n v-if=\"tabItem.id === TAB_ID_CHANNELS\"\n :unmount=\"false\"\n class=\"h-full outline-none\"\n >\n <ProjectSettingsChannels @project-updated=\"$emit('projectUpdated', $event)\" />\n </TabPanel>\n </template>\n </TabPanels>\n </TabGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';\nimport { onBeforeUnmount } from 'vue';\nimport { onBeforeRouteUpdate } from 'vue-router';\nimport type { Project } from '~/utilities/pyscript-api-models';\nimport { useProjectSettingsModalStore } from '~/stores/project-settings-modal-store';\nimport ProjectSettingsModalTabs from '~/components/project-settings/ProjectSettingsModalTabs.vue';\nimport IconSettings from '~icons/carbon/settings';\nimport IconClose from '~icons/mdi/close';\n\nconst props = defineProps<{\n /**\n * Runs a function when the modal is closed.\n *\n * For example, on a project page we want to keep the versions dropdown in sync.\n */\n beforeUnmount?: (...args: any[]) => void;\n}>();\n\ndefineEmits<{\n (e: 'projectDeleted', value: Project): void;\n (e: 'projectUpdated', value: Project): void;\n (e: 'projectAuthUpdated', value: Project): void;\n (e: 'defaultVersionChanged', value: Project): void;\n}>();\n\nconst modalStore = useProjectSettingsModalStore();\n\nfunction closeModal() {\n modalStore.close();\n}\n\n/**\n * Close the modal any time the route changes.\n *\n * Example cases when this may occur:\n * - General tab's project name changes, causing the route to change to match the new slug.\n * - Version tab's view project link is clicked\n */\nonBeforeRouteUpdate(() => {\n closeModal();\n});\n\nonBeforeUnmount(() => {\n if (props.beforeUnmount) {\n props.beforeUnmount();\n }\n});\n</script>\n\n<template>\n <TransitionRoot v-if=\"modalStore.project\" appear :show=\"modalStore.show\" as=\"template\">\n <Dialog as=\"div\" class=\"relative z-20\" @close=\"closeModal\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-300 ease-out\"\n enter-from=\"opacity-0\"\n enter-to=\"opacity-100\"\n leave=\"duration-200 ease-in\"\n leave-from=\"opacity-100\"\n leave-to=\"opacity-0\"\n >\n <div class=\"fixed inset-0 bg-black bg-opacity-25 dark:bg-opacity-40\" />\n </TransitionChild>\n\n <div class=\"fixed inset-0\">\n <div class=\"flex h-full items-stretch justify-center p-2 sm:p-6\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-300 ease-out\"\n enter-from=\"opacity-0 scale-95\"\n enter-to=\"opacity-100 scale-100\"\n leave=\"duration-200 ease-in\"\n leave-from=\"opacity-100 scale-100\"\n leave-to=\"opacity-0 scale-95\"\n >\n <DialogPanel\n class=\"flex h-full w-full max-w-3xl transform flex-col rounded-lg bg-white py-4 shadow-xl transition-all dark:bg-new-gray-900\"\n >\n <DialogTitle as=\"h2\" class=\"mb-3 mt-0.5 px-4 sm:px-6\">\n <div\n class=\"mb-1.5 flex items-center gap-1 text-new-gray-600 dark:text-new-gray-300\"\n >\n <IconSettings class=\"shrink-0 rounded-full text-[11px]\" />\n\n <div class=\"text-[11px] font-semibold uppercase leading-none\">\n Project Settings\n </div>\n </div>\n\n <div class=\"text-xl font-medium\">{{ modalStore.project.name }}</div>\n </DialogTitle>\n\n <button\n type=\"button\"\n title=\"Close modal\"\n class=\"absolute right-4 top-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\"\n @click=\"closeModal\"\n >\n <IconClose aria-hidden=\"true\" />\n </button>\n\n <ProjectSettingsModalTabs\n @project-deleted=\"$emit('projectDeleted', $event)\"\n @project-updated=\"$emit('projectUpdated', $event)\"\n @project-auth-updated=\"$emit('projectAuthUpdated', $event)\"\n @default-version-changed=\"$emit('defaultVersionChanged', $event)\"\n />\n </DialogPanel>\n </TransitionChild>\n </div>\n </div>\n </Dialog>\n </TransitionRoot>\n</template>\n"],"names":["useProjectSettingsModalStore","defineStore","project","tabIndex","pyscriptApi","props","__props","emit","__emit","router","useRouter","route","useRoute","userStore","useUserStore","shareModalStore","useShareModalStore","projectStore","useProjectStore","projectSettingsModalStore","collectionsStore","useCollectionsStore","isOwner","computed","_a","isCollectionProjectsPage","state","reactive","onFork","forkedProject","toProjectPage","onDelete","projectId","projectName","onShowProjectSettings","index","onShowCollections","removeProjectFromCollection","descriptionMaxLength","isDeleting","hasInvalidProjectName","onSubmit","tagsList","tag","formData","error","getErrorMessage","resp","confirmDelete","deleteProject","cancelDelete","ingestImage","img","file","canvas","ctx","blob","onFileChange","e","reader","event","classes","useI18n","projectSettingModalStore","tableWrapperEl","ref","onBeforeMount","fetchVersions","versions","a","b","deleteVersion","versionToDelete","versionsStateBackup","v","createVersion","deleteButtonTooltipText","version","formatDate","sourceDate","formatTimeAgo","date","d","useDateFormat","t","isYesterday","yesterday","confirmDefaultVersionChange","updatedProject","confirmPublishChange","authRadioOptions","isPrivate","updateAuthUsersAllowed","data","node","auth_users_allowed","reset","validateEmailOrUsernameField","email","validateEmailDomain","confirmRemoveUserEmail","userEmail","confirmAuthRequiredChange","value","projectCreatedDate","projectUpdatedDate","dateOptions","watch","modalStore","options","proxy","toggleSuccessMessage","submitHandler","TAB_ID_USER_DETAILS","TAB_ID_VERSIONS","TAB_ID_API_STATS","TAB_ID_API_PROXIES","TAB_ID_API_PERMISSIONS","TAB_ID_CHANNELS","tabOrder","tabList","tabs","closeModal","onBeforeRouteUpdate","onBeforeUnmount"],"mappings":"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"}