feat(templates): add templates sidebar, template picker, save-as-template action, and i18n keys
This commit is contained in:
parent
6b6ca25efa
commit
3de5206dd4
|
|
@ -108,6 +108,20 @@
|
|||
:can-collapse="true"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
v-if="templateProjects.length"
|
||||
class="menu"
|
||||
>
|
||||
<span class="menu-label">
|
||||
{{ $t('project.template.title') }}
|
||||
</span>
|
||||
<ProjectsNavigation
|
||||
:model-value="templateProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="true"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<PoweredByLink
|
||||
|
|
@ -146,6 +160,7 @@ const {sidebarWidth, isResizing, startResize, isMobile} = useSidebarResize()
|
|||
const projects = computed(() => projectStore.notArchivedRootProjects as IProject[])
|
||||
const favoriteProjects = computed(() => projectStore.favoriteProjects as IProject[])
|
||||
const savedFilterProjects = computed(() => projectStore.savedFilterProjects as IProject[])
|
||||
const templateProjects = computed(() => projectStore.templateProjects as IProject[])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@
|
|||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon="copy"
|
||||
@click="saveAsTemplate"
|
||||
>
|
||||
{{ $t('project.template.saveAsTemplate') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantArchiveIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
|
|
@ -140,6 +146,9 @@ import {useConfigStore} from '@/stores/config'
|
|||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {PERMISSIONS} from '@/constants/permissions'
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import {success} from '@/message'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
project: IProject
|
||||
|
|
@ -168,4 +177,18 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
|
||||
const authStore = useAuthStore()
|
||||
const isDefaultProject = computed(() => props.project?.id === authStore.settings.defaultProjectId)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
async function saveAsTemplate() {
|
||||
const templateService = new AbstractService({
|
||||
create: '/projects/{projectId}/template',
|
||||
})
|
||||
const response = await templateService.create({projectId: props.project.id})
|
||||
if (response.project) {
|
||||
projectStore.setProject(response.project)
|
||||
}
|
||||
await projectStore.loadAllProjects()
|
||||
success({message: t('project.template.saveAsTemplateSuccess')})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,14 @@
|
|||
"text": "Select a parent project which should hold the duplicated project:",
|
||||
"success": "The project was successfully duplicated."
|
||||
},
|
||||
"template": {
|
||||
"title": "Templates",
|
||||
"saveAsTemplate": "Save as Template",
|
||||
"saveAsTemplateSuccess": "Project saved as template successfully.",
|
||||
"useTemplate": "Use a template",
|
||||
"selectTemplate": "Select a template...",
|
||||
"createFromTemplate": "Creating project from template..."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit This Project",
|
||||
"title": "Edit \"{project}\"",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@
|
|||
:primary-disabled="project.title === ''"
|
||||
@create="createProject()"
|
||||
>
|
||||
<FormField
|
||||
v-if="projectStore.hasTemplates"
|
||||
:label="$t('project.template.useTemplate')"
|
||||
>
|
||||
<Multiselect
|
||||
v-model="selectedTemplate"
|
||||
:options="templateOptions"
|
||||
:placeholder="$t('project.template.selectTemplate')"
|
||||
label="title"
|
||||
track-by="id"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-model="project.title"
|
||||
v-focus
|
||||
|
|
@ -31,14 +43,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, shallowReactive, watch} from 'vue'
|
||||
import {ref, reactive, shallowReactive, computed, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import ProjectService from '@/services/project'
|
||||
import ProjectModel from '@/models/project'
|
||||
import ProjectDuplicateService from '@/services/projectDuplicateService'
|
||||
import ProjectDuplicateModel from '@/models/projectDuplicateModel'
|
||||
import CreateEdit from '@/components/misc/CreateEdit.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
import FormField from '@/components/input/FormField.vue'
|
||||
import Multiselect from '@/components/input/Multiselect.vue'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
|
@ -51,6 +67,7 @@ const props = defineProps<{
|
|||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
|
||||
useTitle(() => t('project.create.header'))
|
||||
|
||||
|
|
@ -60,6 +77,9 @@ const projectService = shallowReactive(new ProjectService())
|
|||
const projectStore = useProjectStore()
|
||||
const parentProject = ref<IProject | null>(null)
|
||||
const isSubmitting = ref(false)
|
||||
const selectedTemplate = ref<IProject | null>(null)
|
||||
|
||||
const templateOptions = computed(() => projectStore.templateProjects as IProject[])
|
||||
|
||||
watch(
|
||||
() => props.parentProjectId,
|
||||
|
|
@ -85,8 +105,28 @@ async function createProject() {
|
|||
}
|
||||
|
||||
try {
|
||||
await projectStore.createProject(project)
|
||||
success({message: t('project.create.createdSuccess')})
|
||||
if (selectedTemplate.value) {
|
||||
const duplicateService = new ProjectDuplicateService()
|
||||
const duplicate = new ProjectDuplicateModel({
|
||||
projectId: selectedTemplate.value.id,
|
||||
parentProjectId: project.parentProjectId,
|
||||
})
|
||||
const response = await duplicateService.create(duplicate)
|
||||
const newProject = response.duplicatedProject
|
||||
if (newProject) {
|
||||
if (project.title !== selectedTemplate.value.title) {
|
||||
const updatedProject = await projectService.update({...newProject, title: project.title})
|
||||
projectStore.setProject(updatedProject)
|
||||
} else {
|
||||
projectStore.setProject(newProject)
|
||||
}
|
||||
router.push({name: 'project.index', params: {projectId: newProject.id}})
|
||||
}
|
||||
success({message: t('project.create.createdSuccess')})
|
||||
} else {
|
||||
await projectStore.createProject(project)
|
||||
success({message: t('project.create.createdSuccess')})
|
||||
}
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue