Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/app/src/components/content/ContentEditorConflict.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { ref, computed, type PropType } from 'vue'
import type { ContentConflict, DraftItem } from '../../types'
import { useMonacoDiff } from '../../composables/useMonacoDiff'
import { useStudio } from '../../composables/useStudio'
import { ContentFileExtension } from '../../types'
import { joinURL } from 'ufo'
import { ContentFileExtension, StudioFeature } from '../../types'

const props = defineProps({
draftItem: {
Expand All @@ -19,7 +18,7 @@ const diffEditorRef = ref<HTMLDivElement>()

const conflict = computed<ContentConflict>(() => props.draftItem.conflict!)
const repositoryInfo = computed(() => gitProvider.api.getRepositoryInfo())
const fileRemoteUrl = computed(() => joinURL(gitProvider.api.getContentRootDirUrl(), props.draftItem.fsPath))
const fileRemoteUrl = computed(() => gitProvider.api.getFileUrl(StudioFeature.Content, props.draftItem.fsPath))

const language = computed(() => {
switch (props.draftItem.fsPath.split('.').pop()) {
Expand Down
25 changes: 21 additions & 4 deletions src/app/src/components/shared/item/ItemActionsDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const props = defineProps({
type: Object as PropType<TreeItem>,
required: true,
},
extraActions: {
type: Array as PropType<DropdownMenuItem[]>,
default: () => [],
},
})

const isOpen = ref(false)
Expand Down Expand Up @@ -44,11 +48,11 @@ const getPendingActionLabel = (action: StudioAction<StudioItemActionId> | null)
return t('studio.actions.confirmAction', { action: t(`studio.actions.verbs.${verb}`, verb) })
}

const actions = computed<DropdownMenuItem[]>(() => {
const actions = computed<DropdownMenuItem[][]>(() => {
const hasPendingAction = pendingAction.value !== null
const hasLoadingAction = loadingAction.value !== null

return computeItemActions(context.itemActions.value, props.item, context.currentFeature.value).map((action) => {
const itemActions = computeItemActions(context.itemActions.value, props.item, context.currentFeature.value).map((action) => {
const isOneStepAction = oneStepActions.includes(action.id)
const isPending = pendingAction.value?.id === action.id
const isLoading = loadingAction.value?.id === action.id
Expand Down Expand Up @@ -79,11 +83,16 @@ const actions = computed<DropdownMenuItem[]>(() => {

// For two-step actions, execute it without confirmation
if (!isOneStepAction) {
// Navigate into folder before adding form creation
if (props.item.type === 'directory' && [StudioItemActionId.CreateDocument, StudioItemActionId.CreateDocumentFolder, StudioItemActionId.CreateMediaFolder].includes(action.id)) {
// Navigate into folder before adding form creation
context.activeTree.value.selectItemByFsPath(props.item.fsPath)
}

// Navigate to parent folder if needed before renaming
if (action.id === StudioItemActionId.RenameItem && context.activeTree.value.currentItem.value.fsPath === props.item.fsPath) {
context.activeTree.value.selectParentByFsPath(props.item.fsPath)
}

action.handler!(props.item)
return
}
Expand All @@ -107,7 +116,15 @@ const actions = computed<DropdownMenuItem[]>(() => {
}
},
}
})
}) as DropdownMenuItem[]

const groups: DropdownMenuItem[][] = [itemActions]

if (props.extraActions.length > 0) {
groups.push(props.extraActions)
}

return groups
})

const pendingActionLabel = computed(() => getPendingActionLabel(pendingAction.value))
Expand Down
123 changes: 24 additions & 99 deletions src/app/src/components/shared/item/ItemActionsToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computeItemActions, oneStepActions } from '../../../utils/context'
import { useStudio } from '../../../composables/useStudio'
import type { StudioAction } from '../../../types'
import { StudioItemActionId } from '../../../types'
import { StudioItemActionId, TreeStatus } from '../../../types'
import { MEDIA_EXTENSIONS } from '../../../utils/file'
import type { DropdownMenuItem } from '@nuxt/ui/runtime/components/DropdownMenu.vue.d.ts'
import { useI18n } from 'vue-i18n'

const { context } = useStudio()
const { context, gitProvider } = useStudio()
const { t } = useI18n()
const fileInputRef = ref<HTMLInputElement>()
const toolbarRef = ref<HTMLElement>()
Expand All @@ -23,44 +23,25 @@ watch(context.actionInProgress, (action) => {

const item = computed(() => context.activeTree.value.currentItem.value)

const getActionTooltip = (action: StudioAction<StudioItemActionId>, isPending: boolean) => {
if (isPending) {
const verb = action.id.split('-')[0]
return t('studio.actions.confirmAction', { action: t(`studio.actions.verbs.${verb}`, verb) })
}
return action.tooltip ? t(action.tooltip) : t(action.label)
}

const actions = computed(() => {
const hasPendingAction = pendingAction.value !== null
const hasLoadingAction = loadingAction.value !== null

return computeItemActions(context.itemActions.value, item.value, context.currentFeature.value).map((action) => {
const isOneStepAction = oneStepActions.includes(action.id)
const isPending = pendingAction.value?.id === action.id
const isLoading = loadingAction.value?.id === action.id
const isDeleteAction = action.id === StudioItemActionId.DeleteItem

let icon = action.icon
if (isLoading) {
icon = 'i-ph-circle-notch'
}
else if (isPending) {
icon = isDeleteAction ? 'i-ph-x' : 'i-ph-check'
const extraActions = computed<DropdownMenuItem[]>(() => {
const actions: DropdownMenuItem[] = []

if (item.value.type === 'file' && item.value.status !== TreeStatus.Created) {
const providerInfo = gitProvider.api.getRepositoryInfo()
const provider = providerInfo.provider
const feature = context.currentFeature.value

if (feature) {
actions.push({
label: t(`studio.actions.labels.openGitProvider`, { providerName: gitProvider.name }),
icon: provider === 'gitlab' ? 'i-simple-icons:gitlab' : 'i-simple-icons:github',
to: gitProvider.api.getFileUrl(feature, item.value.fsPath),
target: '_blank',
})
}
}

return {
...action,
color: isPending ? (isDeleteAction ? 'error' : 'secondary') : 'neutral',
variant: isPending ? 'soft' : 'ghost',
icon,
tooltip: getActionTooltip(action, isPending),
disabled: (hasPendingAction && !isPending) || hasLoadingAction,
isOneStepAction,
isPending,
isLoading,
}
})
return actions
})

const handleFileSelection = (event: Event) => {
Expand All @@ -75,49 +56,6 @@ const handleFileSelection = (event: Event) => {
}
}

const actionHandler = (action: StudioAction<StudioItemActionId> & { isPending?: boolean, isOneStepAction?: boolean, isLoading?: boolean }, event: Event) => {
// Stop propagation to prevent click outside handler from triggering
event.stopPropagation()

// Don't allow action if already loading
if (action.isLoading) {
return
}

if (action.id === StudioItemActionId.UploadMedia) {
fileInputRef.value?.click()
return
}

const targetItem = item.value

// For two-steps actions, execute without confirmation
if (!action.isOneStepAction) {
if (action.id === StudioItemActionId.RenameItem) {
// Navigate to parent since rename form is displayed in the parent tree
context.activeTree.value.selectParentByFsPath(targetItem.fsPath)
}

action.handler!(targetItem)
return
}

// Second click on pending action - execute it
if (action.isPending) {
loadingAction.value = action
action.handler!(targetItem)
pendingAction.value = null
}
// Click on different action while one is pending - cancel pending state
else if (pendingAction.value !== null) {
pendingAction.value = null
}
// First click - enter pending state
else {
pendingAction.value = action
}
}

const handleClickOutside = () => {
if (pendingAction.value !== null && loadingAction.value === null) {
pendingAction.value = null
Expand All @@ -138,23 +76,10 @@ onUnmounted(() => {
ref="toolbarRef"
class="flex items-center -mr-1"
>
<UTooltip
v-for="action in actions"
:key="action.id"
:text="action.tooltip"
:open="action.isPending ? true : undefined"
>
<UButton
:key="action.id"
:icon="action.icon"
:disabled="action.disabled"
size="sm"
:color="action.color as never"
:variant="action.variant as never"
:loading="action.isLoading"
@click="actionHandler(action, $event)"
/>
</UTooltip>
<ItemActionsDropdown
:item="item"
:extra-actions="extraActions"
/>

<input
ref="fileInputRef"
Expand Down
3 changes: 2 additions & 1 deletion src/app/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@
"uploadMedia": "Upload media",
"duplicateItem": "Duplicate",
"revertItem": "Revert changes",
"publishBranch": "Publish branch"
"publishBranch": "Publish branch",
"openGitProvider": "Open on {providerName}"
},
"tooltips": {
"createDocument": "Create a new file",
Expand Down
3 changes: 2 additions & 1 deletion src/app/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@
"uploadMedia": "Téléverser un média",
"duplicateItem": "Dupliquer",
"revertItem": "Annuler les changements",
"publishBranch": "Publier la branche"
"publishBranch": "Publier la branche",
"openGitProvider": "Ouvrir sur {providerName}"
},
"tooltips": {
"createDocument": "Créer un nouveau fichier",
Expand Down
3 changes: 2 additions & 1 deletion src/app/src/types/git.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DraftStatus } from './draft'
import type { StudioFeature } from '../types'

export type GitProviderType = 'github' | 'gitlab'

Expand Down Expand Up @@ -48,7 +49,7 @@ export interface GitProviderAPI {
getRepositoryUrl(): string
getBranchUrl(): string
getCommitUrl(sha: string): string
getContentRootDirUrl(): string
getFileUrl(feature: StudioFeature, fsPath: string): string
getRepositoryInfo(): { owner: string, repo: string, branch: string, provider: GitProviderType | null }
}

Expand Down
9 changes: 6 additions & 3 deletions src/app/src/utils/providers/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ofetch } from 'ofetch'
import { joinURL } from 'ufo'
import type { GitOptions, GitProviderAPI, GitFile, RawFile, CommitResult, CommitFilesOptions } from '../../types'
import { StudioFeature } from '../../types'
import { DraftStatus } from '../../types/draft'

export function createGitHubProvider(options: GitOptions): GitProviderAPI {
Expand Down Expand Up @@ -165,8 +166,10 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
return `https://github.com/${owner}/${repo}/commit/${sha}`
}

function getContentRootDirUrl() {
return `https://github.com/${owner}/${repo}/tree/${branch}/${rootDir}/content`
function getFileUrl(feature: StudioFeature, fsPath: string) {
const featureDir = feature === StudioFeature.Content ? 'content' : 'public'
const fullPath = joinURL(rootDir, featureDir, fsPath)
return `https://github.com/${owner}/${repo}/blob/${branch}/${fullPath}`
}

function getRepositoryInfo() {
Expand All @@ -184,7 +187,7 @@ export function createGitHubProvider(options: GitOptions): GitProviderAPI {
getRepositoryUrl,
getBranchUrl,
getCommitUrl,
getContentRootDirUrl,
getFileUrl,
getRepositoryInfo,
}
}
9 changes: 6 additions & 3 deletions src/app/src/utils/providers/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ofetch } from 'ofetch'
import { joinURL } from 'ufo'
import type { GitOptions, GitProviderAPI, GitFile, RawFile, CommitResult, CommitFilesOptions } from '../../types'
import { DraftStatus } from '../../types/draft'
import { StudioFeature } from '../../types'

export function createGitLabProvider(options: GitOptions): GitProviderAPI {
const { owner, repo, token, branch, rootDir, authorName, authorEmail, instanceUrl = 'https://gitlab.com' } = options
Expand Down Expand Up @@ -143,8 +144,10 @@ export function createGitLabProvider(options: GitOptions): GitProviderAPI {
return `${instanceUrl}/${owner}/${repo}/-/commit/${sha}`
}

function getContentRootDirUrl() {
return `${instanceUrl}/${owner}/${repo}/-/tree/${branch}/${rootDir}/content`
function getFileUrl(feature: StudioFeature, fsPath: string) {
const featureDir = feature === StudioFeature.Content ? 'content' : 'public'
const fullPath = joinURL(rootDir, featureDir, fsPath)
return `${instanceUrl}/${owner}/${repo}/-/blob/${branch}/${fullPath}`
}

function getRepositoryInfo() {
Expand All @@ -162,7 +165,7 @@ export function createGitLabProvider(options: GitOptions): GitProviderAPI {
getRepositoryUrl,
getBranchUrl,
getCommitUrl,
getContentRootDirUrl,
getFileUrl,
getRepositoryInfo,
}
}
2 changes: 1 addition & 1 deletion src/app/src/utils/providers/null.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function createNullProvider(_options: GitOptions): GitProviderAPI {
getRepositoryUrl: () => '',
getBranchUrl: () => '',
getCommitUrl: () => '',
getContentRootDirUrl: () => '',
getFileUrl: (_feature, _fsPath) => '',
getRepositoryInfo: () => ({ owner: '', repo: '', branch: '', provider: null }),
}
}
Loading