diff --git a/packages/core/src/Controller/Plugins/Plugins.ts b/packages/core/src/Controller/Plugins/Plugins.ts index 3dc772aca1..5671d42eda 100644 --- a/packages/core/src/Controller/Plugins/Plugins.ts +++ b/packages/core/src/Controller/Plugins/Plugins.ts @@ -187,8 +187,14 @@ export function Plugins( addPlugin, findPositions: findPositions, run() { - const { apiKey, apiUrl, projectId, observerOptions, tagNewKeys } = - getInitialOptions(); + const { + apiKey, + apiUrl, + projectId, + observerOptions, + tagNewKeys, + filterTag, + } = getInitialOptions(); instances.ui = plugins.ui?.({ apiKey: apiKey!, apiUrl: apiUrl!, @@ -198,6 +204,7 @@ export function Plugins( findPositions, onPermanentChange: (data) => events.onPermanentChange.emit(data), tagNewKeys, + filterTag, }); instances.observer?.run({ @@ -259,13 +266,14 @@ export function Plugins( }) as BackendGetRecordInternal, getBackendDevRecord: (async ({ language, namespace }) => { - const { apiKey, apiUrl, projectId } = getInitialOptions(); + const { apiKey, apiUrl, projectId, filterTag } = getInitialOptions(); return instances.devBackend?.getRecord({ apiKey, apiUrl, projectId, language, namespace, + filterTag, ...getCommonProps(), }); }) as BackendGetRecordInternal, diff --git a/packages/core/src/Controller/State/initState.ts b/packages/core/src/Controller/State/initState.ts index 2b557ad609..f37c651b23 100644 --- a/packages/core/src/Controller/State/initState.ts +++ b/packages/core/src/Controller/State/initState.ts @@ -119,6 +119,11 @@ export type TolgeeOptionsInternal = { * Specify tags that will be preselected for non-existant keys. */ tagNewKeys?: string[]; + + /** + * Use only keys tagged with one of the listed tags + */ + filterTag?: string[]; }; export type TolgeeOptions = Partial< diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index 882e2c11c3..72ff13cee5 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -14,6 +14,7 @@ export type BackendDevProps = { apiUrl?: string; apiKey?: string; projectId?: number | string; + filterTag?: string[]; }; export type CommonProps = { @@ -160,6 +161,7 @@ export type UiProps = { changeTranslation: ChangeTranslationInterface; onPermanentChange: (props: TranslationDescriptor) => void; tagNewKeys?: string[]; + filterTag?: string[]; }; export type UiKeyOption = { diff --git a/packages/i18next/src/__integration/tolgeeUpdating.test.ts b/packages/i18next/src/__integration/tolgeeUpdating.test.ts index 924a2ca069..416c8eeb5c 100644 --- a/packages/i18next/src/__integration/tolgeeUpdating.test.ts +++ b/packages/i18next/src/__integration/tolgeeUpdating.test.ts @@ -6,7 +6,7 @@ import i18n from 'i18next'; import { DevTools, Tolgee } from '@tolgee/web'; import { withTolgee, I18nextPlugin } from '..'; -const API_URL = 'http://localhost'; +const API_URL = 'http://localhost/'; const API_KEY = 'dummyApiKey'; const fetch = mockCoreFetch(); diff --git a/packages/web/src/app/basicTolgee.ts b/packages/web/src/app/basicTolgee.ts index d7ac5ed871..85e12c9080 100644 --- a/packages/web/src/app/basicTolgee.ts +++ b/packages/web/src/app/basicTolgee.ts @@ -15,6 +15,7 @@ export const tolgee = Tolgee() availableLanguages: ['en', 'cs', 'fr', 'de'], defaultLanguage: 'en', tagNewKeys: ['draft'], + filterTag: ['test'], }); export const useTolgee = (events?: TolgeeEvent[]): TolgeeInstance => { diff --git a/packages/web/src/package/DevBackend.ts b/packages/web/src/package/DevBackend.ts index d4e1b93fe9..233e895e49 100644 --- a/packages/web/src/package/DevBackend.ts +++ b/packages/web/src/package/DevBackend.ts @@ -3,21 +3,36 @@ import { getApiKeyType, getProjectIdFromApiKey } from './tools/decodeApiKey'; function createDevBackend(): BackendDevMiddleware { return { - getRecord({ apiUrl, apiKey, language, namespace, projectId, fetch }) { + getRecord({ + apiUrl, + apiKey, + language, + namespace, + projectId, + filterTag, + fetch, + }) { const pId = getProjectIdFromApiKey(apiKey) ?? projectId; - let url = - pId !== undefined - ? `${apiUrl}/v2/projects/${pId}/translations/${language}` - : `${apiUrl}/v2/projects/translations/${language}`; + const url = new URL(apiUrl); + + if (pId !== undefined) { + url.pathname = `/v2/projects/${pId}/translations/${language}`; + } else { + url.pathname = `/v2/projects/translations/${language}`; + } if (namespace) { - url += `?ns=${namespace}`; + url.searchParams.append('ns', namespace); } + filterTag?.forEach((tag) => { + url.searchParams.append('filterTag', tag); + }); if (getApiKeyType(apiKey) === 'tgpat' && projectId === undefined) { throw new Error("You need to specify 'projectId' when using PAT key"); } - return fetch(url, { + + return fetch(url.toString(), { headers: { 'X-API-Key': apiKey || '', 'Content-Type': 'application/json', diff --git a/packages/web/src/package/ui/KeyDialog/KeyForm.tsx b/packages/web/src/package/ui/KeyDialog/KeyForm.tsx index 89d50fdfd3..92d6464abe 100644 --- a/packages/web/src/package/ui/KeyDialog/KeyForm.tsx +++ b/packages/web/src/package/ui/KeyDialog/KeyForm.tsx @@ -14,6 +14,7 @@ import { PluralFormCheckbox } from './PluralFormCheckbox'; import { ErrorAlert } from './ErrorAlert'; import { HttpError } from '../client/HttpError'; import { Tooltip } from '../common/Tooltip'; +import { FilterTagMissingInfo } from './Tags/FilterTagMissingInfo'; const ScContainer = styled('div')` font-family: Rubik, Roboto, Arial; @@ -53,7 +54,7 @@ const ScKeyHint = styled('span')` `; const ScFieldsWrapper = styled('div')` - margin-top: 20px; + margin-top: 10px; `; const ScTagsWrapper = styled('div')` @@ -103,6 +104,7 @@ export const KeyForm = () => { const fallbackNamespaces = useDialogContext((c) => c.fallbackNamespaces); const selectedNs = useDialogContext((c) => c.selectedNs); const permissions = useDialogContext((c) => c.permissions); + const filterTagMissing = useDialogContext((c) => c.filterTagMissing); const screenshotsView = permissions.canViewScreenshots; const viewPluralCheckbox = permissions.canEditPlural && pluralsSupported; @@ -181,6 +183,7 @@ export const KeyForm = () => { Tags + {filterTagMissing && } )} {ready && viewPluralCheckbox && } @@ -207,7 +210,7 @@ export const KeyForm = () => { { const expanded = _expanded && isPlural; return ( - + { + const filterTag = useDialogContext((c) => c.uiProps.filterTag); + const filterTagMissing = useDialogContext((c) => c.filterTagMissing); + + if (!filterTagMissing) { + return null; + } + + if (filterTag) + return ( + + Missing Required Tag + {filterTag.length > 1 ? ( + + This app is configured to use only keys tagged with the{' '} + . Add one of them to continue. + + ) : ( + + This app is configured to use only keys tagged with the{' '} + . Add this tag to continue. + + )} + + + Read more in the docs + + + + ); +}; diff --git a/packages/web/src/package/ui/KeyDialog/Tags/MissingTag.tsx b/packages/web/src/package/ui/KeyDialog/Tags/MissingTag.tsx new file mode 100644 index 0000000000..3621dd7cff --- /dev/null +++ b/packages/web/src/package/ui/KeyDialog/Tags/MissingTag.tsx @@ -0,0 +1,37 @@ +import { styled } from '@mui/material'; +import { useDialogActions } from '../dialogContext'; + +const StyledTag = styled('div')` + display: inline-flex; + outline: 0; + cursor: default; + padding: 1px 8px; + border-radius: 12px; + align-items: center; + font-size: 14px; + background: ${({ theme }) => theme.palette.grey[200]}; + border: 1px solid transparent; + max-width: 100%; + box-sizing: border-box; + border: 1px solid ${({ theme }) => theme.palette.text.secondary}; + margin: -2px 0px; + cursor: pointer; + &:focus-within, + &:hover { + border: 1px solid ${({ theme }) => theme.palette.primary.main}; + color: ${({ theme }) => theme.palette.primary.main}; + } +`; + +type Props = { + name: string; +}; + +export const MissingTag = ({ name }: Props) => { + const { setTags } = useDialogActions(); + function addTag(name: string) { + setTags((values) => [...values.filter((t) => t !== name), name]); + } + + return addTag(name)}>{name}; +}; diff --git a/packages/web/src/package/ui/KeyDialog/Tags/MissingTagsList.tsx b/packages/web/src/package/ui/KeyDialog/Tags/MissingTagsList.tsx new file mode 100644 index 0000000000..383c3e4806 --- /dev/null +++ b/packages/web/src/package/ui/KeyDialog/Tags/MissingTagsList.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { MissingTag } from './MissingTag'; + +type Props = { + tags: string[]; +}; + +export const MissingTagsList = ({ tags }: Props) => { + return ( + <> + {tags.map((tag, index) => { + if (index === 0) { + return ( + + + + ); + } else if (index < tags.length - 1) { + return ( + + , + + ); + } else { + return ( + + {' '} + or + + ); + } + })} + + ); +}; diff --git a/packages/web/src/package/ui/KeyDialog/dialogContext/index.ts b/packages/web/src/package/ui/KeyDialog/dialogContext/index.ts index 3633e4a8ea..ebe7a222a3 100644 --- a/packages/web/src/package/ui/KeyDialog/dialogContext/index.ts +++ b/packages/web/src/package/ui/KeyDialog/dialogContext/index.ts @@ -81,11 +81,15 @@ export const [DialogProvider, useDialogActions, useDialogContext] = const [saving, setSaving] = useState(false); const [selectedNs, setSelectedNs] = useState(props.namespace); - const [tags, setTags] = useState([]); + const [tags, setTags] = useState(undefined); const [_isPlural, setIsPlural] = useState(); const [_pluralArgName, setPluralArgName] = useState(); const [submitError, setSubmitError] = useState(); + const filterTagMissing = + Boolean(props.uiProps.filterTag?.length) && + tags && + !props.uiProps.filterTag.find((t) => tags.includes(t)); useEffect(() => { // reset when key changes setIsPlural(undefined); @@ -182,7 +186,10 @@ export const [DialogProvider, useDialogActions, useDialogContext] = if (firstKey) { setTags(firstKey?.keyTags?.map((t) => t.name) || []); } else { - setTags(props.uiProps.tagNewKeys ?? []); + setTags([ + ...(props.uiProps.filterTag ?? []), + ...(props.uiProps.tagNewKeys ?? []), + ]); } setScreenshots( firstKey?.screenshots?.map((sc) => ({ @@ -486,7 +493,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] = screenshotDetail, linkToPlatform, keyExists, - tags, + tags: tags || [], permissions, canTakeScreenshots, isPlural, @@ -495,6 +502,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] = pluralsSupported, icuPlaceholders, submitError, + filterTagMissing, } as const; const actions = {