Skip to content
Open
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
38 changes: 30 additions & 8 deletions src/css/edit/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@
.delete-button {
color: #cc1818;

&:hover {
&:hover:not(:disabled) {
color: #9e1313;
}

&:focus {
&:focus:not(:disabled) {
color: #710d0d;
border-color: #710d0d;
}

&:disabled {
color: #a7aaad;
cursor: not-allowed;
}
}

.help-tooltip {
Expand Down Expand Up @@ -61,6 +66,19 @@
> :last-child {
margin-inline-start: auto;
}

&.lock-control-container {
border-block-start: 1px solid #eee;
padding-block-start: 1em;
margin-block-start: 0.5em;

.description {
flex-basis: 100%;
margin-block-start: 4px;
color: #646970;
font-style: italic;
}
}
}

.block-form-field {
Expand Down Expand Up @@ -108,11 +126,15 @@ p.submit {
padding-block-start: 0;
}

.activation-switch-container label {
display: flex;
flex-flow: row;
gap: 5px;
justify-content: center;
.activation-switch-container,
.lock-control-container {
label {
display: flex;
flex-flow: row;
gap: 5px;
justify-content: center;
align-items: center;
}
}

.shortcode-tag-wrapper {
Expand Down Expand Up @@ -152,4 +174,4 @@ p.submit {
.components-spinner {
block-size: 12px;
}
}
}
25 changes: 22 additions & 3 deletions src/css/manage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@use 'common/direction';
@use 'common/select';
@use 'manage/cloud';

.column-name,
.column-type {
.dashicons {
Expand All @@ -23,6 +23,17 @@
.dashicons-clock {
vertical-align: middle;
}

.dashicons-lock {
color: #646970;
margin-inline-start: 4px;
opacity: 0.7;
cursor: help;

&:hover {
opacity: 1;
}
}
}

.active-snippet .column-name > .snippet-name {
Expand Down Expand Up @@ -91,6 +102,14 @@
color: #ddd;
position: relative;
inset-inline-start: 0;

.delete {
&.disabled {
color: #a7aaad;
cursor: not-allowed;
pointer-events: none;
}
}
}

.column-activate {
Expand Down Expand Up @@ -128,7 +147,7 @@
}

&, #all-snippets-table, #search-snippets-table {
a.delete:hover {
a.delete:not(.disabled):hover {
border-block-end: 1px solid #f00;
color: #f00;
}
Expand Down Expand Up @@ -212,4 +231,4 @@ td.column-description {
display: none;
}
}
}
}
5 changes: 4 additions & 1 deletion src/js/components/EditorSidebar/EditorSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MultisiteSharingSettings } from './controls/MultisiteSharingSettings'
import { ExportButtons } from './actions/ExportButtons'
import { SubmitButtons } from './actions/SubmitButtons'
import { ActivationSwitch } from './controls/ActivationSwitch'
import { LockControl } from './controls/LockControl'
import { DeleteButton } from './actions/DeleteButton'
import { PriorityInput } from './controls/PriorityInput'
import { RTLControl } from './controls/RTLControl'
Expand All @@ -29,6 +30,8 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = ({ setIsUpgradeDialog
<div className="box">
{snippet.id && !isCondition(snippet) ? <ActivationSwitch /> : null}

{snippet.id ? <LockControl /> : null}

{isNetworkAdmin() ? <MultisiteSharingSettings /> : null}

{isRTL() ? <RTLControl /> : null}
Expand All @@ -53,4 +56,4 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = ({ setIsUpgradeDialog
<Notices />
</div>
)
}
}
4 changes: 2 additions & 2 deletions src/js/components/EditorSidebar/actions/DeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const DeleteButton: React.FC = () => {
<Button
id="delete-snippet"
className="delete-button"
disabled={isWorking}
disabled={isWorking || snippet.locked}
onClick={() => {
setIsDialogOpen(true)
}}
Expand Down Expand Up @@ -49,4 +49,4 @@ export const DeleteButton: React.FC = () => {
</ConfirmDialog>
</>
)
}
}
52 changes: 52 additions & 0 deletions src/js/components/EditorSidebar/controls/LockControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { SubmitSnippetAction, useSubmitSnippet } from '../../../hooks/useSubmitSnippet'
import { handleUnknownError } from '../../../utils/errors'

export const LockControl: React.FC = () => {
const { snippet, setSnippet, isWorking } = useSnippetForm()
const { submitSnippet } = useSubmitSnippet()

const handleToggle = () => {
const newLockedStatus = !snippet.locked

// Create the updated snippet object immediately
const updatedSnippet = {
...snippet,
locked: newLockedStatus
}

// Update local state for immediate UI response
setSnippet(updatedSnippet)

// Submit to the server using the override to prevent stale state issues
submitSnippet(SubmitSnippetAction.SAVE, updatedSnippet)
.then(() => undefined)
.catch(handleUnknownError)
}

return (
<div className="inline-form-field lock-control-container">
<h4>{__('Lock Snippet', 'code-snippets')}</h4>

<label>
{snippet.locked
? __('Locked', 'code-snippets')
: __('Unlocked', 'code-snippets')}

<input
id="snippet-lock"
type="checkbox"
checked={snippet.locked}
disabled={isWorking}
className="switch"
onChange={handleToggle}
/>
</label>
<p className="description">
{__('Prevent accidental changes or deletion.', 'code-snippets')}
</p>
</div>
)
}
7 changes: 5 additions & 2 deletions src/js/hooks/useSnippetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export const WithSnippetFormContext: React.FC<WithSnippetFormContextProps> = ({
const [currentNotice, setCurrentNotice] = useState<ScreenNotice>()
const [codeEditorInstance, setCodeEditorInstance] = useState<CodeEditorInstance>()

const isReadOnly = useMemo(() => !isLicensed() && isProSnippet({ scope: snippet.scope }), [snippet.scope])
const isReadOnly = useMemo(
() => snippet.locked || (!isLicensed() && isProSnippet({ scope: snippet.scope })),
[snippet.locked, snippet.scope]
)

const handleRequestError = useCallback((error: unknown, message?: string) => {
console.error('Request failed', error)
Expand Down Expand Up @@ -66,4 +69,4 @@ export const WithSnippetFormContext: React.FC<WithSnippetFormContextProps> = ({
}

return <SnippetFormContext.Provider value={value}>{children}</SnippetFormContext.Provider>
}
}
34 changes: 21 additions & 13 deletions src/js/hooks/useSubmitSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,50 +76,58 @@ const SUBMIT_ACTION_DELTA: Record<SubmitSnippetAction, Partial<Snippet>> = {
}

export interface UseSubmitSnippet {
submitSnippet: (action?: SubmitSnippetAction) => Promise<Snippet | undefined>
submitSnippet: (action?: SubmitSnippetAction, snippetOverride?: Snippet) => Promise<Snippet | undefined>
}

export const useSubmitSnippet = (): UseSubmitSnippet => {
const { snippetsAPI } = useRestAPI()
const { setIsWorking, setCurrentNotice, snippet, setSnippet } = useSnippetForm()

const submitSnippet = useCallback(async (action: SubmitSnippetAction = SubmitSnippetAction.SAVE) => {
const submitSnippet = useCallback(async (
action: SubmitSnippetAction = SubmitSnippetAction.SAVE,
snippetOverride?: Snippet
) => {
setCurrentNotice(undefined)
setIsWorking(true)

// Use the override if provided (prevents stale state issues), otherwise use current context state
const activeSnippet = snippetOverride ?? snippet

const result = await (async (): Promise<Snippet | string | undefined> => {
try {
const request: Snippet = { ...snippet, ...SUBMIT_ACTION_DELTA[action] }
const request: Snippet = { ...activeSnippet, ...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
} finally {
setIsWorking(false)
}
})()

const messages = isCondition(snippet) ? conditionMessages : snippetMessages
const messages = isCondition(activeSnippet) ? conditionMessages : snippetMessages

if (undefined === result || 'string' === typeof result) {
const message = [
snippet.id ? messages.failedUpdate : messages.failedCreate,
activeSnippet.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)])
const updatedSnippet = createSnippetObject(result)
setSnippet(updatedSnippet)
setCurrentNotice(['updated', getSuccessNotice(activeSnippet, updatedSnippet, action)])

if (snippet.id && result.id) {
if (activeSnippet.id && updatedSnippet.id) {
window.document.title = window.document.title.replace(snippetMessages.addNew, messages.edit)
window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: result.id }))
window.history.replaceState({}, '', addQueryArgs(window.CODE_SNIPPETS?.urls.edit, { id: updatedSnippet.id }))
}

return result
return updatedSnippet
}
}, [snippetsAPI, setIsWorking, setCurrentNotice, snippet, setSnippet])

return { submitSnippet }
}
}
3 changes: 2 additions & 1 deletion src/js/types/Snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Snippet {
readonly scope: SnippetScope
readonly priority: number
readonly active: boolean
readonly locked: boolean
readonly network: boolean
readonly shared_network?: boolean | null
readonly modified?: string
Expand All @@ -26,4 +27,4 @@ export const SNIPPET_TYPE_SCOPES = <const> {
css: ['admin-css', 'site-css'],
js: ['site-head-js', 'site-footer-js'],
cond: ['condition']
}
}
3 changes: 2 additions & 1 deletion src/js/types/schema/SnippetSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface WritableSnippetSchema {
scope?: SnippetScope
condition_id?: number
active?: boolean
locked?: boolean
priority?: number
network?: boolean | null
shared_network?: boolean | null
Expand All @@ -17,4 +18,4 @@ export interface SnippetSchema extends Readonly<Required<WritableSnippetSchema>>
readonly id: number
readonly modified: string
readonly code_error?: readonly [string, number] | null
}
}
4 changes: 3 additions & 1 deletion src/js/utils/snippets/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const mapToSchema = ({
scope,
priority,
active,
locked,
network,
shared_network,
conditionId
Expand All @@ -45,6 +46,7 @@ const mapToSchema = ({
scope,
priority,
active,
locked,
network,
shared_network,
condition_id: conditionId
Expand Down Expand Up @@ -89,4 +91,4 @@ export const buildSnippetsAPI = ({ get, post, del, put }: RestAPI): SnippetsAPI

detach: snippet =>
put(buildURL(snippet, 'detach'))
})
})
4 changes: 3 additions & 1 deletion src/js/utils/snippets/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const defaults: Omit<Snippet, 'tags'> = {
scope: 'global',
modified: '',
active: false,
locked: false,
network: isNetworkAdmin(),
shared_network: null,
priority: 10,
Expand Down Expand Up @@ -43,9 +44,10 @@ export const parseSnippetObject = (fields: unknown): Snippet => {
...'scope' in fields && isValidScope(fields.scope) && { scope: fields.scope },
...'modified' in fields && 'string' === typeof fields.modified && { modified: fields.modified },
...'active' in fields && 'boolean' === typeof fields.active && { active: fields.active },
...'locked' in fields && 'boolean' === typeof fields.locked && { locked: fields.locked },
...'network' in fields && 'boolean' === typeof fields.network && { network: fields.network },
...'shared_network' in fields && 'boolean' === typeof fields.shared_network && { shared_network: fields.shared_network },
...'priority' in fields && 'number' === typeof fields.priority && { priority: fields.priority },
...'condition_id' in fields && isAbsInt(fields.condition_id) && { conditionId: fields.condition_id }
}
}
}
Loading