Initial commit: WordPress wp-content (themes, plugins, languages)
- Theme: momentry (custom theme with REST API routes) - Plugins: code-snippets (contains all API proxies) - Languages: zh_TW translations - Excludes: cache, backups, uploads, logs
This commit is contained in:
39
plugins/code-snippets/js/hooks/useAxios.ts
Normal file
39
plugins/code-snippets/js/hooks/useAxios.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useMemo } from 'react'
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance, AxiosResponse, CreateAxiosDefaults } from 'axios'
|
||||
|
||||
export interface AxiosAPI {
|
||||
get: <T>(url: string) => Promise<AxiosResponse<T, never>>
|
||||
post: <T, D>(url: string, data?: D) => Promise<AxiosResponse<T, D>>
|
||||
del: <T>(url: string) => Promise<AxiosResponse<T, never>>
|
||||
axiosInstance: AxiosInstance
|
||||
}
|
||||
|
||||
const debugRequest = async <T, D = never>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
url: string,
|
||||
doRequest: Promise<AxiosResponse<T, D>>,
|
||||
data?: D
|
||||
): Promise<AxiosResponse<T, D>> => {
|
||||
console.debug(`${method} ${url}`, ...data ? [data] : [])
|
||||
const response = await doRequest
|
||||
console.debug('Response', response)
|
||||
return response
|
||||
}
|
||||
|
||||
export const useAxios = (defaultConfig: CreateAxiosDefaults): AxiosAPI => {
|
||||
const axiosInstance = useMemo(() => axios.create(defaultConfig), [defaultConfig])
|
||||
|
||||
return useMemo((): AxiosAPI => ({
|
||||
get: <T>(url: string): Promise<AxiosResponse<T, never>> =>
|
||||
debugRequest('GET', url, axiosInstance.get<T, AxiosResponse<T, never>, never>(url)),
|
||||
|
||||
post: <T, D>(url: string, data?: D) =>
|
||||
debugRequest('POST', url, axiosInstance.post<T, AxiosResponse<T, D>, D>(url, data), data),
|
||||
|
||||
del: <T>(url: string) =>
|
||||
debugRequest('DELETE', url, axiosInstance.delete<T, AxiosResponse<T, never>, never>(url)),
|
||||
|
||||
axiosInstance
|
||||
}), [axiosInstance])
|
||||
}
|
||||
87
plugins/code-snippets/js/hooks/useFileUploadAPI.ts
Normal file
87
plugins/code-snippets/js/hooks/useFileUploadAPI.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useAxios } from './useAxios'
|
||||
import type { AxiosResponse, CreateAxiosDefaults } from 'axios'
|
||||
|
||||
export interface FileUploadRequest {
|
||||
files: FileList
|
||||
}
|
||||
|
||||
export interface FileParseResponse {
|
||||
snippets: ImportableSnippet[]
|
||||
total_count: number
|
||||
message: string
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
export interface ImportableSnippet {
|
||||
id?: number
|
||||
name: string
|
||||
desc?: string
|
||||
description?: string
|
||||
code: string
|
||||
tags?: string[]
|
||||
scope?: string
|
||||
source_file?: string
|
||||
table_data: {
|
||||
id: number | string
|
||||
title: string
|
||||
scope: string
|
||||
tags: string
|
||||
description: string
|
||||
type: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SnippetImportRequest {
|
||||
snippets: ImportableSnippet[]
|
||||
duplicate_action: 'ignore' | 'replace' | 'skip'
|
||||
network?: boolean
|
||||
}
|
||||
|
||||
export interface SnippetImportResponse {
|
||||
imported: number
|
||||
imported_ids: number[]
|
||||
message: string
|
||||
}
|
||||
|
||||
const ROUTE_BASE = `${window.CODE_SNIPPETS?.restAPI.base}code-snippets/v1/`
|
||||
|
||||
const AXIOS_CONFIG: CreateAxiosDefaults = {
|
||||
headers: { 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce }
|
||||
}
|
||||
|
||||
export interface FileUploadAPI {
|
||||
parseFiles: (request: FileUploadRequest) => Promise<AxiosResponse<FileParseResponse>>
|
||||
importSnippets: (request: SnippetImportRequest) => Promise<AxiosResponse<SnippetImportResponse>>
|
||||
}
|
||||
|
||||
export const useFileUploadAPI = (): FileUploadAPI => {
|
||||
const { axiosInstance } = useAxios(AXIOS_CONFIG)
|
||||
|
||||
return useMemo((): FileUploadAPI => ({
|
||||
parseFiles: (request: FileUploadRequest) => {
|
||||
const formData = new FormData()
|
||||
|
||||
for (let i = 0; i < request.files.length; i++) {
|
||||
formData.append('files[]', request.files[i])
|
||||
}
|
||||
|
||||
return axiosInstance.post<FileParseResponse>(
|
||||
`${ROUTE_BASE}file-upload/parse`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
importSnippets: (request: SnippetImportRequest) => {
|
||||
return axiosInstance.post<SnippetImportResponse>(
|
||||
`${ROUTE_BASE}file-upload/import`,
|
||||
request
|
||||
)
|
||||
}
|
||||
}), [axiosInstance])
|
||||
}
|
||||
52
plugins/code-snippets/js/hooks/useImportersAPI.ts
Normal file
52
plugins/code-snippets/js/hooks/useImportersAPI.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useAxios } from './useAxios'
|
||||
import type { AxiosResponse, CreateAxiosDefaults } from 'axios'
|
||||
|
||||
export interface Importer {
|
||||
name: string
|
||||
title: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export interface ImportableSnippet {
|
||||
id: number
|
||||
title: string
|
||||
table_data: {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImportRequest {
|
||||
ids: number[]
|
||||
network?: boolean
|
||||
auto_add_tags?: boolean
|
||||
tag_value?: string
|
||||
}
|
||||
|
||||
export interface ImportResponse {
|
||||
imported: number[]
|
||||
}
|
||||
|
||||
const ROUTE_BASE = `${window.CODE_SNIPPETS?.restAPI.base}code-snippets/v1/`
|
||||
|
||||
const AXIOS_CONFIG: CreateAxiosDefaults = {
|
||||
headers: { 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce }
|
||||
}
|
||||
|
||||
export interface ImportersAPI {
|
||||
fetchAll: () => Promise<AxiosResponse<Importer[]>>
|
||||
fetchSnippets: (importerName: string) => Promise<AxiosResponse<ImportableSnippet[]>>
|
||||
importSnippets: (importerName: string, request: ImportRequest) => Promise<AxiosResponse<ImportResponse>>
|
||||
}
|
||||
|
||||
export const useImportersAPI = (): ImportersAPI => {
|
||||
const { get, post } = useAxios(AXIOS_CONFIG)
|
||||
|
||||
return useMemo((): ImportersAPI => ({
|
||||
fetchAll: () => get<Importer[]>(`${ROUTE_BASE}importers`),
|
||||
fetchSnippets: (importerName: string) => get<ImportableSnippet[]>(`${ROUTE_BASE}${importerName}`),
|
||||
importSnippets: (importerName: string, request: ImportRequest) =>
|
||||
post<ImportResponse, ImportRequest>(`${ROUTE_BASE}${importerName}/import`, request)
|
||||
}), [get, post])
|
||||
}
|
||||
60
plugins/code-snippets/js/hooks/useRestAPI.tsx
Normal file
60
plugins/code-snippets/js/hooks/useRestAPI.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import axios from 'axios'
|
||||
import { createContextHook } from '../utils/hooks'
|
||||
import { REST_API_AXIOS_CONFIG } from '../utils/restAPI'
|
||||
import { buildSnippetsAPI } from '../utils/snippets/api'
|
||||
import type { SnippetsAPI } from '../utils/snippets/api'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios'
|
||||
|
||||
export interface RestAPIContext {
|
||||
api: RestAPI
|
||||
snippetsAPI: SnippetsAPI
|
||||
axiosInstance: AxiosInstance
|
||||
}
|
||||
|
||||
export interface RestAPI {
|
||||
get: <T>(url: string) => Promise<T>
|
||||
post: <T>(url: string, data?: object) => Promise<T>
|
||||
put: <T>(url: string, data?: object) => Promise<T>
|
||||
del: <T>(url: string) => Promise<T>
|
||||
}
|
||||
|
||||
const debugRequest = async <T, D = never>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
url: string,
|
||||
doRequest: Promise<AxiosResponse<T, D>>,
|
||||
data?: D
|
||||
): Promise<T> => {
|
||||
console.debug(`${method} ${url}`, ...data ? [data] : [])
|
||||
const response = await doRequest
|
||||
console.debug('Response', response)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const buildRestAPI = (axiosInstance: AxiosInstance): RestAPI => ({
|
||||
get: <T, >(url: string): Promise<T> =>
|
||||
debugRequest('GET', url, axiosInstance.get<T, AxiosResponse<T, never>, never>(url)),
|
||||
|
||||
post: <T, >(url: string, data?: object): Promise<T> =>
|
||||
debugRequest('POST', url, axiosInstance.post<T, AxiosResponse<T, typeof data>, typeof data>(url, data), data),
|
||||
|
||||
del: <T, >(url: string): Promise<T> =>
|
||||
debugRequest('DELETE', url, axiosInstance.delete<T, AxiosResponse<T, never>, never>(url)),
|
||||
|
||||
put: <T, >(url: string, data?: object): Promise<T> =>
|
||||
debugRequest('PUT', url, axiosInstance.put<T, AxiosResponse<T, typeof data>, typeof data>(url, data), data)
|
||||
})
|
||||
|
||||
export const [RestAPIContext, useRestAPI] = createContextHook<RestAPIContext>('RestAPI')
|
||||
|
||||
export const WithRestAPIContext: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const axiosInstance = useMemo(() => axios.create(REST_API_AXIOS_CONFIG), [])
|
||||
|
||||
const api = useMemo(() => buildRestAPI(axiosInstance), [axiosInstance])
|
||||
const snippetsAPI = useMemo(() => buildSnippetsAPI(api), [api])
|
||||
|
||||
const value: RestAPIContext = { api, snippetsAPI, axiosInstance }
|
||||
|
||||
return <RestAPIContext.Provider value={value}>{children}</RestAPIContext.Provider>
|
||||
}
|
||||
69
plugins/code-snippets/js/hooks/useSnippetForm.tsx
Normal file
69
plugins/code-snippets/js/hooks/useSnippetForm.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { isAxiosError } from 'axios'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { createContextHook } from '../utils/hooks'
|
||||
import { isLicensed } from '../utils/screen'
|
||||
import { isProSnippet } from '../utils/snippets/snippets'
|
||||
import type { Dispatch, PropsWithChildren, SetStateAction } from 'react'
|
||||
import type { ScreenNotice } from '../types/ScreenNotice'
|
||||
import type { Snippet } from '../types/Snippet'
|
||||
import type { CodeEditorInstance } from '../types/WordPressCodeEditor'
|
||||
|
||||
export interface SnippetFormContext {
|
||||
snippet: Snippet
|
||||
isWorking: boolean
|
||||
isReadOnly: boolean
|
||||
setSnippet: Dispatch<SetStateAction<Snippet>>
|
||||
updateSnippet: Dispatch<SetStateAction<Snippet>>
|
||||
setIsWorking: Dispatch<SetStateAction<boolean>>
|
||||
currentNotice: ScreenNotice | undefined
|
||||
setCurrentNotice: Dispatch<SetStateAction<ScreenNotice | undefined>>
|
||||
codeEditorInstance: CodeEditorInstance | undefined
|
||||
handleRequestError: (error: unknown, message?: string) => void
|
||||
setCodeEditorInstance: Dispatch<SetStateAction<CodeEditorInstance | undefined>>
|
||||
}
|
||||
|
||||
export const [SnippetFormContext, useSnippetForm] = createContextHook<SnippetFormContext>('SnippetForm')
|
||||
|
||||
export interface WithSnippetFormContextProps extends PropsWithChildren {
|
||||
initialSnippet: () => Snippet
|
||||
}
|
||||
|
||||
export const WithSnippetFormContext: React.FC<WithSnippetFormContextProps> = ({ children, initialSnippet }) => {
|
||||
const [snippet, setSnippet] = useState<Snippet>(initialSnippet)
|
||||
const [isWorking, setIsWorking] = useState(false)
|
||||
const [currentNotice, setCurrentNotice] = useState<ScreenNotice>()
|
||||
const [codeEditorInstance, setCodeEditorInstance] = useState<CodeEditorInstance>()
|
||||
|
||||
const isReadOnly = useMemo(() => !isLicensed() && isProSnippet({ scope: snippet.scope }), [snippet.scope])
|
||||
|
||||
const handleRequestError = useCallback((error: unknown, message?: string) => {
|
||||
console.error('Request failed', error)
|
||||
setIsWorking(false)
|
||||
setCurrentNotice(['error', [message, isAxiosError(error) ? error.message : ''].filter(Boolean).join(' ')])
|
||||
}, [setIsWorking, setCurrentNotice])
|
||||
|
||||
const updateSnippet: Dispatch<SetStateAction<Snippet>> = useCallback((value: SetStateAction<Snippet>) => {
|
||||
setSnippet(previous => {
|
||||
const updated = 'object' === typeof value ? value : value(previous)
|
||||
codeEditorInstance?.codemirror.setValue(updated.code)
|
||||
window.tinymce?.activeEditor.setContent(updated.desc)
|
||||
return updated
|
||||
})
|
||||
}, [codeEditorInstance?.codemirror])
|
||||
|
||||
const value: SnippetFormContext = {
|
||||
snippet,
|
||||
isWorking,
|
||||
isReadOnly,
|
||||
setSnippet,
|
||||
setIsWorking,
|
||||
updateSnippet,
|
||||
currentNotice,
|
||||
setCurrentNotice,
|
||||
codeEditorInstance,
|
||||
handleRequestError,
|
||||
setCodeEditorInstance
|
||||
}
|
||||
|
||||
return <SnippetFormContext.Provider value={value}>{children}</SnippetFormContext.Provider>
|
||||
}
|
||||
42
plugins/code-snippets/js/hooks/useSnippetsList.tsx
Normal file
42
plugins/code-snippets/js/hooks/useSnippetsList.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { createContextHook } from '../utils/hooks'
|
||||
import { isNetworkAdmin } from '../utils/screen'
|
||||
import { useRestAPI } from './useRestAPI'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { Snippet } from '../types/Snippet'
|
||||
|
||||
export interface SnippetsListContext {
|
||||
snippetsList: readonly Snippet[] | undefined
|
||||
refreshSnippetsList: () => Promise<void>
|
||||
}
|
||||
|
||||
const [SnippetsListContext, useSnippetsList] = createContextHook<SnippetsListContext>('SnippetsList')
|
||||
|
||||
export const WithSnippetsListContext: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { snippetsAPI: { fetchAll } } = useRestAPI()
|
||||
const [snippetsList, setSnippetsList] = useState<Snippet[]>()
|
||||
|
||||
const refreshSnippetsList = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
console.info('Fetching snippets list')
|
||||
const response = await fetchAll(isNetworkAdmin())
|
||||
setSnippetsList(response)
|
||||
} catch (error: unknown) {
|
||||
console.error('Error fetching snippets list', error)
|
||||
}
|
||||
}, [fetchAll])
|
||||
|
||||
useEffect(() => {
|
||||
refreshSnippetsList()
|
||||
.catch(() => undefined)
|
||||
}, [refreshSnippetsList])
|
||||
|
||||
const value: SnippetsListContext = {
|
||||
snippetsList,
|
||||
refreshSnippetsList
|
||||
}
|
||||
|
||||
return <SnippetsListContext.Provider value={value}>{children}</SnippetsListContext.Provider>
|
||||
}
|
||||
|
||||
export { useSnippetsList }
|
||||
125
plugins/code-snippets/js/hooks/useSubmitSnippet.ts
Normal file
125
plugins/code-snippets/js/hooks/useSubmitSnippet.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { addQueryArgs } from '@wordpress/url'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { useCallback } from 'react'
|
||||
import { createSnippetObject, isCondition } from '../utils/snippets/snippets'
|
||||
import { useRestAPI } from './useRestAPI'
|
||||
import { useSnippetForm } from './useSnippetForm'
|
||||
import type { Snippet } from '../types/Snippet'
|
||||
|
||||
const snippetMessages = <const> {
|
||||
addNew: __('Add New Snippet', 'code-snippets'),
|
||||
edit: __('Edit Snippet', 'code-snippets'),
|
||||
created: __('Snippet <strong>created</strong>.', 'code-snippets'),
|
||||
updated: __('Snippet <strong>updated</strong>.', 'code-snippets'),
|
||||
createdActivated: __('Snippet <strong>created</strong> and <strong>activated</strong>.', 'code-snippets'),
|
||||
updatedActivated: __('Snippet <strong>updated</strong> and <strong>activated</strong>.', 'code-snippets'),
|
||||
updatedDeactivated: __('Snippet <strong>updated</strong> and <strong>deactivated</strong>'),
|
||||
updatedExecuted: __('Snippet <strong>updated</strong> and <strong>executed</strong>.', 'code-snippets'),
|
||||
failedCreate: __('Could not create snippet.', 'code-snippets'),
|
||||
failedUpdate: __('Could not update snippet.', 'code-snippets')
|
||||
}
|
||||
|
||||
const conditionCreated = __('Condition <strong>created</strong>.', 'code-snippets')
|
||||
const conditionUpdated = __('Condition <strong>updated</strong>.', 'code-snippets')
|
||||
|
||||
const conditionMessages: typeof snippetMessages = {
|
||||
addNew: __('Add New Condition', 'code-snippets'),
|
||||
edit: __('Edit Condition', 'code-snippets'),
|
||||
created: conditionCreated,
|
||||
updated: conditionUpdated,
|
||||
createdActivated: conditionCreated,
|
||||
updatedActivated: conditionUpdated,
|
||||
updatedDeactivated: conditionUpdated,
|
||||
updatedExecuted: conditionUpdated,
|
||||
failedCreate: __('Could not create condition.', 'code-snippets'),
|
||||
failedUpdate: __('Could not update condition.', 'code-snippets')
|
||||
}
|
||||
|
||||
export enum SubmitSnippetAction {
|
||||
SAVE = 'save_snippet',
|
||||
SAVE_AND_ACTIVATE = 'save_snippet_activate',
|
||||
SAVE_AND_EXECUTE = 'save_snippet_execute',
|
||||
SAVE_AND_DEACTIVATE = 'save_snippet_deactivate'
|
||||
}
|
||||
|
||||
const getSuccessNotice = (request: Snippet, response: Snippet, action: SubmitSnippetAction): string => {
|
||||
const messages = 'condition' === request.scope ? conditionMessages : snippetMessages
|
||||
const wasCreated = 0 === request.id
|
||||
|
||||
switch (action) {
|
||||
case SubmitSnippetAction.SAVE:
|
||||
return wasCreated ? messages.created : messages.updated
|
||||
|
||||
case SubmitSnippetAction.SAVE_AND_EXECUTE:
|
||||
return messages.updatedExecuted
|
||||
|
||||
case SubmitSnippetAction.SAVE_AND_ACTIVATE:
|
||||
if ('single-use' === response.scope) {
|
||||
return messages.updatedExecuted
|
||||
} else {
|
||||
return wasCreated
|
||||
? messages.createdActivated
|
||||
: messages.updatedActivated
|
||||
}
|
||||
|
||||
case SubmitSnippetAction.SAVE_AND_DEACTIVATE:
|
||||
return messages.updatedDeactivated
|
||||
}
|
||||
}
|
||||
|
||||
const SUBMIT_ACTION_DELTA: Record<SubmitSnippetAction, Partial<Snippet>> = {
|
||||
[SubmitSnippetAction.SAVE]: {},
|
||||
[SubmitSnippetAction.SAVE_AND_ACTIVATE]: { active: true },
|
||||
[SubmitSnippetAction.SAVE_AND_DEACTIVATE]: { active: false },
|
||||
[SubmitSnippetAction.SAVE_AND_EXECUTE]: { active: true }
|
||||
}
|
||||
|
||||
export interface UseSubmitSnippet {
|
||||
submitSnippet: (action?: SubmitSnippetAction) => Promise<Snippet | undefined>
|
||||
}
|
||||
|
||||
export const useSubmitSnippet = (): UseSubmitSnippet => {
|
||||
const { snippetsAPI } = useRestAPI()
|
||||
const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm()
|
||||
|
||||
const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => {
|
||||
setCurrentNotice(undefined)
|
||||
|
||||
const result = await (async (): Promise<Snippet | string | undefined> => {
|
||||
try {
|
||||
const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] }
|
||||
const response = await (0 === request.id ? snippetsAPI.create(request) : snippetsAPI.update(request))
|
||||
setIsWorking(false)
|
||||
return response.id ? response : undefined
|
||||
} catch (error) {
|
||||
setIsWorking(false)
|
||||
return isAxiosError(error) ? error.message : undefined
|
||||
}
|
||||
})()
|
||||
|
||||
const messages = isCondition(snippet) ? conditionMessages : snippetMessages
|
||||
|
||||
if (undefined === result || 'string' === typeof result) {
|
||||
const message = [
|
||||
snippet.id ? messages.failedUpdate : messages.failedCreate,
|
||||
result ?? __('The server did not send a valid response.', 'code-snippets')
|
||||
]
|
||||
|
||||
setCurrentNotice(['error', message.filter(Boolean).join(' ')])
|
||||
return undefined
|
||||
} else {
|
||||
setSnippet(createSnippetObject(result))
|
||||
setCurrentNotice(['updated', getSuccessNotice(snippet, result, action)])
|
||||
|
||||
if (snippet.id && result.id) {
|
||||
window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit)
|
||||
window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id }))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet])
|
||||
|
||||
return { submitSnippet }
|
||||
}
|
||||
Reference in New Issue
Block a user