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"
|
:can-collapse="true"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</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>
|
</template>
|
||||||
|
|
||||||
<PoweredByLink
|
<PoweredByLink
|
||||||
|
|
@ -146,6 +160,7 @@ const {sidebarWidth, isResizing, startResize, isMobile} = useSidebarResize()
|
||||||
const projects = computed(() => projectStore.notArchivedRootProjects as IProject[])
|
const projects = computed(() => projectStore.notArchivedRootProjects as IProject[])
|
||||||
const favoriteProjects = computed(() => projectStore.favoriteProjects as IProject[])
|
const favoriteProjects = computed(() => projectStore.favoriteProjects as IProject[])
|
||||||
const savedFilterProjects = computed(() => projectStore.savedFilterProjects as IProject[])
|
const savedFilterProjects = computed(() => projectStore.savedFilterProjects as IProject[])
|
||||||
|
const templateProjects = computed(() => projectStore.templateProjects as IProject[])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@
|
||||||
>
|
>
|
||||||
{{ $t('menu.duplicate') }}
|
{{ $t('menu.duplicate') }}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
icon="copy"
|
||||||
|
@click="saveAsTemplate"
|
||||||
|
>
|
||||||
|
{{ $t('project.template.saveAsTemplate') }}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-tooltip="isDefaultProject ? $t('menu.cantArchiveIsDefault') : ''"
|
v-tooltip="isDefaultProject ? $t('menu.cantArchiveIsDefault') : ''"
|
||||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||||
|
|
@ -140,6 +146,9 @@ import {useConfigStore} from '@/stores/config'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {PERMISSIONS} from '@/constants/permissions'
|
import {PERMISSIONS} from '@/constants/permissions'
|
||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import {success} from '@/message'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
project: IProject
|
project: IProject
|
||||||
|
|
@ -168,4 +177,18 @@ function setSubscriptionInStore(sub: ISubscription) {
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const isDefaultProject = computed(() => props.project?.id === authStore.settings.defaultProjectId)
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,14 @@
|
||||||
"text": "Select a parent project which should hold the duplicated project:",
|
"text": "Select a parent project which should hold the duplicated project:",
|
||||||
"success": "The project was successfully duplicated."
|
"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": {
|
"edit": {
|
||||||
"header": "Edit This Project",
|
"header": "Edit This Project",
|
||||||
"title": "Edit \"{project}\"",
|
"title": "Edit \"{project}\"",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,18 @@
|
||||||
:primary-disabled="project.title === ''"
|
:primary-disabled="project.title === ''"
|
||||||
@create="createProject()"
|
@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
|
<FormField
|
||||||
v-model="project.title"
|
v-model="project.title"
|
||||||
v-focus
|
v-focus
|
||||||
|
|
@ -31,14 +43,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 {useI18n} from 'vue-i18n'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
import ProjectService from '@/services/project'
|
import ProjectService from '@/services/project'
|
||||||
import ProjectModel from '@/models/project'
|
import ProjectModel from '@/models/project'
|
||||||
|
import ProjectDuplicateService from '@/services/projectDuplicateService'
|
||||||
|
import ProjectDuplicateModel from '@/models/projectDuplicateModel'
|
||||||
import CreateEdit from '@/components/misc/CreateEdit.vue'
|
import CreateEdit from '@/components/misc/CreateEdit.vue'
|
||||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||||
import FormField from '@/components/input/FormField.vue'
|
import FormField from '@/components/input/FormField.vue'
|
||||||
|
import Multiselect from '@/components/input/Multiselect.vue'
|
||||||
|
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
@ -51,6 +67,7 @@ const props = defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
useTitle(() => t('project.create.header'))
|
useTitle(() => t('project.create.header'))
|
||||||
|
|
||||||
|
|
@ -60,6 +77,9 @@ const projectService = shallowReactive(new ProjectService())
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const parentProject = ref<IProject | null>(null)
|
const parentProject = ref<IProject | null>(null)
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
|
const selectedTemplate = ref<IProject | null>(null)
|
||||||
|
|
||||||
|
const templateOptions = computed(() => projectStore.templateProjects as IProject[])
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.parentProjectId,
|
() => props.parentProjectId,
|
||||||
|
|
@ -85,8 +105,28 @@ async function createProject() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await projectStore.createProject(project)
|
if (selectedTemplate.value) {
|
||||||
success({message: t('project.create.createdSuccess')})
|
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 {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue