Refactor conflicts page to view all languages side-by-side

This commit is contained in:
Simon Prévost 2024-02-14 22:25:12 -05:00
parent 8bcdf3b1c9
commit 995c2a5859
96 changed files with 1653 additions and 1249 deletions

View File

@ -15,8 +15,8 @@ defmodule Accent.GraphQL.Paginated do
@enforce_keys [:entries, :meta]
defstruct entries: [], meta: %{}
def paginate(query, args) do
Accent.Repo.paginate(query, page: args[:page], page_size: args[:page_size])
def paginate(query, args, options \\ []) do
Accent.Repo.paginate(query, page: args[:page], page_size: args[:page_size], options: options)
end
def format(paginated_list) do

View File

@ -1,5 +1,8 @@
defmodule Accent.GraphQL.Resolvers.Translation do
@moduledoc false
import Absinthe.Resolution.Helpers, only: [batch: 3]
alias Accent.Document
alias Accent.GraphQL.Paginated
alias Accent.Plugs.GraphQLContext
alias Accent.Project
@ -27,6 +30,59 @@ defmodule Accent.GraphQL.Resolvers.Translation do
|> then(&{:ok, &1})
end
def batch_translation_ids(grouped_translation, _args, _resolution) do
ids = Enum.reject(grouped_translation.translation_ids, &is_nil/1)
batch(
{__MODULE__, :from_translation_ids},
ids,
fn batch_results ->
translations =
Map.get(batch_results, {grouped_translation.key, grouped_translation.document_id}, [])
translations =
Enum.sort(translations, fn a, b ->
cond do
a.revision.master == true -> true
DateTime.compare(a.revision.inserted_at, b.revision.inserted_at) === :lt -> true
true -> false
end
end)
{:ok, translations}
end
)
end
def from_translation_ids(_, ids) do
ids = Enum.map(List.flatten(ids), &Ecto.UUID.cast!(&1))
query =
Query.from(translations in Translation,
preload: [:revision],
where: translations.id in ^ids
)
query
|> Repo.all()
|> Enum.group_by(&{&1.key, &1.document_id})
end
def batch_document(grouped_translation, _args, _resolution) do
batch({__MODULE__, :from_document_id}, grouped_translation.document_id, fn batch_results ->
[document | _] = Map.get(batch_results, grouped_translation.document_id)
{:ok, document}
end)
end
def from_document_id(_, ids) do
query = Query.from(documents in Document, where: documents.id in ^ids)
query
|> Repo.all()
|> Enum.group_by(& &1.id)
end
@spec correct(Translation.t(), %{text: String.t()}, GraphQLContext.t()) :: translation_operation
def correct(translation, %{text: text}, info) do
%Context{}
@ -124,12 +180,29 @@ defmodule Accent.GraphQL.Resolvers.Translation do
translations =
Translation
|> list(args, project.id)
|> TranslationScope.from_project(project.id)
|> Paginated.paginate(args)
{:ok, Paginated.format(translations)}
end
@spec list_grouped_project(Project.t(), map(), GraphQLContext.t()) :: {:ok, Paginated.t(Translation.t())}
def list_grouped_project(project, args, _) do
total_entries =
Translation
|> list_grouped_count(args, project.id)
|> Repo.all()
|> Enum.count()
translations =
Translation
|> list_grouped(args, project.id)
|> Paginated.paginate(args, total_entries: total_entries)
revisions = grouped_related_revisions(Map.put(args, :project_id, project.id))
{:ok, Map.put(Paginated.format(translations), :revisions, revisions)}
end
@spec related_translations(Translation.t(), map(), struct()) :: {:ok, [Translation.t()]}
def related_translations(translation, _, _) do
translations =
@ -179,7 +252,112 @@ defmodule Accent.GraphQL.Resolvers.Translation do
end
end
defp grouped_related_revisions(args) do
query_revision_ids =
if Enum.empty?(args[:related_revisions]) do
Query.from(
revisions in Revision,
where: revisions.project_id == ^args[:project_id],
order_by: [asc: :inserted_at],
limit: 2
)
else
Query.from(
revisions in Revision,
where: revisions.id in ^args[:related_revisions],
order_by: [asc: :inserted_at],
limit: 2
)
end
Repo.all(query_revision_ids)
end
defp grouped_related_query(schema, args, project_id) do
revision_ids =
if Enum.empty?(args[:related_revisions]) do
Enum.map(grouped_related_revisions(Map.put(args, :project_id, project_id)), & &1.id)
else
args[:related_revisions]
end
query =
schema
|> TranslationScope.from_version(args[:version])
|> TranslationScope.from_project(project_id)
|> TranslationScope.from_revisions(revision_ids)
|> TranslationScope.active()
|> TranslationScope.not_locked()
{query, revision_ids}
end
defp list_grouped_count(schema, args, project_id) do
query = list_base_query(schema, args, project_id)
{related_query, revision_ids} = grouped_related_query(schema, args, project_id)
query =
Query.from(
translations in query,
left_join: related_translations in subquery(related_query),
as: :related_translations,
on:
related_translations.revision_id in ^revision_ids and
related_translations.key == translations.key and
related_translations.document_id == translations.document_id,
distinct: [translations.key, translations.document_id],
select: translations.key,
group_by: [translations.key, translations.document_id]
)
if args[:is_conflicted] do
Query.from([related_translations: related_translations] in query,
having: fragment("array_agg(distinct(?))", related_translations.conflicted) != [false]
)
else
query
end
end
defp list_grouped(schema, args, project_id) do
query = list_base_query(schema, args, project_id)
{related_query, revision_ids} = grouped_related_query(schema, args, project_id)
query =
Query.from(
translations in query,
left_join: related_translations in subquery(related_query),
as: :related_translations,
on:
related_translations.revision_id in ^revision_ids and
related_translations.key == translations.key and
related_translations.document_id == translations.document_id,
distinct: translations.key,
select: %{
key: translations.key,
document_id: translations.document_id,
translation_ids: fragment("array_agg(distinct(?))", related_translations.id)
},
group_by: [translations.key, translations.document_id]
)
if args[:is_conflicted] do
Query.from([related_translations: related_translations] in query,
having: fragment("array_agg(distinct(?))", related_translations.conflicted) != [false]
)
else
query
end
end
defp list(schema, args, project_id) do
schema
|> list_base_query(args, project_id)
|> Query.distinct(true)
|> Query.preload(:revision)
end
defp list_base_query(schema, args, project_id) do
schema
|> TranslationScope.active()
|> TranslationScope.not_locked()
@ -193,7 +371,6 @@ defmodule Accent.GraphQL.Resolvers.Translation do
|> TranslationScope.parse_empty(args[:is_text_empty])
|> TranslationScope.parse_commented_on(args[:is_commented_on])
|> TranslationScope.from_version(args[:version])
|> Query.distinct(true)
|> Query.preload(:revision)
|> TranslationScope.from_project(project_id)
end
end

View File

@ -6,6 +6,8 @@ defmodule Accent.GraphQL.Types.Project do
import Accent.GraphQL.Helpers.Authorization
import Accent.GraphQL.Helpers.Fields
alias Accent.GraphQL.Resolvers.Translation, as: TranslationResolver
object :projects do
field(:meta, non_null(:pagination_meta))
field(:entries, list_of(:project))
@ -135,13 +137,14 @@ defmodule Accent.GraphQL.Types.Project do
resolve(project_authorize(:index_documents, &Accent.GraphQL.Resolvers.Document.list_project/3))
end
field :translations, :translations do
field :grouped_translations, :grouped_translations do
arg(:page, :integer)
arg(:page_size, :integer)
arg(:order, :string)
arg(:document, :id)
arg(:version, :id)
arg(:related_revisions, list_of(non_null(:id)))
arg(:query, :string)
arg(:is_translated, :boolean)
arg(:is_conflicted, :boolean)
arg(:is_text_empty, :boolean)
arg(:is_text_not_empty, :boolean)
@ -151,11 +154,28 @@ defmodule Accent.GraphQL.Types.Project do
resolve(
project_authorize(
:index_translations,
&Accent.GraphQL.Resolvers.Translation.list_project/3
&TranslationResolver.list_grouped_project/3
)
)
end
field :translations, :translations do
arg(:page, :integer)
arg(:page_size, :integer)
arg(:order, :string)
arg(:document, :id)
arg(:version, :id)
arg(:query, :string)
arg(:is_translated, :boolean)
arg(:is_conflicted, :boolean)
arg(:is_text_empty, :boolean)
arg(:is_text_not_empty, :boolean)
arg(:is_added_last_sync, :boolean)
arg(:is_commented_on, :boolean)
resolve(project_authorize(:index_translations, &TranslationResolver.list_project/3))
end
field :activities, :activities do
arg(:page, :integer)
arg(:page_size, :integer)
@ -182,7 +202,7 @@ defmodule Accent.GraphQL.Types.Project do
field :translation, :translation do
arg(:id, non_null(:id))
resolve(project_authorize(:show_translation, &Accent.GraphQL.Resolvers.Translation.show_project/3))
resolve(project_authorize(:show_translation, &TranslationResolver.show_project/3))
end
field :activity, :activity do
@ -200,6 +220,7 @@ defmodule Accent.GraphQL.Types.Project do
field :revisions, list_of(:revision) do
arg(:version_id, :id)
resolve(project_authorize(:index_revisions, &Accent.GraphQL.Resolvers.Revision.list_project/3))
end

View File

@ -18,6 +18,15 @@ defmodule Accent.GraphQL.Types.Translation do
value(:float, as: "float")
end
object :grouped_translation do
field(:key, non_null(:string))
field(:document, non_null(:document), resolve: &Accent.GraphQL.Resolvers.Translation.batch_document/3)
field(:translations, non_null(list_of(non_null(:translation))),
resolve: &Accent.GraphQL.Resolvers.Translation.batch_translation_ids/3
)
end
object :translation do
field(:id, non_null(:id))
field(:key, non_null(:string), resolve: &Accent.GraphQL.Resolvers.Translation.key/3)
@ -119,4 +128,10 @@ defmodule Accent.GraphQL.Types.Translation do
field(:meta, :pagination_meta)
field(:entries, list_of(:translation))
end
object :grouped_translations do
field(:meta, :pagination_meta)
field(:entries, list_of(:grouped_translation))
field(:revisions, non_null(list_of(non_null(:revision))))
end
end

View File

@ -34,6 +34,7 @@ module.exports = {
'no-shadowed-elements': true,
'no-trailing-spaces': true,
'no-triple-curlies': false,
'no-inline-styles': false,
'no-unused-block-params': true,
quotes: false,
'require-valid-alt-text': false,
@ -41,7 +42,7 @@ module.exports = {
'require-button-type': false,
'self-closing-void-elements': false,
'simple-unless': false,
'style-concatenation': true,
'style-concatenation': false,
'table-groups': true,
'template-length': [true, {min: 1, max: 200}],
}

View File

@ -14,12 +14,16 @@ interface Args {
conflicts: any;
document: any;
documents: any;
relatedRevisions: any;
defaultRelatedRevisions: any[];
revisions: any;
version: any;
versions: any;
query: any;
withAdvancedFilters: boolean;
onChangeDocument: () => void;
onChangeReference: () => void;
onChangeVersion: () => void;
onChangeRevisions: () => void;
onChangeQuery: (query: string) => void;
}
@ -30,9 +34,19 @@ export default class ConflictsFilters extends Component<Args> {
@gt('args.documents.length', 1)
showDocumentsSelect: boolean;
@gt('args.revisions.length', 1)
showRevisionsSelect: boolean;
@gt('args.versions.length', 0)
showVersionsSelect: boolean;
get showSomeFilters() {
return this.showDocumentsSelect || this.showVersionsSelect;
}
@tracked
displayAdvancedFilters = this.args.withAdvancedFilters;
@tracked
debouncedQuery = this.args.query;
@ -64,6 +78,29 @@ export default class ConflictsFilters extends Component<Args> {
return documents;
}
get relatedRevisionsValue() {
if (this.args.relatedRevisions.length === 0) {
const revisionIds = this.args.defaultRelatedRevisions.map(
({id}: any) => id
);
return this.mappedRevisions.filter(({value}: {value: string}) =>
revisionIds.includes(value)
);
}
return this.mappedRevisions.filter(({value}: {value: string}) =>
this.args.relatedRevisions?.includes(value)
);
}
get mappedRevisionsOptions() {
const values = this.relatedRevisionsValue.map(({value}: any) => value);
return this.mappedRevisions.filter(
({value}: {value: string}) => !values.includes(value)
);
}
get documentValue() {
return this.mappedDocuments.find(
({value}: {value: string}) => value === this.args.document
@ -88,6 +125,20 @@ export default class ConflictsFilters extends Component<Args> {
return versions;
}
get mappedRevisions() {
return this.args.revisions.map(
(revision: {
id: string;
name: string | null;
slug: string | null;
language: {slug: string; name: string};
}) => ({
label: revision.name || revision.language.name,
value: revision.id,
})
);
}
get versionValue() {
return this.mappedVersions.find(
({value}: {value: string}) => value === this.args.version
@ -101,6 +152,11 @@ export default class ConflictsFilters extends Component<Args> {
this.debounceQuery.perform(target.value);
}
@action
toggleAdvancedFilters() {
this.displayAdvancedFilters = !this.displayAdvancedFilters;
}
@action
submitForm(event: Event) {
event.preventDefault();

View File

@ -0,0 +1,21 @@
import Component from '@glimmer/component';
interface Args {
revisions: any[];
isTextEmptyFilter: boolean;
isTextNotEmptyFilter: boolean;
isAddedLastSyncFilter: boolean;
isNotTranslatedFilter: boolean;
isCommentedOnFilter: boolean;
onChangeAdvancedFilterBoolean: (
key:
| 'isTextEmpty'
| 'isTextNotEmpty'
| 'isAddedLastSync'
| 'isCommentedOn'
| 'isNotTranslated',
event: InputEvent
) => void;
}
export default class AdvancedFilters extends Component<Args> {}

View File

@ -1,9 +1,11 @@
import {tracked} from '@glimmer/tracking';
import {action} from '@ember/object';
import Component from '@glimmer/component';
interface Args {
permissions: Record<string, true>;
project: any;
conflicts: any;
groupedTranslations: any;
version: any;
versions: any[];
query: any;
@ -15,7 +17,10 @@ interface Args {
) => void;
}
export default class ConflictsItems extends Component<Args> {
export default class ConflictsList extends Component<Args> {
@tracked
selectedTranslationId: string | null = null;
get currentVersion() {
if (!this.args.versions) return;
if (!this.args.version) return;
@ -24,4 +29,34 @@ export default class ConflictsItems extends Component<Args> {
(version) => version.id === this.args.version
);
}
get revisions() {
if (this.args.groupedTranslations.length === 0) return [];
return this.args.groupedTranslations[0].translations.map(
({revision}: any) => revision
);
}
get mappedRevisions() {
if (this.args.groupedTranslations.length === 0) return [];
return this.args.groupedTranslations[0].translations.map(
({revision}: any) => {
return {
name: revision.name || revision.language.name,
slug: revision.slug || revision.language.slug,
};
}
);
}
get showRevisionsHeader() {
return this.revisions.length > 1;
}
@action
handleFocus(id: string) {
this.selectedTranslationId = id;
}
}

View File

@ -0,0 +1,29 @@
import {action} from '@ember/object';
import Component from '@glimmer/component';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
interface Args {
selectedTranslationId: string | null;
groupedTranslation: {
key: string;
translations: any[];
};
onFocus: (id: string) => void;
}
export default class ConflictsListGroup extends Component<Args> {
translationKey = parsedKeyProperty(this.args.groupedTranslation.key);
get masterTranslation() {
return this.args.groupedTranslation.translations[0];
}
get isFocused() {
return this.masterTranslation.id === this.args.selectedTranslationId;
}
@action
handleFocus() {
this.args.onFocus(this.masterTranslation.id);
}
}

View File

@ -2,15 +2,15 @@ import {action} from '@ember/object';
import {empty} from '@ember/object/computed';
import Component from '@glimmer/component';
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
import {dropTask} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
import {MutationResponse} from 'accent-webapp/services/apollo-mutate';
interface Conflict {
interface Translation {
id: string;
key: string;
conflictedText: string;
correctedText: string;
isConflicted: boolean;
revision: {
name: string | null;
slug: string | null;
@ -22,13 +22,6 @@ interface Conflict {
rtl: boolean;
};
};
relatedTranslations: Array<{
id: string;
correctedText: string;
revision: {
isMaster: boolean;
};
}>;
}
interface Args {
@ -36,79 +29,53 @@ interface Args {
index: number;
project: any;
prompts: any[];
conflict: Conflict;
onCorrect: (conflict: any, textInput: string) => Promise<MutationResponse>;
onCopyTranslation: (
text: string,
sourceLanguageSlug: string | null,
targetLanguageSlug: string
) => Promise<{text: string | null}>;
translation: Translation;
onFocus: () => void;
onBlur: () => void;
onCorrect: (translation: any, textInput: string) => Promise<MutationResponse>;
onUpdate: (translation: any, textInput: string) => Promise<MutationResponse>;
onUncorrect: (
translation: any,
textInput: string
) => Promise<MutationResponse>;
}
export default class ConflictItem extends Component<Args> {
@empty('args.conflict.conflictedText')
export default class ConflictsListItem extends Component<Args> {
@empty('args.translation.conflictedText')
emptyPreviousText: boolean;
@tracked
textInput = this.args.conflict.correctedText;
textInput = this.args.translation.correctedText;
@tracked
loading = false;
conflictResolved = false;
@tracked
isCorrectLoading = false;
@tracked
isUncorrectLoading = false;
@tracked
isUpdateLoading = false;
@tracked
error = false;
@tracked
resolved = false;
@tracked
inputDisabled = false;
conflictKey = parsedKeyProperty(this.args.conflict.key);
textOriginal = this.args.conflict.correctedText;
get relatedTranslations() {
const masterConflict = this.args.conflict.relatedTranslations.find(
(translation) => translation.revision.isMaster
);
if (!masterConflict) return [];
return this.args.conflict.relatedTranslations.filter((translation) => {
return (
translation.id === masterConflict.id ||
translation.correctedText !== masterConflict.correctedText
);
});
}
get showTextDiff() {
if (!this.args.conflict.conflictedText) return false;
return this.textInput !== this.args.conflict.conflictedText;
}
translationKey = parsedKeyProperty(this.args.translation.key);
textOriginal = this.args.translation.correctedText;
get showOriginalButton() {
return this.textInput !== this.textOriginal;
}
get revisionName() {
return (
this.args.conflict.revision.name ||
this.args.conflict.revision.language.name
);
}
get revisionSlug() {
return (
this.args.conflict.revision.slug ||
this.args.conflict.revision.language.slug
);
}
get revisionTextDirRtl() {
return this.args.conflict.revision.rtl !== null
? this.args.conflict.revision.rtl
: this.args.conflict.revision.language.rtl;
return this.args.translation.revision.rtl !== null
? this.args.translation.revision.rtl
: this.args.translation.revision.language.rtl;
}
@action
@ -132,30 +99,12 @@ export default class ConflictItem extends Component<Args> {
this.inputDisabled = false;
}
copyTranslationTask = dropTask(
async (text: string, sourceLanguageSlug: string) => {
this.inputDisabled = true;
const copyTranslation = await this.args.onCopyTranslation(
text,
sourceLanguageSlug,
this.revisionSlug
);
this.inputDisabled = false;
if (copyTranslation.text) {
this.textInput = copyTranslation.text;
}
}
);
@action
async correct() {
this.onLoading();
async correctConflict() {
this.onCorrectLoading();
const response = await this.args.onCorrect(
this.args.conflict,
this.args.translation,
this.textInput
);
@ -166,18 +115,71 @@ export default class ConflictItem extends Component<Args> {
}
}
private onLoading() {
@action
async uncorrectConflict() {
this.onUncorrectLoading();
const response = await this.args.onUncorrect(
this.args.translation,
this.textInput
);
if (response.errors) {
this.onError();
} else {
this.onUncorrectSuccess();
}
}
@action
async updateConflict() {
this.onUpdateLoading();
const response = await this.args.onUpdate(
this.args.translation,
this.textInput
);
if (response.errors) {
this.onError();
} else {
this.onUpdateSuccess();
}
}
private onCorrectLoading() {
this.error = false;
this.loading = true;
this.isCorrectLoading = true;
}
private onUncorrectLoading() {
this.error = false;
this.isUncorrectLoading = true;
}
private onUpdateLoading() {
this.error = false;
this.isUpdateLoading = true;
}
private onError() {
this.error = true;
this.loading = false;
this.isUpdateLoading = false;
this.isCorrectLoading = false;
this.isUncorrectLoading = false;
}
private onCorrectSuccess() {
this.resolved = true;
this.loading = false;
this.conflictResolved = true;
this.isCorrectLoading = false;
}
private onUncorrectSuccess() {
this.conflictResolved = false;
this.isCorrectLoading = false;
}
private onUpdateSuccess() {
this.isUpdateLoading = false;
}
}

View File

@ -1,42 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
interface Args {
project: any;
translation: any;
onCopyTranslation: (text: string, languageSlug: string) => void;
}
const MAX_TEXT_LENGTH = 600;
export default class ConflictItemRelatedTranslation extends Component<Args> {
get text() {
const text = this.args.translation.correctedText;
if (text.length < MAX_TEXT_LENGTH) return text;
return `${text.substring(0, MAX_TEXT_LENGTH - 1)}`;
}
get revisionName() {
return (
this.args.translation.revision.slug ||
this.args.translation.revision.language.slug
);
}
@action
translate() {
this.args.onCopyTranslation(
this.args.translation.correctedText,
this.revisionSlug
);
}
private get revisionSlug() {
return (
this.args.translation.revision.slug ||
this.args.translation.revision.language.slug
);
}
}

View File

@ -1,29 +0,0 @@
import Component from '@glimmer/component';
interface Args {
project: any;
revision: any;
translations: any;
isLoading: boolean;
showLoading: boolean;
document: any;
version: any;
permissions: Record<string, true>;
documents: any;
versions: any;
showSkeleton: boolean;
query: any;
reference: any;
referenceRevision: any;
referenceRevisions: any;
onCorrect: () => void;
onCopyTranslation: () => void;
onCorrectAll: () => void;
onSelectPage: () => void;
onChangeDocument: () => void;
onChangeVersion: () => void;
onChangeReference: () => void;
onChangeQuery: () => void;
}
export default class ConflictsPage extends Component<Args> {}

View File

@ -1,6 +1,5 @@
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {gt} from '@ember/object/computed';
import Component from '@glimmer/component';
import IntlService from 'ember-intl/services/intl';
import GlobalState from 'accent-webapp/services/global-state';
@ -28,11 +27,17 @@ export default class RevisionExportOptions extends Component<Args> {
@service('global-state')
globalState: GlobalState;
@gt('mappedDocuments.length', 1)
showDocuments: boolean;
get showDocuments() {
return this.mappedDocuments.length > 1;
}
@gt('mappedVersions.length', 1)
showVersions: boolean;
get showVersions() {
return this.mappedVersions.length > 1;
}
get showSomeFilters() {
return this.showDocuments || this.showVersions;
}
@tracked
debouncedQuery = this.args.query;

View File

@ -1,5 +1,6 @@
import {inject as service} from '@ember/service';
import {equal} from '@ember/object/computed';
import {next} from '@ember/runloop';
import {action} from '@ember/object';
import Component from '@glimmer/component';
@ -105,6 +106,21 @@ export default class TranslationEditForm extends Component<Args> {
this.args.onKeyUp?.(value);
}
@action
handleFocus() {
this.args.onFocus?.();
}
@action
handleBlur() {
next(this, () => this.args.onBlur?.());
}
@action
handleSubmit() {
this.args.onSubmit();
}
@action
changeText(event: Event) {
const target = event.target as HTMLInputElement;

View File

@ -0,0 +1,212 @@
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {readOnly, equal, and} from '@ember/object/computed';
import Controller from '@ember/controller';
import translationCorrectQuery from 'accent-webapp/queries/correct-translation';
import translationUncorrectQuery from 'accent-webapp/queries/uncorrect-translation';
import translationUpdateQuery from 'accent-webapp/queries/update-translation';
import IntlService from 'ember-intl/services/intl';
import FlashMessages from 'ember-cli-flash/services/flash-messages';
import Apollo from 'accent-webapp/services/apollo';
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
import GlobalState from 'accent-webapp/services/global-state';
import {tracked} from '@glimmer/tracking';
const FLASH_MESSAGE_CORRECT_SUCCESS =
'pods.project.conflicts.flash_messages.correct_success';
const FLASH_MESSAGE_CORRECT_ERROR =
'pods.project.conflicts.flash_messages.correct_error';
const FLASH_MESSAGE_UNCORRECT_SUCCESS =
'pods.project.conflicts.flash_messages.uncorrect_success';
const FLASH_MESSAGE_UNCORRECT_ERROR =
'pods.project.conflicts.flash_messages.uncorrect_error';
const FLASH_MESSAGE_UPDATE_SUCCESS =
'pods.project.conflicts.flash_messages.update_success';
const FLASH_MESSAGE_UPDATE_ERROR =
'pods.project.conflicts.flash_messages.update_error';
export default class ConflictsController extends Controller {
@tracked
model: any;
@service('intl')
intl: IntlService;
@service('flash-messages')
flashMessages: FlashMessages;
@service('apollo')
apollo: Apollo;
@service('apollo-mutate')
apolloMutate: ApolloMutate;
@service('global-state')
globalState: GlobalState;
queryParams = [
'page',
'query',
'document',
'version',
'relatedRevisions',
'isTextEmpty',
'isTextNotEmpty',
'isAddedLastSync',
'isCommentedOn',
'isTranslatedFilter',
];
@tracked
query = '';
@tracked
document: string | null = null;
@tracked
version: string | null = null;
@tracked
relatedRevisions: string[] = [];
@tracked
isTextEmpty: 'true' | null = null;
@tracked
isTextNotEmpty: 'true' | null = null;
@tracked
isAddedLastSync: 'true' | null = null;
@tracked
isTranslated: 'true' | null = null;
@tracked
isCommentedOn: 'true' | null = null;
@tracked
page = 1;
@readOnly('globalState.permissions')
permissions: any;
@equal('model.groupedTranslations.entries', undefined)
emptyEntries: boolean;
@and('emptyEntries', 'model.loading')
showLoading: boolean;
@and('emptyEntries', 'model.loading')
showSkeleton: boolean;
get withAdvancedFilters() {
return [
this.isTextEmpty,
this.isTextNotEmpty,
this.isAddedLastSync,
this.isCommentedOn,
this.isTranslated,
].filter((filter) => filter === 'true').length;
}
@action
changeAdvancedFilterBoolean(
key:
| 'isTextEmpty'
| 'isTextNotEmpty'
| 'isAddedLastSync'
| 'isCommentedOn'
| 'isTranslated',
event: InputEvent
) {
this[key] = (event.target as HTMLInputElement).checked ? 'true' : null;
}
@action
async correctConflict(conflict: any, text: string) {
const response = await this.apolloMutate.mutate({
mutation: translationCorrectQuery,
variables: {
translationId: conflict.id,
text,
},
});
if (response.errors) {
this.flashMessages.error(this.intl.t(FLASH_MESSAGE_CORRECT_ERROR));
} else {
this.flashMessages.success(this.intl.t(FLASH_MESSAGE_CORRECT_SUCCESS));
}
return response;
}
@action
async uncorrectConflict(conflict: any, text: string) {
const response = await this.apolloMutate.mutate({
mutation: translationUncorrectQuery,
variables: {
translationId: conflict.id,
text,
},
});
if (response.errors) {
this.flashMessages.error(this.intl.t(FLASH_MESSAGE_UNCORRECT_ERROR));
} else {
this.flashMessages.success(this.intl.t(FLASH_MESSAGE_UNCORRECT_SUCCESS));
}
return response;
}
@action
async updateConflict(conflict: any, text: string) {
const response = await this.apolloMutate.mutate({
mutation: translationUpdateQuery,
variables: {
translationId: conflict.id,
text,
},
});
if (response.errors) {
this.flashMessages.error(this.intl.t(FLASH_MESSAGE_UPDATE_ERROR));
} else {
this.flashMessages.success(this.intl.t(FLASH_MESSAGE_UPDATE_SUCCESS));
}
return response;
}
@action
changeQuery(query: string) {
this.page = 1;
this.query = query;
}
@action
changeDocument(select: HTMLSelectElement) {
this.page = 1;
this.document = select.value ? select.value : null;
}
@action
changeVersion(select: HTMLSelectElement) {
this.page = 1;
this.version = select.value ? select.value : null;
}
@action
changeRelatedRevisions(choices: Array<{value: string}>) {
this.page = 1;
this.relatedRevisions = choices.map(({value}) => value);
}
@action
selectPage(page: number) {
window.scrollTo(0, 0);
this.page = page;
}
}

View File

@ -12,9 +12,7 @@ import promptConfigSaveQuery, {
} from 'accent-webapp/queries/save-project-prompt-config';
import promptConfigDeleteQuery from 'accent-webapp/queries/delete-project-prompt-config';
import promptDeleteQuery from 'accent-webapp/queries/delete-project-prompt';
import projectPromptConfigQuery, {
ProjectPromptConfigResponse,
} from 'accent-webapp/queries/project-prompt-config';
import projectPromptConfigQuery from 'accent-webapp/queries/project-prompt-config';
import {InMemoryCache} from '@apollo/client/cache';
const FLASH_MESSAGE_PREFIX = 'pods.project.edit.flash_messages.';
@ -125,21 +123,25 @@ export default class PromptsController extends Controller {
variables,
refetchQueries: ['Project'],
update: (cache: InMemoryCache) => {
const data = cache.readQuery({
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
}) as ProjectPromptConfigResponse;
const prompts = data.viewer.project.prompts.filter(
(prompt) => prompt.id !== variables.promptId
cache.updateQuery(
{
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
},
(data) => {
return {
viewer: {
...data.viewer,
project: {
...data.viewer.project,
prompts: data.viewer.project.prompts.filter(
(prompt: any) => prompt.id !== variables.promptId
),
},
},
};
}
);
data.viewer.project.prompts = prompts;
cache.writeQuery({
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
data,
});
},
});

View File

@ -12,9 +12,7 @@ import RouterService from '@ember/routing/router-service';
import promptCreateQuery, {
CreatePromptResponse,
} from 'accent-webapp/queries/create-project-prompt';
import projectPromptConfigQuery, {
ProjectPromptConfigResponse,
} from 'accent-webapp/queries/project-prompt-config';
import projectPromptConfigQuery from 'accent-webapp/queries/project-prompt-config';
import {InMemoryCache} from '@apollo/client/cache';
const FLASH_MESSAGE_PREFIX = 'pods.project.edit.flash_messages.';
@ -87,20 +85,25 @@ export default class PromptsNewController extends Controller {
data: {createProjectPrompt},
}: {data: {createProjectPrompt: CreatePromptResponse}}
) => {
const data = cache.readQuery({
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
}) as ProjectPromptConfigResponse;
const prompts = data.viewer.project.prompts.concat([
createProjectPrompt.prompt,
]);
data.viewer.project.prompts = prompts;
cache.writeQuery({
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
data,
});
cache.updateQuery(
{
query: projectPromptConfigQuery,
variables: {projectId: this.project.id},
},
(data) => {
return {
viewer: {
...data.viewer,
project: {
...data.viewer.project,
prompts: data.viewer.project.prompts.concat([
createProjectPrompt.prompt,
]),
},
},
};
}
);
},
});

View File

@ -1,159 +0,0 @@
import {camelize} from '@ember/string';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {readOnly, equal, empty, and} from '@ember/object/computed';
import Controller from '@ember/controller';
import translationCorrectQuery from 'accent-webapp/queries/correct-translation';
import IntlService from 'ember-intl/services/intl';
import FlashMessages from 'ember-cli-flash/services/flash-messages';
import Apollo from 'accent-webapp/services/apollo';
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
import GlobalState from 'accent-webapp/services/global-state';
import {tracked} from '@glimmer/tracking';
import projectTranslateTextQuery from 'accent-webapp/queries/translate-text-project';
const FLASH_MESSAGE_CORRECT_SUCCESS =
'pods.project.conflicts.flash_messages.correct_success';
const FLASH_MESSAGE_CORRECT_ERROR =
'pods.project.conflicts.flash_messages.correct_error';
const FLASH_MESSAGE_TRANSLATE_ERROR_PREFIX =
'pods.project.conflicts.flash_messages.translate_error';
const FLASH_MESSAGE_TRANSLATE_PROVIDER_ERROR =
'pods.project.conflicts.flash_messages.translate_provider_error';
export default class ConflictsController extends Controller {
@tracked
model: any;
@service('intl')
intl: IntlService;
@service('flash-messages')
flashMessages: FlashMessages;
@service('apollo')
apollo: Apollo;
@service('apollo-mutate')
apolloMutate: ApolloMutate;
@service('global-state')
globalState: GlobalState;
queryParams = ['page', 'query', 'document', 'version'];
@tracked
query = '';
@tracked
document: string | null = null;
@tracked
version: string | null = null;
@tracked
page = 1;
@readOnly('globalState.permissions')
permissions: any;
@equal('model.translations.entries', undefined)
emptyEntries: boolean;
@readOnly('model.revisionModel.project.revisions')
revisions: any;
@empty('document')
emptyDocument: boolean;
@equal('query', '')
emptyQuery: boolean;
@and('emptyEntries', 'model.loading')
showLoading: boolean;
@and('emptyEntries', 'model.loading', 'emptyQuery', 'emptyDocument')
showSkeleton: boolean;
@action
async copyTranslation(
text: string,
sourceLanguageSlug: string,
targetLanguageSlug: string
) {
const {data} = await this.apollo.client.query({
fetchPolicy: 'network-only',
query: projectTranslateTextQuery,
variables: {
text,
sourceLanguageSlug,
targetLanguageSlug,
projectId: this.model.project.id,
},
});
if (data.viewer?.project?.translatedText?.text) {
return data.viewer.project.translatedText;
} else if (data.viewer?.project?.translatedText?.error) {
const error = data.viewer?.project?.translatedText?.error;
const source = sourceLanguageSlug;
const target = targetLanguageSlug;
this.flashMessages.error(
this.intl.t(`${FLASH_MESSAGE_TRANSLATE_ERROR_PREFIX}.${error}`, {
source,
target,
})
);
} else {
const provider = camelize(data.viewer?.project?.translatedText?.provider);
this.flashMessages.error(
this.intl.t(FLASH_MESSAGE_TRANSLATE_PROVIDER_ERROR, {provider})
);
}
}
@action
async correctConflict(conflict: any, text: string) {
const response = await this.apolloMutate.mutate({
mutation: translationCorrectQuery,
variables: {
translationId: conflict.id,
text,
},
});
if (response.errors) {
this.flashMessages.error(this.intl.t(FLASH_MESSAGE_CORRECT_ERROR));
} else {
this.flashMessages.success(this.intl.t(FLASH_MESSAGE_CORRECT_SUCCESS));
}
return response;
}
@action
changeQuery(query: string) {
this.page = 1;
this.query = query;
}
@action
changeDocument(select: HTMLSelectElement) {
this.page = 1;
this.document = select.value ? select.value : null;
}
@action
changeVersion(select: HTMLSelectElement) {
this.page = 1;
this.version = select.value ? select.value : null;
}
@action
selectPage(page: number) {
window.scrollTo(0, 0);
this.page = page;
}
}

View File

@ -141,16 +141,25 @@
"reload_button": "Reload"
},
"conflicts_filters": {
"advanced_filters_button": "Advanced filters",
"total_entries_count": "{count, plural, =0 {No strings to review} =1 {1 string to review} other {# strings to review}}",
"reference_default_option_text": "No reference language",
"version_default_option_text": "Latest version",
"document_default_option_text": "All documents",
"input_placeholder_text": "Search for a string"
"input_placeholder_text": "Search for a string",
"advanced_filters_title": "Advanced filters",
"advanced_filters": {
"empty": "Text is empty (\"\" or null value)",
"not_empty": "Text is not empty",
"added_last_sync": "Strings added last sync",
"commented_on": "Strings with comments",
"translated": "To translate"
}
},
"conflicts_items": {
"translations_version_notice": "You are viewing strings to review for the version",
"conflicts_list": {
"translations_version_notice": "You are viewing the strings to be reviewed in the version",
"correct_all_button": "Mark all strings as reviewed for this language",
"no_translations": "No strings to review for: {query}",
"no_translations": "No strings found for: {query}",
"all_reviewed_title": "All reviewed",
"all_reviewed_subtitle": "All strings have been marked as reviewed"
},

View File

@ -157,13 +157,23 @@
"reload_button": "Recharger"
},
"conflicts_filters": {
"advanced_filters_button": "Filtres avancés",
"total_entries_count": "{count, plural, =0 {Aucune chaîne à vérifier} =1 {1 chaîne à vérifier} other {# chaînes à vérifier}}",
"reference_default_option_text": "Pas de langue de référence",
"version_default_option_text": "Dernière version",
"document_default_option_text": "Tous les documents",
"input_placeholder_text": "Rechercher une chaîne"
"input_placeholder_text": "Rechercher une chaîne",
"advanced_filters_title": "Filtres avancés",
"advanced_filters": {
"empty": "Le texte est vide (\"\" ou valeur nulle)",
"not_empty": "Le texte nest pas vide",
"added_last_sync": "Ajoutées à la dernière synchronisation",
"commented_on": "Chaînes avec commentaires",
"conflicted": "À réviser",
"translated": "À traduire"
}
},
"conflicts_items": {
"conflicts_list": {
"translations_version_notice": "Vous consultez les textes à réviser de la version",
"correct_all_button": "Marquer toutes les chaînes comme révisées pour cette langue",
"no_translations": "Aucune chaîne à examiner pour : {query}",

View File

@ -3,11 +3,16 @@ import {gql} from '@apollo/client/core';
export default gql`
query Conflicts(
$projectId: ID!
$revisionId: ID!
$query: String
$page: Int
$document: ID
$relatedRevisions: [ID!]
$version: ID
$isTextEmpty: Boolean
$isTextNotEmpty: Boolean
$isAddedLastSync: Boolean
$isCommentedOn: Boolean
$isTranslated: Boolean
) {
viewer {
project(id: $projectId) {
@ -34,30 +39,60 @@ export default gql`
name
}
revision(id: $revisionId) {
revisions {
id
isMaster
slug
name
translations(
query: $query
page: $page
pageSize: 20
document: $document
version: $version
isConflicted: true
) {
meta {
totalEntries
totalPages
currentPage
nextPage
previousPage
language {
id
slug
name
}
}
groupedTranslations(
query: $query
page: $page
pageSize: 20
document: $document
version: $version
relatedRevisions: $relatedRevisions
isConflicted: true
isTextEmpty: $isTextEmpty
isTextNotEmpty: $isTextNotEmpty
isAddedLastSync: $isAddedLastSync
isTranslated: $isTranslated
isCommentedOn: $isCommentedOn
) {
meta {
totalEntries
totalPages
currentPage
nextPage
previousPage
}
revisions {
id
}
entries {
key
document {
id
path
}
entries {
translations {
id
key
conflictedText
correctedText
valueType
isConflicted
isTranslated
lintMessages {
text
@ -83,31 +118,6 @@ export default gql`
rtl
}
}
relatedTranslations {
id
correctedText
isConflicted
revision {
id
isMaster
name
slug
rtl
language {
id
name
slug
rtl
}
}
}
document {
id
path
}
}
}
}

View File

@ -44,6 +44,7 @@ export default gql`
id
translations(
query: $query
pageSize: 50
page: $page
document: $document
version: $version

View File

@ -64,9 +64,10 @@ export default Router.map(function () {
});
this.route('comments', {path: 'conversation'});
this.route('conflicts');
this.route('revision', {path: 'revisions/:revisionId'}, function () {
this.route('translations');
this.route('conflicts');
this.route('lint-translations');
});

View File

@ -42,6 +42,9 @@ export default class IndexRoute extends Route {
}: {query: any; page: number; document: any; version: any},
transition: Transition
) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationsQuery,

View File

@ -18,6 +18,9 @@ export default class TranslationRoute extends Route {
subscription: Subscription;
model({translationId}: {translationId: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationQuery,

View File

@ -53,6 +53,9 @@ export default class ActivitiesRoute extends Route {
},
transition: Transition
) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectActivitiesQuery,

View File

@ -19,6 +19,9 @@ export default class Activity extends Route {
subscription: Subscription;
model({activityId}: {activityId: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectActivityQuery,

View File

@ -19,6 +19,9 @@ export default class CollaboratorsRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectCollaboratorsQuery,

View File

@ -25,6 +25,9 @@ export default class CommentsRoute extends Route {
subscription: Subscription;
model({page}: {page: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
const pageNumber = page ? parseInt(page, 10) : null;
this.subscription = this.apolloSubscription.graphql(

View File

@ -8,7 +8,7 @@ import ApolloSubscription, {
} from 'accent-webapp/services/apollo-subscription';
import RouteParams from 'accent-webapp/services/route-params';
import Transition from '@ember/routing/transition';
import ConflictsController from 'accent-webapp/controllers/logged-in/project/revision/conflicts';
import ConflictsController from 'accent-webapp/controllers/logged-in/project/conflicts';
import RouterService from '@ember/routing/router-service';
export default class ConflictsRoute extends Route {
@ -34,44 +34,58 @@ export default class ConflictsRoute extends Route {
version: {
refreshModel: true,
},
relatedRevisions: {
refreshModel: true,
},
isTextEmpty: {
refreshModel: true,
},
isTextNotEmpty: {
refreshModel: true,
},
isAddedLastSync: {
refreshModel: true,
},
isCommentedOn: {
refreshModel: true,
},
isConflicted: {
refreshModel: true,
},
isTranslated: {
refreshModel: true,
},
};
subscription: Subscription;
model(
{
query,
page,
document,
version,
}: {query: any; page: number; document: any; version: any},
transition: Transition
) {
model(params: any, transition: Transition) {
params.isTextEmpty = params.isTextEmpty === 'true' ? true : null;
params.isTextNotEmpty = params.isTextNotEmpty === 'true' ? true : null;
params.isAddedLastSync = params.isAddedLastSync === 'true' ? true : null;
params.isCommentedOn = params.isCommentedOn === 'true' ? true : null;
params.isTranslated = params.isTranslated === 'true' ? false : null;
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationsQuery,
{
props: (data) => ({
revisionModel: this.modelFor('logged-in.project.revision'),
projectModel: this.modelFor('logged-in.project'),
prompts: data.viewer.project.prompts,
documents: data.viewer.project.documents.entries,
versions: data.viewer.project.versions.entries,
prompts: data.viewer.project.prompts,
revisions: data.viewer.project.revisions,
relatedRevisions: data.viewer.project.groupedTranslations.revisions,
project: data.viewer.project,
translations: data.viewer.project.revision.translations,
groupedTranslations: data.viewer.project.groupedTranslations,
}),
options: {
fetchPolicy: 'cache-and-network',
variables: {
projectId: this.routeParams.fetch(transition, 'logged-in.project')
.projectId,
revisionId: this.routeParams.fetch(
transition,
'logged-in.project.revision'
).revisionId,
query,
page,
document,
version,
...params,
},
},
}
@ -98,30 +112,4 @@ export default class ConflictsRoute extends Route {
onRefresh() {
this.refresh();
}
@action
onRevisionChange({revisionId}: {revisionId: string}) {
const {project} = this.modelFor('logged-in.project') as {project: any};
this.apolloSubscription.clearSubscription(this.subscription);
this.router.transitionTo(
'logged-in.project.revision.conflicts',
project.id,
revisionId,
{
queryParams: this.fetchQueryParams(
this.controller as ConflictsController
),
}
);
}
private fetchQueryParams(controller: ConflictsController) {
const query = controller.query;
return {
query,
};
}
}

View File

@ -18,6 +18,9 @@ export default class APITokenRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectApiTokenQuery,

View File

@ -19,6 +19,9 @@ export default class BadgesRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectEditQuery,

View File

@ -19,6 +19,9 @@ export default class ProjectEditIndexRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectEditQuery,

View File

@ -18,6 +18,9 @@ export default class MachineTranslationsRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectMachineTranslationsConfigQuery,

View File

@ -18,6 +18,9 @@ export default class PomptsRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectPromptConfigQuery,

View File

@ -19,6 +19,9 @@ export default class ServiceIntegrationsController extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectServiceIntegrationsQuery,

View File

@ -34,6 +34,9 @@ export default class FilesRoute extends Route {
}: {page: string; excludeEmptyTranslations: boolean},
transition: Transition
) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
const pageNumber = page ? parseInt(page, 10) : null;
this.subscription = this.apolloSubscription.graphql(

View File

@ -18,6 +18,9 @@ export default class ProjectIndexRoute extends Route {
subscription: Subscription;
model(_params: object, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
const props = (data: any) => ({project: data.viewer.project});
this.subscription = this.apolloSubscription.graphql(

View File

@ -20,6 +20,9 @@ export default class ManageLanguagesRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
projectNewLanguageQuery,

View File

@ -43,6 +43,9 @@ export default class LintRoute extends Route {
};
model(params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
lintQuery,

View File

@ -57,6 +57,9 @@ export default class TranslationsRoute extends Route {
subscription: Subscription;
model(params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
params.isTextEmpty = params.isTextEmpty === 'true' ? true : null;
params.isTextNotEmpty = params.isTextNotEmpty === 'true' ? true : null;
params.isAddedLastSync = params.isAddedLastSync === 'true' ? true : null;

View File

@ -18,6 +18,9 @@ export default class TranslationsRoute extends Route {
subscription: Subscription;
model({translationId}: {translationId: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationQuery,

View File

@ -26,6 +26,9 @@ export default class ActivitiesRoute extends Route {
subscription: Subscription;
model({page}: {page: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationActivitiesQuery,

View File

@ -26,6 +26,9 @@ export default class CommentsRoute extends Route {
subscription: Subscription;
model({page}: {page: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
const pageNumber = page ? parseInt(page, 10) : null;
this.subscription = this.apolloSubscription.graphql(

View File

@ -23,6 +23,9 @@ export default class TranslationEditionsRoute extends Route {
subscription: Subscription;
model(_params: any, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
translationEditionsQuery,

View File

@ -25,6 +25,9 @@ export default class VersionsRoute extends Route {
subscription: Subscription;
model({page}: {page: string}, transition: Transition) {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
const pageNumber = page ? parseInt(page, 10) : null;
this.subscription = this.apolloSubscription.graphql(

View File

@ -21,6 +21,9 @@ export default class LoginRoute extends Route {
subscription: Subscription;
model() {
if (this.subscription)
this.apolloSubscription.clearSubscription(this.subscription);
this.subscription = this.apolloSubscription.graphql(
() => this.modelFor(this.routeName),
authenticationProvidersQuery,

View File

@ -15,9 +15,11 @@ export default class ApolloMutate extends Service {
const operationName = Object.keys(data)[0];
if (!data[operationName]?.errors?.length) {
data[operationName].errors = null;
return data[operationName];
const updatedData = {
data,
[operationName]: {...data[operationName], errors: null},
};
return updatedData;
}
return data[operationName];

View File

@ -1,23 +1,16 @@
.filters-wrapper {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.conflicts-filters {
:global(.filters) {
margin-bottom: 20px;
.filters-content {
flex-grow: 1;
margin-right: 15px;
}
.queryForm {
position: relative;
}
.totalEntries {
flex-shrink: 0;
margin-top: 10px;
color: var(--color-grey);
font-size: 12px;
:global(.ember-power-select-multiple-trigger) {
width: 100%;
border-width: 2px;
margin-bottom: 10px;
&:focus {
border-width: 2px;
}
}
}
}
.search-icon {
@ -47,10 +40,25 @@
}
}
.totalEntries {
margin-top: 10px;
color: var(--color-grey);
font-size: 12px;
button.advancedFilters {
position: relative;
box-shadow: none;
flex-shrink: 0;
&:focus,
&:hover {
transform: translate3d(0, 0, 0);
}
}
.advancedFilters-badge {
position: absolute;
top: -4px;
right: -7px;
background: var(--color-primary);
border-radius: var(--border-radius);
padding: 0 4px 1px;
color: #fff;
font-size: 10px;
}
@media (max-width: 440px) {

View File

@ -1,9 +1,17 @@
.conflicts-items {
position: relative;
margin-top: 20px;
.conflicts-header {
position: sticky;
padding: 14px 0 0 15px;
top: 0;
z-index: 4;
display: grid;
grid-template-columns: repeat(var(--group-columns-count, 1), 1fr);
font-size: 16px;
font-weight: bold;
margin: 0 0 10px -14px;
background: var(--content-background);
}
.conflicts-items-version {
.conflicts-list-version {
margin: 0 0 15px;
padding: 15px;
border-radius: var(--border-radius);
@ -16,7 +24,7 @@
color: var(--color-blue);
}
.conflicts-items-version-tag {
.conflicts-list-version-tag {
display: inline-flex;
align-items: center;
margin-left: 2px;
@ -26,6 +34,17 @@
text-transform: none;
}
.conflicts-header-item {
padding-bottom: 10px;
}
.conflicts-header-item-slug {
font-weight: normal;
opacity: 0.6;
margin-left: 2px;
font-size: 12px;
}
.all-reviewed {
max-width: 400px;
margin: 100px auto 20px;
@ -43,7 +62,3 @@
font-size: 12px;
color: var(--color-grey);
}
.empty-content {
margin-top: 40px;
}

View File

@ -0,0 +1,31 @@
.conflicts-list-advanced-filters {
margin: 15px 0 0 0;
display: flex;
flex-direction: column;
}
.title {
text-transform: uppercase;
color: var(--text-color-normal);
font-size: 11px;
font-weight: bold;
}
.label {
display: flex;
align-items: flex-start;
width: 100%;
max-width: 250px;
margin-top: 10px;
font-size: 12px;
input {
margin-right: 6px;
}
}
.labels {
display: flex;
flex-wrap: wrap;
max-width: 960px;
}

View File

@ -0,0 +1,110 @@
.item {
--grid-item-reviewed-opacity: 0.5;
--grid-item-actions-opacity: 0;
border-bottom: 1px solid var(--background-light-highlight);
transition: 0.2s ease-in-out;
transition-property: border-color;
background: var(--content-background);
:global(.lint-translations-item) {
display: none;
padding: 2px 6px;
border-radius: var(--border-radius);
margin: 0 15px 10px;
background: var(--background-light);
}
&.item--focus {
background: var(--background-light);
:global(.lint-translations-item) {
display: block;
}
.item-key {
transform: translateY(2px);
}
--grid-item-reviewed-opacity: 1;
--grid-item-actions-opacity: 1;
}
}
.item-grid {
display: grid;
grid-template-columns: repeat(var(--group-columns-count, 1), 1fr);
}
.item-grid-item {
--border-color: var(--background-light-highlight);
position: relative;
padding: 0;
border-left: 2px solid var(--border-color);
opacity: 1;
&.item-grid-item--translated {
--border-color: var(--color-warning);
opacity: 1;
}
&.item-grid-item--reviewed {
--border-color: var(--color-green);
opacity: var(--grid-item-reviewed-opacity);
}
&:hover {
opacity: 1;
}
&::before {
position: absolute;
content: '';
top: -24px;
left: -2px;
height: 32px;
width: 2px;
background: var(--border-color);
}
}
.item-key-prefix {
display: inline-flex;
font-size: 11px;
color: #959595;
gap: 6px;
font-weight: 300;
&::before {
content: '/';
}
}
.item-key {
position: sticky;
top: 46px;
display: inline-flex;
align-items: center;
gap: 5px;
background: var(--content-background);
padding: 2px 6px;
text-decoration: none;
font-family: var(--font-monospace);
box-shadow: 0 2px 4px var(--shadow-color);
border-radius: var(--border-radius);
font-weight: 600;
z-index: 2;
word-break: break-all;
transition: 0.2s ease-in-out;
transition-property: color;
margin-left: 4px;
line-height: 1.5;
font-size: 11px;
color: var(--text-color-normal);
transition: 0.2s ease-in-out;
transition-property: transform;
}
.key {
text-decoration: none;
}

View File

@ -1,19 +1,13 @@
.root {
&:nth-child(even) {
background: var(--background-light);
}
}
.conflict-item {
padding: 10px;
margin-bottom: 15px;
border-radius: var(--border-radius);
.translation-item {
&:hover {
.form-helpers {
pointer-events: all;
opacity: 1;
}
.button-submit {
pointer-events: all;
opacity: 1;
}
}
}
@ -36,6 +30,7 @@
.item-details__column {
align-items: flex-end;
}
.item-details__column:first-of-type {
margin-right: 0;
margin-left: 15px;
@ -67,105 +62,11 @@
margin-right: 15px;
}
.item-detail-conflict {
display: flex;
width: 100%;
.translation-item.resolved {
background: var(--color-primary-opacity-10);
}
.item-detail-conflict-actions {
margin-left: 6px;
flex-shrink: 0;
flex-grow: 1;
}
.item-detail-conflict-actions-flag,
.item-detail-conflict-actions-link {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 3px;
border-radius: 50%;
width: 24px;
height: 24px;
background: transparent;
color: var(--color-black);
line-height: 1;
}
.item-detail-conflict-actions-link--loading {
pointer-events: none;
background: transparent;
&:focus,
&:hover {
background: transparent;
}
}
.item-detail-conflict-actions-flag--link,
.item-detail-conflict-actions-link {
cursor: pointer;
&:focus,
&:hover {
background: var(--background-light-highlight);
}
}
.item-detail-conflict-actions-flag {
position: relative;
top: -1px;
padding: 0 5px;
margin-left: 0;
margin-right: 5px;
width: auto;
border-radius: 5px;
border: 1px solid var(--background-light-highlight);
font-size: 10px;
text-transform: uppercase;
text-decoration: none;
&:last-of-type {
margin-right: 0;
}
}
.item-detail-conflict-actions-flag--link {
&:focus,
&:hover {
color: var(--content-background);
border-color: var(--color-primary);
background: var(--color-primary);
}
}
.item-detail-conflict-actions-icon {
width: 12px;
height: 12px;
}
.conflict-item.resolved {
padding: 5px 10px;
margin: 5px 0;
background: hsl(
var(--color-green-hue),
var(--color-green-saturation),
var(--color-highlight-lighteness)
);
.item-key-prefix {
opacity: 0.7;
color: var(--color-green);
}
.item-key {
margin-bottom: 0;
font-size: 12px;
color: var(--color-green);
}
}
.conflict-item.errored {
.translation-item.errored {
.textInput {
border-color: var(--color-error);
}
@ -177,70 +78,18 @@
color: var(--color-error);
}
.item-key-prefix {
display: inline-flex;
font-size: 11px;
color: #959595;
gap: 6px;
font-weight: 300;
&::before {
content: '/';
}
}
.item-key {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 12px;
font-family: var(--font-monospace);
word-break: break-all;
transition: 0.2s ease-in-out;
transition-property: color;
margin-right: 15px;
line-height: 1.5;
font-size: 11px;
color: var(--color-primary);
}
.key {
text-decoration: none;
}
.textResolved {
display: flex;
align-items: flex-start;
}
.textResolved-content {
flex-grow: 1;
}
.textResolved-text {
margin-bottom: 5px;
font-size: 13px;
}
.textDiff {
padding: 8px 0 3px;
color: var(--color-grey);
}
.textDiff {
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
}
.button-submit {
display: flex;
justify-content: flex-end;
position: absolute;
gap: 10px;
top: 10px;
pointer-events: none;
opacity: var(--grid-item-actions-opacity);
gap: 0;
bottom: 15px;
right: 10px;
z-index: 3;
transition: 0.2s ease-in-out;
transition-property: opacity;
&[data-dir='rtl'] {
right: auto;
@ -249,34 +98,11 @@
}
}
.item-revision {
flex-shrink: 0;
margin-right: 4px;
color: var(--color-black);
opacity: 0.6;
text-decoration: none;
font-weight: bold;
font-size: 12px;
text-align: right;
&:focus,
&:hover {
opacity: 1;
color: var(--color-primary);
}
}
.item-revision {
opacity: 1;
padding: 4px 0 3px;
margin: 0 9px 0 0;
font-size: 13px;
}
.textInput {
flex-grow: 1;
flex-shrink: 0;
width: 100%;
font-size: 13px;
}
.item-text {
@ -297,19 +123,6 @@
}
}
.conflictedText-references {
width: 100%;
padding-left: 4px;
}
.conflictedText-references-conflicted {
display: flex;
align-items: flex-start;
margin-top: 7px;
font-size: 12px;
color: var(--color-black);
}
.form-helpers {
pointer-events: none;
opacity: 0;
@ -318,24 +131,3 @@
transition: 0.2s ease-in-out;
transition-property: opacity;
}
.conflictedText-references-conflicted-label {
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
margin-right: 10px;
margin-top: 4px;
font-weight: 500;
font-size: 11px;
text-align: right;
}
.conflictedText-references-conflicted-value {
white-space: pre-wrap;
}
.conflictedText-references-conflicted-icon {
width: 12px;
height: 12px;
}

View File

@ -1,107 +0,0 @@
.item {
display: flex;
align-items: flex-start;
flex-direction: row;
padding: 5px 0 3px;
color: var(--color-grey);
font-size: 13px;
}
.item-link {
display: inline-flex;
place-items: center;
flex-shrink: 0;
margin-right: 2px;
color: var(--color-black);
opacity: 0.8;
text-decoration: none;
padding: 1px 5px;
background: var(--background-light-highlight);
border-radius: var(--border-radius);
font-size: 11px;
transition: 0.2s ease-in-out;
transition-property: opacity;
&:focus,
&:hover {
opacity: 1;
}
}
.item-text {
width: 100%;
display: flex;
align-items: flex-start;
}
.item-text-content {
display: block;
white-space: pre-wrap;
margin: 0 0 0 5px;
color: var(--color-black);
opacity: 0.8;
font-size: 12px;
}
.item-actions {
margin-left: 6px;
flex-shrink: 0;
flex-grow: 0;
}
.item-actions-link {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
height: 17px;
width: 17px;
padding: 3px;
background: transparent;
color: var(--color-black);
background: var(--color-primary-opacity-20);
line-height: 1;
}
.item-actions-link--loading {
pointer-events: none;
background: transparent;
&:focus,
&:hover {
background: transparent;
}
}
.item-actions-flag--link,
.item-actions-link {
cursor: pointer;
&:focus,
&:hover {
background: var(--background-light-highlight);
}
}
.item-actions-flag {
position: relative;
top: -1px;
padding: 0 5px;
margin-left: 0;
margin-right: 5px;
width: auto;
border-radius: 5px;
border: 1px solid var(--background-light-highlight);
font-size: 10px;
text-transform: uppercase;
text-decoration: none;
&:last-of-type {
margin-right: 0;
}
}
.item-actions-icon {
width: 10px;
height: 10px;
}

View File

@ -1,3 +0,0 @@
.conflicts-page {
background: var(--content-background);
}

View File

@ -37,7 +37,7 @@
align-items: center;
justify-content: center;
width: 100%;
margin: 20px auto 40px;
margin: 20px auto 10px;
}
.numberStat-reviewPercentage {
@ -92,11 +92,10 @@
.stats-title {
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
margin-bottom: 0;
margin-bottom: 10px;
padding-bottom: 2px;
margin-left: -8px;
}
.slaves {
@ -146,7 +145,7 @@
}
.master {
margin: 0 0 10px;
margin: 25px 0 5px;
}
.stats-title-links {

View File

@ -8,7 +8,7 @@
margin-bottom: 20px;
margin-right: 20px;
border-radius: var(--border-radius);
border: 1px solid var(--background-light-highlight);
box-shadow: 0 6px 25px var(--shadow-color), 0 2px 7px var(--shadow-color);
&:nth-child(even) {
flex: 1 1 50%;

View File

@ -64,13 +64,13 @@ button.button {
opacity: 1;
}
.prompt-button-quick-access[data-rtl] {
left: 36px;
transform: translateX(36px);
right: auto;
}
.prompt-button-quick-access {
opacity: 1;
right: 36px;
transform: translateX(-36px);
padding: 0 7px;
top: -1px;
pointer-events: all;
@ -87,6 +87,7 @@ button.button {
background: var(--input-background);
opacity: 0;
pointer-events: none;
transform: translateX(0);
right: 0;
display: flex;
gap: 4px;

View File

@ -68,12 +68,6 @@
color: var(--color-primary);
}
.details-associations-overflow {
max-height: 400px;
overflow-y: scroll;
margin-top: -12px;
}
.details-associations-pagination {
border-top: 1px solid var(--content-background-border);
}

View File

@ -34,7 +34,7 @@
}
:global([data-theme='dark']) .list-item-link:global(.active) {
color: rgba(255, 255, 255, 0.4);
color: var(--color-primary);
}
.list-item-link {

View File

@ -25,6 +25,7 @@
font-size: 13px;
transition: 0.2s ease-in-out;
transition-property: background, box-shadow, color, transform;
overflow: hidden;
strong {
padding: 10px 20px 5px;

View File

@ -1,11 +1,11 @@
.wrapper {
display: flex;
flex-direction: column;
margin-top: 25px;
}
.content {
flex: 1 1 auto;
padding-right: 30px;
margin-top: 20px;
}
.list {
@ -13,10 +13,10 @@
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
max-width: 450px;
}
.config {
flex: 0 1 auto;
max-width: 450px;
width: 100%;
}
@ -30,5 +30,5 @@
.text {
display: block;
font-size: 13px;
padding-bottom: 22px;
margin-bottom: 12px;
}

View File

@ -1,5 +1,4 @@
.form {
margin-top: 25px;
padding: 16px;
border-radius: var(--border-radius);
background: var(--background-light);
@ -31,7 +30,6 @@
}
.select select {
background: transparent;
border: 1px solid var(--background-light-highlight);
padding: 7px 10px;
font-weight: bold;
@ -71,5 +69,5 @@
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
margin-top: 6px;
}

View File

@ -1,4 +1,4 @@
div.filters {
.filters:global(.filters) {
margin: 20px auto 0;
padding: 0 20px;
max-width: var(--screen-lg);

View File

@ -7,6 +7,7 @@
.exportOptions {
display: flex;
justify-content: space-between;
width: 100%;
}
.exportOptions-advancedFilters {

View File

@ -74,12 +74,21 @@
width: 100%;
height: 100%;
padding: 10px 130px 10px 10px;
font-size: 12px;
&[dir='rtl'] {
padding: 10px 10px 10px 130px;
}
&.inputText--borderless {
border-color: transparent;
background: transparent;
&:focus {
border-color: transparent;
background: transparent;
}
}
&::placeholder {
opacity: 0.2;
font-style: italic;

View File

@ -4,11 +4,6 @@
align-items: flex-start;
}
.filters-content {
flex-grow: 1;
margin-right: 15px;
}
.queryForm {
position: relative;
}
@ -43,6 +38,7 @@
button.advancedFilters {
position: relative;
box-shadow: none;
flex-shrink: 0;
&:focus,
&:hover {
transform: translate3d(0, 0, 0);
@ -60,13 +56,6 @@ button.advancedFilters {
font-size: 10px;
}
.totalEntries {
flex-shrink: 0;
margin-top: 10px;
color: var(--color-grey);
font-size: 12px;
}
@media (max-width: 440px) {
.filters-wrapper {
flex-direction: column;

View File

@ -160,6 +160,7 @@
.item-meta {
display: flex;
align-items: center;
gap: 3px;
}
.item-text {

View File

@ -235,6 +235,10 @@
}
}
.button--borderLess {
border-color: transparent;
}
.button--grey {
--button-text-color: var(--color-grey);
--button-text-hover-color: var(--color-grey);

View File

@ -10,10 +10,11 @@
background: none;
border-color: transparent;
box-shadow: none;
padding: 0;
}
.ember-power-select-trigger {
padding: 5px 10px;
padding: 5px 27px 5px 5px;
background: var(--content-background);
box-shadow: none;
border: 1px solid transparent;
@ -30,13 +31,28 @@
.filters-wrapper {
position: relative;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.filters-content {
display: flex;
flex-direction: column;
gap: 10px;
flex-grow: 1;
}
.queryForm {
position: relative;
display: flex;
gap: 10px;
}
.queryForm-filters {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
}
.queryForm-filters-column {

View File

@ -70,6 +70,65 @@
}
}
.ember-power-select-multiple-options
.ember-power-select-multiple-option:first-of-type {
span[role='button'] {
display: none;
}
}
.ember-power-select-multiple-options {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
list-style: none;
& li.ember-power-select-trigger-multiple-input-container {
flex-grow: 1;
display: flex;
& input {
flex-grow: 1;
}
}
}
.ember-power-select-trigger-multiple-input {
font-family: inherit;
font-size: inherit;
border: none;
line-height: inherit;
-webkit-appearance: none;
outline: none;
padding: 0;
background-color: transparent;
text-indent: 2px;
}
html[data-theme='dark'] .ember-power-select-multiple-option {
color: var(--color-primary);
}
.ember-power-select-multiple-option {
display: inline-block;
background: var(--color-primary-opacity-25);
color: var(--color-primary-darken-50);
border-radius: var(--border-radius);
padding: 3px 9px;
font-size: 14px;
}
.ember-power-select-multiple-remove-btn {
cursor: pointer;
&:not(:hover) {
opacity: 0.5;
}
}
.ember-power-select-status-icon {
position: absolute;
right: 0;

View File

@ -17,6 +17,10 @@
<PowerSelect @options={{@options}} @selected={{@selected}} @placeholder={{@placeholder}} @renderInPlace={{@renderInPlace}} @onChange={{@onchange}} as |option|>
{{option.label}}
</PowerSelect>
{{else if @multi}}
<PowerSelectMultiple @options={{@options}} @selected={{@selected}} @placeholder={{@placeholder}} @renderInPlace={{@renderInPlace}} @onChange={{@onchange}} as |option|>
{{option.label}}
</PowerSelectMultiple>
{{else}}
<div local-class='root' ...attributes>
<select {{on 'change' this.selectChange}}>

View File

@ -2,6 +2,18 @@
<div class='filters'>
<form class='filters-wrapper' local-class='filters-wrapper' {{on 'submit' (fn this.submitForm)}}>
<div class='filters-content' local-class='filters-content'>
{{#if this.showRevisionsSelect}}
<AccSelect
@matchTriggerWidth={{false}}
@searchEnabled={{false}}
@multi={{true}}
@selected={{this.relatedRevisionsValue}}
@options={{this.mappedRevisionsOptions}}
@onchange={{fn @onChangeRevisions}}
/>
{{/if}}
<div class='queryForm' local-class='queryForm'>
{{inline-svg '/assets/search.svg' local-class='search-icon'}}
@ -13,46 +25,66 @@
{{on-key 'Enter' (fn this.submitForm)}}
{{on 'keyup' this.setDebouncedQuery}}
/>
{{#if @onChangeAdvancedFilterBoolean}}
<button {{on 'click' (fn this.toggleAdvancedFilters)}} local-class='advancedFilters' class='button button--filled button--white'>
{{inline-svg 'assets/filter.svg' class='button-icon'}}
{{t 'components.conflicts_filters.advanced_filters_button'}}
{{#if @withAdvancedFilters}}
<span local-class='advancedFilters-badge'>
{{@withAdvancedFilters}}
</span>
{{/if}}
</button>
{{/if}}
</div>
<div class='queryForm-filters'>
<div class='queryForm-filters-column'>
{{#if this.showDocumentsSelect}}
<div class='queryForm-filter'>
<div class='queryForm-filter-select'>
<AccSelect
@matchTriggerWidth={{false}}
@searchEnabled={{false}}
@selected={{this.documentValue}}
@options={{this.mappedDocuments}}
@onchange={{fn @onChangeDocument}}
/>
{{#if this.showSomeFilters}}
<div class='queryForm-filters'>
<div class='queryForm-filters-column'>
{{#if this.showDocumentsSelect}}
<div class='queryForm-filter'>
<div class='queryForm-filter-select'>
<AccSelect
@matchTriggerWidth={{false}}
@searchEnabled={{false}}
@selected={{this.documentValue}}
@options={{this.mappedDocuments}}
@onchange={{fn @onChangeDocument}}
/>
</div>
</div>
</div>
{{/if}}
{{/if}}
{{#if this.showVersionsSelect}}
<div class='queryForm-filter'>
<div class='queryForm-filter-select'>
<AccSelect
@matchTriggerWidth={{false}}
@searchEnabled={{false}}
@selected={{this.versionValue}}
@options={{this.mappedVersions}}
@onchange={{fn @onChangeVersion}}
/>
{{#if this.showVersionsSelect}}
<div class='queryForm-filter'>
<div class='queryForm-filter-select'>
<AccSelect
@matchTriggerWidth={{false}}
@searchEnabled={{false}}
@selected={{this.versionValue}}
@options={{this.mappedVersions}}
@onchange={{fn @onChangeVersion}}
/>
</div>
</div>
</div>
{{/if}}
{{/if}}
</div>
</div>
</div>
</div>
{{/if}}
{{#if @meta.totalEntries}}
<span local-class='totalEntries'>
{{t 'components.conflicts_filters.total_entries_count' count=@meta.totalEntries}}
</span>
{{/if}}
{{#if this.displayAdvancedFilters}}
<ConflictsList::AdvancedFilters
@isTextEmptyFilter={{@isTextEmptyFilter}}
@isTextNotEmptyFilter={{@isTextNotEmptyFilter}}
@isAddedLastSyncFilter={{@isAddedLastSyncFilter}}
@isCommentedOnFilter={{@isCommentedOnFilter}}
@isTranslatedFilter={{@isTranslatedFilter}}
@onChangeAdvancedFilterBoolean={{@onChangeAdvancedFilterBoolean}}
/>
{{/if}}
</div>
</form>
</div>
</div>

View File

@ -1,34 +1,49 @@
<ul local-class='conflicts-items'>
{{#if this.currentVersion}}
<div local-class='conflicts-items-version'>
{{#if this.currentVersion}}
<div local-class='conflicts-list-version'>
{{t 'components.conflicts_items.translations_version_notice'}}
<span local-class='conflicts-items-version-tag'>{{this.currentVersion.tag}}</span>
</div>
{{/if}}
{{#each @conflicts key='id' as |conflict index|}}
<ConflictsList::Item
{{t 'components.conflicts_list.translations_version_notice'}}
<span local-class='conflicts-list-version-tag'>{{this.currentVersion.tag}}</span>
</div>
{{/if}}
{{#if this.showRevisionsHeader}}
<ul local-class='conflicts-header' style='--group-columns-count: {{this.revisions.length}};'>
{{#each this.mappedRevisions as |revision|}}
<li local-class='conflicts-header-item'>
{{revision.name}}
<span local-class='conflicts-header-item-slug'>
{{revision.slug}}
</span>
</li>
{{/each}}
</ul>
{{/if}}
<ul local-class='conflicts-items' style='--group-columns-count: {{this.revisions.length}};'>
{{#each @groupedTranslations key='id' as |groupedTranslation index|}}
<ConflictsList::Group
@index={{index}}
@permissions={{@permissions}}
@revisions={{@revisions}}
@revisions={{this.revisions}}
@project={{@project}}
@prompts={{@prompts}}
@conflict={{conflict}}
@groupedTranslation={{groupedTranslation}}
@onCorrect={{@onCorrect}}
@onCopyTranslation={{@onCopyTranslation}}
@onUncorrect={{@onUncorrect}}
@onUpdate={{@onUpdate}}
@selectedTranslationId={{this.selectedTranslationId}}
@onFocus={{this.handleFocus}}
/>
{{else if @query}}
<EmptyContent local-class='empty-content' @center={{true}} @iconPath='assets/empty.svg' @text={{t 'components.conflicts_items.no_translations' query=@query}} />
{{else}}
<div local-class='all-reviewed'>
<img local-class='all-reviewed-image' src='/assets/all-reviewed-splash.svg' />
<div local-class='all-reviewed-title'>
{{t 'components.conflicts_items.all_reviewed_title'}}
{{t 'components.conflicts_list.all_reviewed_title'}}
</div>
<div local-class='all-reviewed-subtitle'>
{{t 'components.conflicts_items.all_reviewed_subtitle'}}
{{t 'components.conflicts_list.all_reviewed_subtitle'}}
</div>
</div>
{{/each}}

View File

@ -0,0 +1,42 @@
<div local-class='conflicts-list-advanced-filters'>
<span local-class='title'>
{{t 'components.conflicts_filters.advanced_filters_title'}}
</span>
<div local-class='labels'>
<label local-class='label'>
<input type='checkbox' checked={{@isTextEmptyFilter}} {{on 'change' (fn @onChangeAdvancedFilterBoolean 'isTextEmpty')}} />
<span class='label-text'>
{{t 'components.conflicts_filters.advanced_filters.empty'}}
</span>
</label>
<label local-class='label'>
<input type='checkbox' checked={{@isTextNotEmptyFilter}} {{on 'change' (fn @onChangeAdvancedFilterBoolean 'isTextNotEmpty')}} />
<span>
{{t 'components.conflicts_filters.advanced_filters.not_empty'}}
</span>
</label>
<label local-class='label'>
<input type='checkbox' checked={{@isAddedLastSyncFilter}} {{on 'change' (fn @onChangeAdvancedFilterBoolean 'isAddedLastSync')}} />
<span>
{{t 'components.conflicts_filters.advanced_filters.added_last_sync'}}
</span>
</label>
<label local-class='label'>
<input type='checkbox' checked={{@isCommentedOnFilter}} {{on 'change' (fn @onChangeAdvancedFilterBoolean 'isCommentedOn')}} />
<span>
{{t 'components.conflicts_filters.advanced_filters.commented_on'}}
</span>
</label>
<label local-class='label'>
<input type='checkbox' checked={{@isTranslatedFilter}} {{on 'change' (fn @onChangeAdvancedFilterBoolean 'isTranslated')}} />
<span>
{{t 'components.conflicts_filters.advanced_filters.translated'}}
</span>
</label>
</div>
</div>

View File

@ -0,0 +1,38 @@
<li local-class='item {{if this.isFocused "item--focus"}}'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id this.masterTranslation.id}} local-class='item-key' tabindex='-1'>
{{this.translationKey.value}}
<small local-class='item-key-prefix'>
{{#if this.translationKey.prefix}}
{{this.translationKey.prefix}}
{{else}}
{{@groupedTranslation.document.path}}
{{/if}}
</small>
</LinkTo>
<ul local-class='item-grid'>
{{#each @groupedTranslation.translations as |translation|}}
<li
local-class='item-grid-item {{if translation.isTranslated "item-grid-item--translated"}} {{if
translation.isConflicted
"item-grid-item--conflicted"
"item-grid-item--reviewed"
}}'
>
<ConflictsList::Item
@permissions={{@permissions}}
@project={{@project}}
@revisions={{@revisions}}
@prompts={{@prompts}}
@translation={{translation}}
@onCorrect={{@onCorrect}}
@onUncorrect={{@onUncorrect}}
@onUpdate={{@onUpdate}}
@isFocused={{this.isFocused}}
@onFocus={{this.handleFocus}}
/>
</li>
{{/each}}
</ul>
</li>

View File

@ -1,123 +1,80 @@
<div local-class='root'>
<div local-class='conflict-item {{if this.resolved "resolved"}} {{if this.error "errored"}}'>
<li>
{{#if this.resolved}}
<div local-class='textResolved'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @conflict.id}} local-class='key'>
<strong local-class='item-key'>
{{this.conflictKey.value}}
<small local-class='item-key-prefix'>
{{#if this.conflictKey.prefix}}
{{this.conflictKey.prefix}}
{{else}}
{{@conflict.document.path}}
{{/if}}
</small>
</strong>
</LinkTo>
<div local-class='textResolved-content'>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.uncorrect_error_text'}}
</div>
{{/if}}
</div>
</div>
{{else}}
<div local-class='item-details' data-dir={{if this.revisionTextDirRtl 'rtl'}}>
<div local-class='item-details__column'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @conflict.id}} local-class='key'>
<strong local-class='item-key'>
{{this.conflictKey.value}}
<small local-class='item-key-prefix'>
{{#if this.conflictKey.prefix}}
{{this.conflictKey.prefix}}
{{else}}
{{@conflict.document.path}}
{{/if}}
</small>
</strong>
</LinkTo>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.correct_error_text'}}
</div>
{{/if}}
</div>
<div local-class='item-details__column'>
<div local-class='textInput'>
<TranslationEdit::Form
@projectId={{@project.id}}
@translationId={{@conflict.id}}
@lintMessages={{@conflict.lintMessages}}
@valueType={{@conflict.valueType}}
@value={{this.textInput}}
@inputDisabled={{this.inputDisabled}}
@showTypeHints={{false}}
@onKeyUp={{fn this.changeTranslationText}}
@onSubmit={{fn this.correct}}
@rtl={{this.revisionTextDirRtl}}
lang={{this.revisionSlug}}
as |form|
>
{{#component form.submit}}
<div local-class='button-submit' data-dir={{form.dir}}>
{{#if this.showOriginalButton}}
<AsyncButton @onClick={{fn this.setOriginalText}} local-class='revert-button' class='button button--iconOnly button--white'>
{{inline-svg '/assets/revert.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
<div local-class='form-helpers'>
<TranslationEdit::Helpers
@permissions={{@permissions}}
@project={{@project}}
@revisions={{@revisions}}
@prompts={{@prompts}}
@rtl={{this.revisionTextDirRtl}}
@text={{this.textInput}}
@onUpdatingText={{fn this.onUpdatingText}}
@onUpdateText={{fn this.onUpdateText}}
/>
</div>
{{#if (get @permissions 'correct_translation')}}
<AsyncButton @onClick={{fn this.correct}} @loading={{this.loading}} class='button button--iconOnly button--filled button--green'>
{{inline-svg '/assets/check.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
</div>
{{/component}}
</TranslationEdit::Form>
</div>
<div local-class='conflictedText-references'>
{{#if this.showTextDiff}}
<div local-class='conflictedText-references-conflicted'>
<span local-class='conflictedText-references-conflicted-label'>
{{inline-svg '/assets/diff.svg' local-class='conflictedText-references-conflicted-icon'}}
</span>
<div local-class='conflictedText-references-conflicted-value'>{{string-diff this.textInput @conflict.conflictedText}}</div>
</div>
{{/if}}
{{#if @conflict.relatedTranslations}}
{{#each this.relatedTranslations key='id' as |relatedTranslation|}}
<ConflictsList::Item::RelatedTranslation
@project={{@project}}
@translation={{relatedTranslation}}
@permissions={{@permissions}}
@onCopyTranslation={{perform this.copyTranslationTask}}
/>
{{/each}}
{{/if}}
</div>
</div>
<li local-class='translation-item {{if @isFocused "focused"}} {{if this.error "errored"}} {{if this.conflictResolved "resolved"}}'>
<div local-class='item-details' data-dir={{if this.revisionTextDirRtl 'rtl'}}>
<div local-class='item-details__column'>
{{#if this.error}}
<div local-class='error'>
{{t 'components.translation_item.correct_error_text'}}
</div>
{{/if}}
</li>
</div>
<div local-class='item-details__column'>
<div local-class='textInput'>
<TranslationEdit::Form
@borderless={{true}}
@projectId={{@project.id}}
@translationId={{@translation.id}}
@lintMessages={{@translation.lintMessages}}
@valueType={{@translation.valueType}}
@value={{this.textInput}}
@inputDisabled={{this.inputDisabled}}
@showTypeHints={{false}}
@onKeyUp={{fn this.changeTranslationText}}
@onSubmit={{fn this.correctConflict}}
@onFocus={{@onFocus}}
@onBlur={{@onBlur}}
@rtl={{this.revisionTextDirRtl}}
lang={{this.revisionSlug}}
as |form|
>
{{#component form.submit}}
<div local-class='button-submit' data-dir={{form.dir}}>
{{#if this.showOriginalButton}}
<AsyncButton @onClick={{fn this.setOriginalText}} local-class='revert-button' class='button button--iconOnly button--white'>
{{inline-svg '/assets/revert.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
<div local-class='form-helpers'>
<TranslationEdit::Helpers
@permissions={{@permissions}}
@project={{@project}}
@revisions={{@revisions}}
@prompts={{@prompts}}
@rtl={{this.revisionTextDirRtl}}
@text={{this.textInput}}
@onUpdatingText={{fn this.onUpdatingText}}
@onUpdateText={{fn this.onUpdateText}}
/>
</div>
{{#if @translation.isConflicted}}
{{#if (get @permissions 'update_translation')}}
<AsyncButton @loading={{this.isUpdateLoading}} tabindex='-1' class='button button--borderLess button--iconOnly button--grey' @onClick={{fn this.updateConflict}}>
{{inline-svg '/assets/pencil.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
{{#if (get @permissions 'correct_translation')}}
<AsyncButton @loading={{this.isCorrectLoading}} class='button button--iconOnly button--borderLess button--green' @onClick={{fn this.correctConflict}}>
{{inline-svg '/assets/check.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
{{else}}
{{#if (get @permissions 'update_translation')}}
<AsyncButton @loading={{this.isUpdateLoading}} tabindex='-1' class='button button--borderLess button--iconOnly button--grey' @onClick={{fn this.updateConflict}}>
{{inline-svg '/assets/pencil.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
{{#if (get @permissions 'uncorrect_translation')}}
<AsyncButton @loading={{this.isUncorrectLoading}} class='button button--borderLess button--iconOnly button--red' @onClick={{fn this.uncorrectConflict}}>
{{inline-svg '/assets/revert.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
{{/if}}
</div>
{{/component}}
</TranslationEdit::Form>
</div>
</div>
</div>
</div>
</li>

View File

@ -1,17 +0,0 @@
<div local-class='item'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @translation.id}} local-class='item-link'>
{{this.revisionName}}
</LinkTo>
<div local-class='item-text'>
<span local-class='item-text-content'>{{this.text}}</span>
<div local-class='item-actions'>
{{#if (get @permissions 'machine_translations_translate')}}
<button title={{t 'components.conflict_item.translate_tooltip'}} class='tooltip tooltip--top' local-class='item-actions-link' {{on 'click' this.translate}}>
{{inline-svg 'assets/language.svg' local-class='item-actions-icon'}}
</button>
{{/if}}
</div>
</div>
</div>

View File

@ -1,38 +0,0 @@
<div local-class='conflicts-page'>
<ConflictsFilters
@meta={{@translations.meta}}
@conflicts={{@translations.entries}}
@document={{@document}}
@documents={{@documents}}
@version={{@version}}
@versions={{@versions}}
@query={{@query}}
@onChangeDocument={{@onChangeDocument}}
@onChangeQuery={{@onChangeQuery}}
@onChangeVersion={{@onChangeVersion}}
/>
{{#if @isLoading}}
<SkeletonUi::ProgressLine />
{{/if}}
{{#if @showSkeleton}}
<SkeletonUi::ConflictsItems />
{{else if @showLoading}}
<LoadingContent @label={{t 'pods.project.conflicts.loading_content'}} />
{{else}}
<ConflictsList
@version={{@version}}
@versions={{@versions}}
@permissions={{@permissions}}
@project={{@project}}
@revisions={{@revisions}}
@prompts={{@prompts}}
@conflicts={{@translations.entries}}
@query={{@query}}
@onCorrect={{@onCorrect}}
@onCopyTranslation={{@onCopyTranslation}}
/>
<ResourcePagination @meta={{@translations.meta}} @onSelectPage={{@onSelectPage}} />
{{/if}}
</div>

View File

@ -30,7 +30,7 @@
<h2 local-class='stats-title'>
<div local-class='stats-title-links'>
{{#if (get @permissions 'sync')}}
<LinkTo @route='logged-in.project.files.sync' @models={{array @project.id @document.id}} class='button button--borderLess button--filled button--white'>
<LinkTo @route='logged-in.project.files.sync' @models={{array @project.id @document.id}} class='button button--filled button--white'>
{{inline-svg '/assets/sync.svg' class='button-icon'}}
{{t 'components.documents_list.sync'}}
</LinkTo>
@ -59,17 +59,6 @@
</div>
{{#if this.slaveRevisions}}
<h2 local-class='stats-title'>
<div local-class='stats-title-links'>
{{#if (get @permissions 'create_slave')}}
<LinkTo @route='logged-in.project.manage-languages' @model={{@project.id}} class='button button--filled button--white button--borderLess'>
{{inline-svg 'assets/language.svg' class='button-icon'}}
{{t 'components.dashboard_revisions.manage_languages_link_title'}}
</LinkTo>
{{/if}}
</div>
</h2>
<div local-class='slaves'>
{{#each this.slaveRevisions key='id' as |revision|}}
<DashboardRevisions::Item

View File

@ -81,45 +81,28 @@
{{#unless this.empty}}
<div local-class='links'>
{{#if (get @permissions 'sync')}}
<LinkTo
@route='logged-in.project.files.sync'
@models={{array @project.id @document.id}}
local-class='button-sync'
title={{t 'components.documents_list.sync'}}
class='tooltip tooltip--top button button--filled button--iconOnly'
>
<LinkTo @route='logged-in.project.files.sync' @models={{array @project.id @document.id}} local-class='button-sync' class='button button--filled'>
{{inline-svg '/assets/sync.svg' class='button-icon'}}
{{t 'components.documents_list.sync'}}
</LinkTo>
{{/if}}
{{#if this.multipleRevisions}}
{{#if (get @permissions 'merge')}}
<LinkTo
@route='logged-in.project.files.add-translations'
@models={{array @project.id @document.id}}
title={{t 'components.documents_list.merge'}}
class='tooltip tooltip--top button button--filled button--white button--iconOnly'
>
<LinkTo @route='logged-in.project.files.add-translations' @models={{array @project.id @document.id}} class='button button--filled button--white'>
{{inline-svg '/assets/merge.svg' class='button-icon'}}
{{t 'components.documents_list.merge'}}
</LinkTo>
{{/if}}
{{/if}}
{{#if (get @permissions 'machine_translations_translate')}}
<LinkTo
@route='logged-in.project.files.machine-translations'
@models={{array @project.id @document.id}}
title={{t 'components.documents_list.machine_translations'}}
class='tooltip tooltip--top button button--filled button--white button--iconOnly'
>
<LinkTo @route='logged-in.project.files.machine-translations' @models={{array @project.id @document.id}} class='button button--filled button--white'>
{{inline-svg '/assets/language.svg' class='button-icon'}}
{{t 'components.documents_list.machine_translations'}}
</LinkTo>
{{/if}}
<LinkTo
@route='logged-in.project.files.export'
@models={{array @project.id @document.id}}
title={{t 'components.documents_list.export'}}
class='tooltip tooltip--top button button--filled button--white button--iconOnly'
>
<LinkTo @route='logged-in.project.files.export' @models={{array @project.id @document.id}} class='button button--filled button--white'>
{{inline-svg '/assets/export.svg' class='button-icon'}}
{{t 'components.documents_list.export'}}
</LinkTo>
</div>

View File

@ -1,7 +1,7 @@
<div local-class='lint-options'>
<div class='filters' local-class='filters'>
<div class='filters-wrapper'>
<form class='filters-wrapper' local-class='filters-wrapper' {{on 'submit' (fn this.submitForm)}}>
<form class='filters-wrapper' local-class='filters-wrapper' {{on 'submit' (fn this.submitForm)}}>
<div class='filters-content' local-class='filters-content'>
<div class='queryForm' local-class='queryForm'>
{{inline-svg '/assets/search.svg' local-class='search-icon'}}
@ -15,24 +15,26 @@
/>
</div>
<div class='queryForm-filters'>
{{#if this.showDocuments}}
<div class='queryForm-filters-column'>
<div local-class='exportOptions-item'>
<AccSelect @searchEnabled={{false}} @selected={{this.documentValue}} @options={{this.mappedDocuments}} @onchange={{fn this.documentChanged}} />
{{#if this.showSomeFilters}}
<div class='queryForm-filters'>
{{#if this.showDocuments}}
<div class='queryForm-filters-column'>
<div local-class='exportOptions-item'>
<AccSelect @searchEnabled={{false}} @selected={{this.documentValue}} @options={{this.mappedDocuments}} @onchange={{fn this.documentChanged}} />
</div>
</div>
</div>
{{/if}}
{{/if}}
{{#if this.showVersions}}
<div class='queryForm-filters-column'>
<div local-class='exportOptions-item'>
<AccSelect @searchEnabled={{false}} @selected={{this.versionValue}} @options={{this.mappedVersions}} @onchange={{fn this.versionChanged}} />
{{#if this.showVersions}}
<div class='queryForm-filters-column'>
<div local-class='exportOptions-item'>
<AccSelect @searchEnabled={{false}} @selected={{this.versionValue}} @options={{this.mappedVersions}} @onchange={{fn this.versionChanged}} />
</div>
</div>
</div>
{{/if}}
</div>
</form>
</div>
{{/if}}
</div>
{{/if}}
</div>
</form>
</div>
</div>

View File

@ -1,5 +1,5 @@
{{#if @lintTranslation.messages}}
<div local-class='wrapper'>
<div local-class='wrapper' class='lint-translations-item'>
<ul local-class='messages'>
{{#each this.messages as |message|}}
<span local-class='description'>

View File

@ -208,18 +208,16 @@
{{t 'components.project_activity.operations_label'}}
</span>
<div local-class='details-associations-overflow'>
{{#each this.operations.entries key='id' as |activity|}}
<ActivityItem
@compact={{true}}
@permissions={{@permissions}}
@showTranslationLink={{true}}
@componentTranslationPrefix='project_activities_list_item'
@project={{@project}}
@activity={{activity}}
/>
{{/each}}
</div>
{{#each this.operations.entries key='id' as |activity|}}
<ActivityItem
@compact={{true}}
@permissions={{@permissions}}
@showTranslationLink={{true}}
@componentTranslationPrefix='project_activities_list_item'
@project={{@project}}
@activity={{activity}}
/>
{{/each}}
{{#if this.operations.meta.nextPage}}
<div local-class='details-associations-pagination'>
<ResourcePagination @meta={{this.operations.meta}} @onSelectPage={{fn this.refreshActivities}} />

View File

@ -26,7 +26,7 @@
{{#if (get @permissions 'correct_translation')}}
<li local-class='list-item'>
<LinkTo @route='logged-in.project.revision.conflicts' @models={{array @project.id @selectedRevision}} local-class='list-item-link'>
<LinkTo @route='logged-in.project.conflicts' @model={{@project.id}} local-class='list-item-link'>
{{inline-svg '/assets/check.svg' local-class='list-item-link-icon'}}
<span local-class='list-item-link-text'>
{{t 'components.project_navigation.conflicts_link_title'}}

View File

@ -1,28 +1,31 @@
<div local-class='wrapper'>
<div local-class='content'>
<h2 local-class='title'>
{{t 'components.project_settings.prompts.title'}}
</h2>
<p local-class='text'>
{{t 'components.project_settings.prompts.help'}}
</p>
<ul local-class='list'>
{{#each @prompts as |prompt|}}
<li>
<ProjectSettings::Prompts::Item @project={{@project}} @prompt={{prompt}} @onDelete={{@onDeletePrompt}} />
</li>
{{/each}}
</ul>
<LinkTo @route='logged-in.project.edit.prompts.new' @models={{array @project.id}} class='button button--xl button--primary button--highlight'>
{{inline-svg '/assets/add.svg' class='button-icon'}}
{{t 'components.project_settings.prompts.new_button'}}
</LinkTo>
</div>
<h2 local-class='title'>
{{t 'components.project_settings.prompts.title'}}
</h2>
<p local-class='text'>
{{t 'components.project_settings.prompts.help'}}
</p>
<div local-class='config'>
<ProjectSettings::Prompts::Config @project={{@project}} @onSave={{@onSaveConfig}} @onDelete={{@onDeleteConfig}} />
</div>
{{#if @project.promptConfig}}
<div local-class='content'>
{{#if @prompts}}
<ul local-class='list'>
{{#each @prompts as |prompt|}}
<li>
<ProjectSettings::Prompts::Item @project={{@project}} @prompt={{prompt}} @onDelete={{@onDeletePrompt}} />
</li>
{{/each}}
</ul>
{{/if}}
<LinkTo @route='logged-in.project.edit.prompts.new' @models={{array @project.id}} class='button button--xl button--primary button--highlight'>
{{inline-svg '/assets/add.svg' class='button-icon'}}
{{t 'components.project_settings.prompts.new_button'}}
</LinkTo>
</div>
{{/if}}
</div>

View File

@ -24,16 +24,16 @@
</p>
<textarea placeholder={{this.configKeyPlaceholder}} local-class='textInput' rows='1' {{on 'change' (fn this.onConfigKeyChange)}}>{{this.configKey}}</textarea>
</div>
<div local-class='actions'>
{{#if @project.promptConfig}}
<AsyncButton @onClick={{perform this.remove}} @loading={{this.isRemoving}} class='button button--red button--borderless'>
{{t 'components.project_settings.integrations.delete'}}
<div local-class='actions'>
{{#if @project.promptConfig}}
<AsyncButton @onClick={{perform this.remove}} @loading={{this.isRemoving}} class='button button--red button--borderless'>
{{t 'components.project_settings.integrations.delete'}}
</AsyncButton>
{{/if}}
<AsyncButton @onClick={{perform this.submit}} @loading={{this.isSubmitting}} class='button button--filled'>
{{t 'components.project_settings.integrations.save'}}
</AsyncButton>
{{/if}}
<AsyncButton @onClick={{perform this.submit}} @loading={{this.isSubmitting}} class='button button--filled'>
{{t 'components.project_settings.integrations.save'}}
</AsyncButton>
</div>
</div>

View File

@ -23,7 +23,7 @@
{{#if @translation.isConflicted}}
<AccBadge @link={{true}}>
<LinkTo tabindex='-1' @route='logged-in.project.revision.conflicts' @models={{array @project.id @translation.revision.id}} @query={{hash query=@translation.id}}>
<LinkTo tabindex='-1' @route='logged-in.project.conflicts' @model={{@project.id}} @query={{hash query=@translation.id}}>
{{t 'components.related_translations_list.conflicted_label'}}
</LinkTo>
</AccBadge>

View File

@ -95,9 +95,11 @@
<textarea
{{autoresize @value}}
{{on-key 'Escape' (fn this.cancel)}}
{{on-key 'cmd+Enter' @onSubmit}}
{{on-key 'cmd+Enter' (fn this.handleSubmit)}}
{{on 'input' (fn this.changeText)}}
local-class='inputText'
{{on 'focus' (fn this.handleFocus)}}
{{on 'blur' (fn this.handleBlur)}}
local-class='inputText {{if @borderless "inputText--borderless"}}'
disabled={{@inputDisabled}}
value={{@value}}
dir={{if @rtl 'rtl'}}

View File

@ -19,4 +19,4 @@
</div>
</EmptyContent>
{{/each}}
</ul>
</ul>

View File

@ -15,7 +15,7 @@
<span local-class='item-meta'>
{{#if @translation.isConflicted}}
<AccBadge title={{t 'components.translations_list.in_review_tooltip'}} class='tooltip tooltip--top' @link={{true}}>
<LinkTo @route='logged-in.project.revision.conflicts' @models={{array @project.id @translation.revision.id}} @query={{hash query=@translation.id}}>
<LinkTo @route='logged-in.project.conflicts' @model={{@project.id}} @query={{hash query=@translation.id}}>
{{t 'components.translations_list.in_review_label'}}
</LinkTo>
</AccBadge>

View File

@ -39,7 +39,7 @@
{{else if @translation.isConflicted}}
<AccBadge @link={{this.withRevisionLink}} @primary={{true}}>
{{#if this.withRevisionLink}}
<LinkTo @route='logged-in.project.revision.conflicts' @models={{array @project.id @translation.revision.id}} @query={{hash query=@translation.id}}>
<LinkTo @route='logged-in.project.conflicts' @model={{@project.id}} @query={{hash query=@translation.id}}>
{{t 'components.translation_splash_title.conflicted_label'}}
</LinkTo>
{{else}}

View File

@ -1,9 +1,10 @@
<div local-class='translations-filter'>
<div class='filters'>
<form local-class='filters-wrapper' class='filters-wrapper' {{on 'submit' (fn this.submitForm)}}>
<div local-class='filters-content'>
<div local-class='queryForm'>
<form class='filters-wrapper' local-class='filters-wrapper' {{on 'submit' (fn this.submitForm)}}>
<div class='filters-content' local-class='filters-content'>
<div class='queryForm' local-class='queryForm'>
{{inline-svg '/assets/search.svg' local-class='search-icon'}}
<input
local-class='input'
type='text'
@ -12,6 +13,19 @@
{{on-key 'Enter' (fn this.submitForm)}}
{{on 'keyup' (fn this.setDebouncedQuery)}}
/>
{{#if @onChangeAdvancedFilterBoolean}}
<button {{on 'click' (fn this.toggleAdvancedFilters)}} local-class='advancedFilters' class='button button--filled button--white'>
{{inline-svg 'assets/filter.svg' class='button-icon'}}
{{t 'components.translations_filter.advanced_filters_button'}}
{{#if @withAdvancedFilters}}
<span local-class='advancedFilters-badge'>
{{@withAdvancedFilters}}
</span>
{{/if}}
</button>
{{/if}}
</div>
<div class='queryForm-filters'>
@ -44,20 +58,6 @@
</div>
{{/if}}
</div>
<div class='queryForm-filters-column queryForm-filters-column--end'>
{{#if @onChangeAdvancedFilterBoolean}}
<button {{on 'click' (fn this.toggleAdvancedFilters)}} local-class='advancedFilters' class='button button--filled button--white'>
{{inline-svg 'assets/filter.svg' class='button-icon'}}
{{t 'components.translations_filter.advanced_filters_button'}}
{{#if @withAdvancedFilters}}
<span local-class='advancedFilters-badge'>
{{@withAdvancedFilters}}
</span>
{{/if}}
</button>
{{/if}}
</div>
</div>
{{#if this.displayAdvancedFilters}}
@ -73,12 +73,6 @@
/>
{{/if}}
</div>
{{#if @meta.totalEntries}}
<span local-class='totalEntries'>
{{t 'components.translations_filter.total_entries_count' count=@meta.totalEntries}}
</span>
{{/if}}
</form>
</div>
</div>

View File

@ -53,7 +53,7 @@
{{#if @translation.isConflicted}}
<AccBadge title={{t 'components.translations_list.in_review_tooltip'}} class='tooltip tooltip--top' @link={{true}}>
<LinkTo @route='logged-in.project.revision.conflicts' @models={{array @project.id @translation.revision.id}} @query={{hash query=@translation.id}}>
<LinkTo @route='logged-in.project.conflicts' @model={{@project.id}} @query={{hash query=@translation.id}}>
{{t 'components.translations_list.in_review_label'}}
</LinkTo>
</AccBadge>

View File

@ -0,0 +1,48 @@
<ConflictsFilters
@meta={{this.model.groupedTranslations.meta}}
@conflicts={{this.model.groupedTranslations.entries}}
@document={{this.document}}
@documents={{this.model.documents}}
@version={{this.version}}
@versions={{this.model.versions}}
@relatedRevisions={{this.relatedRevisions}}
@defaultRelatedRevisions={{this.model.relatedRevisions}}
@revisions={{this.model.revisions}}
@withAdvancedFilters={{this.withAdvancedFilters}}
@isTextEmptyFilter={{this.isTextEmpty}}
@isTextNotEmptyFilter={{this.isTextNotEmpty}}
@isAddedLastSyncFilter={{this.isAddedLastSync}}
@isCommentedOnFilter={{this.isCommentedOn}}
@isConflictedFilter={{this.isConflicted}}
@isTranslatedFilter={{this.isTranslated}}
@query={{this.query}}
@onChangeDocument={{this.changeDocument}}
@onChangeQuery={{this.changeQuery}}
@onChangeVersion={{this.changeVersion}}
@onChangeRevisions={{this.changeRelatedRevisions}}
@onChangeAdvancedFilterBoolean={{fn this.changeAdvancedFilterBoolean}}
/>
{{#if this.model.loading}}
<SkeletonUi::ProgressLine />
{{/if}}
{{#if this.showSkeleton}}
<SkeletonUi::TranslationsList />
{{else if this.showLoading}}
<LoadingContent @label={{t 'pods.project.translations.loading_content'}} />
{{else}}
<ConflictsList
@permissions={{this.permissions}}
@version={{this.version}}
@versions={{this.model.versions}}
@project={{this.model.project}}
@revisions={{this.revisions}}
@prompts={{this.model.prompts}}
@groupedTranslations={{this.model.groupedTranslations.entries}}
@onCorrect={{this.correctConflict}}
@onUncorrect={{this.uncorrectConflict}}
@onUpdate={{this.updateConflict}}
/>
<ResourcePagination @meta={{this.model.groupedTranslations.meta}} @onSelectPage={{fn this.selectPage}} />
{{/if}}

View File

@ -1,21 +0,0 @@
<ConflictsPage
@project={{this.model.project}}
@revisions={{this.revisions}}
@translations={{this.model.translations}}
@isLoading={{this.model.loading}}
@showLoading={{this.showLoading}}
@version={{this.version}}
@document={{this.document}}
@permissions={{this.permissions}}
@documents={{this.model.documents}}
@prompts={{this.model.prompts}}
@versions={{this.model.versions}}
@showSkeleton={{this.showSkeleton}}
@query={{this.query}}
@onCorrect={{fn this.correctConflict}}
@onCopyTranslation={{fn this.copyTranslation}}
@onSelectPage={{fn this.selectPage}}
@onChangeDocument={{fn this.changeDocument}}
@onChangeVersion={{fn this.changeVersion}}
@onChangeQuery={{fn this.changeQuery}}
/>