Fix markup

This commit is contained in:
Anton 2023-12-11 09:18:22 +01:00
parent 7ffad5b659
commit 75f7437001
31 changed files with 1093 additions and 697 deletions

View file

@ -267,6 +267,18 @@ class ExperimentsController < ApplicationController
}
end
def projects_to_clone
projects = @experiment.project.team.projects.active
.with_user_permission(current_user, ProjectPermissions::EXPERIMENTS_CREATE)
.map { |p| [p.id, p.name] }
render json: { data: projects }, status: :ok
end
def projects_to_move
projects = @experiment.movable_projects(current_user).map { |p| [p.id, p.name] }
render json: { data: projects }, status: :ok
end
# POST: clone_experiment(id)
def clone
project = current_team.projects.find(move_experiment_param)
@ -279,11 +291,10 @@ class ExperimentsController < ApplicationController
if service.succeed?
flash[:success] = t('experiments.clone.success_flash',
experiment: @experiment.name)
redirect_to canvas_experiment_path(service.cloned_experiment)
render json: { url: canvas_experiment_path(service.cloned_experiment) }
else
flash[:error] = t('experiments.clone.error_flash',
experiment: @experiment.name)
redirect_to experiments_path(project_id: @experiment.project)
render json: { message: t('experiments.clone.error_flash',
experiment: @experiment.name) }, status: :unprocessable_entity
end
end
@ -534,7 +545,7 @@ class ExperimentsController < ApplicationController
actions:
Toolbars::ExperimentsService.new(
current_user,
experiment_ids: params[:experiment_ids].split(',')
experiment_ids: JSON.parse(params[:items]).map { |i| i['id'] }
).actions
}
end

View file

@ -13,22 +13,58 @@
:filters="filters"
:viewRenders="viewRenders"
@tableReloaded="reloadingTable = false"
@archive="archive"
@restore="restore"
@showDescription="showDescription"
@duplicate="duplicate"
@move="move"
>
<template> </template>
</DataTable>
<ConfirmationModal
:title="i18n.t('experiments.index.archive_confirm_title')"
:description="i18n.t('experiments.index.archive_confirm')"
:confirmClass="'btn btn-primary'"
:confirmText="i18n.t('general.archive')"
ref="archiveModal"
></ConfirmationModal>
<DescriptionModal
v-if="descriptionModalObject"
:experiment="descriptionModalObject"
@close="descriptionModalObject = null"/>
<DuplicateModal
v-if="duplicateModalObject"
:experiment="duplicateModalObject"
@close="duplicateModalObject = null"/>
<MoveModal
v-if="moveModalObject"
:experiment="moveModalObject"
@close="moveModalObject = null"
@submit="updateTable"/>
</template>
<script>
/* global HelperModule */
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue';
import DescriptionRenderer from './renderers/description.vue';
import ConfirmationModal from '../shared/confirmation_modal.vue';
import CompletedTasksRenderer from './renderers/completed_tasks.vue';
import NameRenderer from './renderers/name.vue';
import DescriptionModal from './modals/description.vue';
import DuplicateModal from './modals/duplicate.vue';
import MoveModal from './modals/move.vue';
export default {
name: 'ExperimentsList',
components: {
DataTable,
ConfirmationModal,
DescriptionModal,
DuplicateModal,
MoveModal,
},
props: {
dataSource: { type: String, required: true },
@ -39,6 +75,9 @@ export default {
},
data() {
return {
moveModalObject: null,
duplicateModalObject: null,
descriptionModalObject: null,
reloadingTable: false,
};
},
@ -82,6 +121,7 @@ export default {
headerName: this.i18n.t('experiments.card.completed_task'),
cellRenderer: CompletedTasksRenderer,
sortable: false,
minWidth: 120,
});
columns.push({
field: 'description',
@ -130,6 +170,41 @@ export default {
return filters;
},
},
methods: {},
methods: {
updateTable() {
this.moveModalObject = null;
this.duplicateModalObject = null;
this.descriptionModalObject = null;
this.reloadingTable = true;
},
async archive(event, rows) {
const ok = await this.$refs.archiveModal.show();
if (ok) {
axios.post(event.path, { experiment_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
},
restore(event, rows) {
axios.post(event.path, { experiment_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
showDescription(_e, experiment) {
[this.descriptionModalObject] = experiment;
},
duplicate(_e, experiment) {
[this.duplicateModalObject] = experiment;
},
move(_e, experiment) {
[this.moveModalObject] = experiment;
},
},
};
</script>

View file

@ -0,0 +1,35 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="experiment.name">
{{ experiment.name }}
</h4>
</div>
<div class="modal-body">
{{ experiment.description }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'DescriptionModal',
props: {
experiment: Object,
},
mixins: [modalMixin],
};
</script>

View file

@ -0,0 +1,68 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="experiment.name">
{{ i18n.t("experiments.clone.modal_title", { experiment: experiment.name }) }}
</h4>
</div>
<div class="modal-body">
<SelectDropdown :optionsUrl="experiment.urls.projects_to_clone"
:value="targetProject"
@change="changeProject" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" :disabled="!targetProject" @click="submit" type="submit">
{{ i18n.t('experiments.clone.modal_submit') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule */
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'CloneModal',
props: {
experiment: Object,
},
mixins: [modalMixin],
components: {
SelectDropdown,
},
data() {
return {
targetProject: null,
};
},
methods: {
submit() {
axios.post(this.experiment.urls.clone, {
experiment: {
project_id: this.targetProject,
},
}).then((response) => {
this.$emit('update');
window.location.replace(response.data.url);
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
});
},
changeProject(project) {
this.targetProject = project;
},
},
};
</script>

View file

@ -0,0 +1,69 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="experiment.name">
{{ i18n.t("experiments.move.modal_title", { experiment: experiment.name }) }}
</h4>
</div>
<div class="modal-body">
<p><small>{{ i18n.t("experiments.move.notice") }}</small></p>
<SelectDropdown :optionsUrl="experiment.urls.projects_to_move"
:value="targetProject"
@change="changeProject" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" :disabled="!targetProject" @click="submit" type="submit">
{{ i18n.t('experiments.clone.modal_submit') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule */
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'MoveModal',
props: {
experiment: Object,
},
mixins: [modalMixin],
components: {
SelectDropdown,
},
data() {
return {
targetProject: null,
};
},
methods: {
submit() {
axios.post(this.experiment.urls.move, {
experiment: {
project_id: this.targetProject,
},
}).then((response) => {
this.$emit('submit');
window.location.replace(response.data.url);
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
});
},
changeProject(project) {
this.targetProject = project;
},
},
};
</script>

View file

@ -6,7 +6,7 @@
all: params.data.total_tasks
}) }}
<div class="py-1">
<div class="w-48 h-1 bg-sn-light-grey">
<div class="w-24 h-1 bg-sn-light-grey">
<div class="h-full bg-sn-blue" :style="{
width: params.data.completed_tasks / params.data.total_tasks * 100 + '%'
}"></div>

View file

@ -25,7 +25,7 @@ export default {
},
methods: {
showMore() {
this.params.dtComponent.$emit('showDescription', null, [this.params.data]);
},
},
};

View file

@ -1,6 +1,7 @@
<template>
<div ref="dropdown" class="insert-field-dropdown dropdown">
<a class="open-dropdown-button collapsed" role="button" data-toggle="dropdown" id="fieldsContainer" aria-expanded="false">
<a class="open-dropdown-button collapsed" role="button" data-toggle="dropdown"
id="fieldsContainer" aria-expanded="false">
{{ i18n.t('label_templates.show.insert_dropdown.button') }}
<i class="fas fa-chevron-down"></i>
</a>
@ -74,98 +75,97 @@
</template>
<script>
import LogoInsertModal from './components/logo_insert_modal.vue'
import LogoInsertModal from './components/logo_insert_modal.vue';
export default {
name: 'InsertFieldDropdown',
props: {
labelTemplate: {
type: Object,
required: true
}
export default {
name: 'InsertFieldDropdown',
props: {
labelTemplate: {
type: Object,
required: true,
},
data() {
return {
fields: {
default: [],
common: [],
repositories: []
},
openLogoModal: false,
logoDimension: null,
searchValue: ''
}
},
components: {LogoInsertModal},
computed: {
tooltipTemplate() {
return `<div class="tooltip" role="tooltip">
<div class="tooltip-arrow"></div>
<div class="tooltip-body">
<div class="tooltip-inner"></div>
</div>
</div>`
},
data() {
return {
fields: {
default: [],
common: [],
repositories: [],
},
filteredFields() {
this.$nextTick(() => {
$('.tooltip').remove();
$('[data-toggle="tooltip"]').tooltip();
});
if (this.searchValue.length == 0) {
return this.fields;
} else {
return {
default: this.filterArray(this.fields.default, 'key'),
common: this.filterArray(this.fields.common, 'key'),
repositories: this.filterArray(this.fields.repositories, 'repository_name').map((repo) => {
return { ...repo, tags: this.filterArray(repo.tags, 'key') };
})
};
}
},
noResults() {
return this.filteredFields.default.concat(this.filteredFields.common, this.filteredFields.repositories).length === 0;
}
openLogoModal: false,
logoDimension: null,
searchValue: '',
};
},
components: { LogoInsertModal },
computed: {
tooltipTemplate() {
return `<div class="tooltip" role="tooltip">
<div class="tooltip-arrow"></div>
<div class="tooltip-body">
<div class="tooltip-inner"></div>
</div>
</div>`;
},
mounted() {
$.get(this.labelTemplate.attributes.urls.fields, (result) => {
result.default.map((value) => {
value.key = this.i18n.t(`label_templates.default_columns.${value.key}`)
return value;
});
this.fields = result;
this.$nextTick(() => {
$('[data-toggle="tooltip"]').tooltip();
});
});
filteredFields() {
this.$nextTick(() => {
$(this.$refs.dropdown).on('show.bs.dropdown', () => {
this.$nextTick(() => {
$('.insert-field-dropdown')[1].focus()
});
this.searchValue = '';
});
$('.tooltip').remove();
$('[data-toggle="tooltip"]').tooltip();
});
},
methods: {
insertTag(field) {
if (field.id == 'logo') {
this.logoDimension = field.dimension
this.openLogoModal = true
return
}
this.$emit('insertTag', field.tag)
},
filterArray(array, key) {
return array.filter(field => {
return (
field[key].toLowerCase().indexOf(this.searchValue.toLowerCase()) !== -1 ||
field.tags
);
});
if (this.searchValue.length === 0) {
return this.fields;
}
}
}
return {
default: this.filterArray(this.fields.default, 'key'),
common: this.filterArray(this.fields.common, 'key'),
repositories: this.filterArray(this.fields.repositories, 'repository_name').map((repo) => (
{ ...repo, tags: this.filterArray(repo.tags, 'key') }
)),
};
},
noResults() {
const defaultField = this.filteredFields.default;
return defaultField.concat(this.filteredFields.common, this.filteredFields.repositories).length === 0;
},
},
mounted() {
$.get(this.labelTemplate.attributes.urls.fields, (result) => {
result.default.map((value) => {
const newValue = value;
newValue.key = this.i18n.t(`label_templates.default_columns.${value.key}`);
return newValue;
});
this.fields = result;
this.$nextTick(() => {
$('[data-toggle="tooltip"]').tooltip();
});
});
this.$nextTick(() => {
$(this.$refs.dropdown).on('show.bs.dropdown', () => {
this.$nextTick(() => {
$('.insert-field-dropdown')[1].focus();
});
this.searchValue = '';
});
});
},
methods: {
insertTag(field) {
if (field.id === 'logo') {
this.logoDimension = field.dimension;
this.openLogoModal = true;
return;
}
this.$emit('insertTag', field.tag);
},
filterArray(array, key) {
return array.filter((field) => (
field[key].toLowerCase().indexOf(this.searchValue.toLowerCase()) !== -1
|| field.tags
));
},
},
};
</script>

View file

@ -24,51 +24,83 @@
</template>
<script>
/* global HelperModule */
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue'
import DeleteModal from '../shared/confirmation_modal.vue'
import DataTable from '../shared/datatable/table.vue';
import DeleteModal from '../shared/confirmation_modal.vue';
export default {
name: 'LabelTemplatesTable',
components: {
DataTable,
DeleteModal
DeleteModal,
},
props: {
dataSource: {
type: String,
required: true
required: true,
},
actionsUrl: {
type: String,
required: true
required: true,
},
createUrl: {
type: String,
},
syncFluicsUrl: {
type: String,
}
},
},
data() {
return {
reloadingTable: false,
columnDefs: [ { field: "default", headerName: '', width: 80, minWidth: 80,
cellRenderer: this.defaultRenderer, sortable: true, headerComponentParams: { html: '<i class="fas fa-thumbtack"></i>' } },
{ field: "name", headerName: this.i18n.t('label_templates.index.thead_name'), cellRenderer: this.labelNameRenderer, sortable: true},
{ field: "format", headerName: this.i18n.t('label_templates.index.format'), sortable: true },
{ field: "description", headerName: this.i18n.t('label_templates.index.description'), sortable: true },
{ field: "modified_by", headerName: this.i18n.t('label_templates.index.updated_by'), sortable: true },
{ field: "updated_at", headerName: this.i18n.t('label_templates.index.updated_at'), sortable: true },
{ field: "created_by", headerName: this.i18n.t('label_templates.index.created_by'), sortable: true },
{ field: "created_at", headerName: this.i18n.t('label_templates.index.created_at'), sortable: true }
]
}
columnDefs: [
{
field: 'default',
headerName: '',
width: 80,
minWidth: 80,
cellRenderer: this.defaultRenderer,
sortable: true,
headerComponentParams: { html: '<i class="fas fa-thumbtack"></i>' },
}, {
field: 'name',
headerName: this.i18n.t('label_templates.index.thead_name'),
cellRenderer: this.labelNameRenderer,
sortable: true,
}, {
field: 'format',
headerName: this.i18n.t('label_templates.index.format'),
sortable: true,
}, {
field: 'description',
headerName: this.i18n.t('label_templates.index.description'),
sortable: true,
}, {
field: 'modified_by',
headerName: this.i18n.t('label_templates.index.updated_by'),
sortable: true,
}, {
field: 'updated_at',
headerName: this.i18n.t('label_templates.index.updated_at'),
sortable: true,
}, {
field: 'created_by',
headerName: this.i18n.t('label_templates.index.created_by'),
sortable: true,
}, {
field: 'created_at',
headerName: this.i18n.t('label_templates.index.created_at'),
sortable: true,
},
],
};
},
computed: {
toolbarActions() {
let left = []
const left = [];
if (this.createUrl) {
left.push({
name: 'create',
@ -76,8 +108,8 @@ export default {
label: this.i18n.t('label_templates.index.toolbar.new'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary'
})
buttonStyle: 'btn btn-primary',
});
}
if (this.syncFluicsUrl) {
left.push({
@ -86,30 +118,30 @@ export default {
label: this.i18n.t('label_templates.index.toolbar.update_fluics_labels'),
type: 'emit',
path: this.syncFluicsUrl,
buttonStyle: 'btn btn-light'
})
buttonStyle: 'btn btn-light',
});
}
return {
left: left,
right: []
}
}
left,
right: [],
};
},
},
methods: {
labelNameRenderer(params) {
let editUrl = params.data.urls.show;
const editUrl = params.data.urls.show;
return `<a href="${editUrl}">
${params.data.icon_url}
${params.data.name}
</a>`
</a>`;
},
defaultRenderer(params) {
let defaultSelected = params.data.default;
const defaultSelected = params.data.default;
return defaultSelected ? '<i class="fas fa-thumbtack"></i>' : '';
},
setDefault(action) {
axios.post(action.path).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -117,7 +149,7 @@ export default {
},
duplicate(action, rows) {
axios.post(action.path, { selected_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -126,28 +158,28 @@ export default {
createTemplate(action) {
axios.post(action.path).then((response) => {
window.location.href = response.data.redirect_url;
})
});
},
syncFluicsLabels(action) {
axios.post(action.path).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
async deleteTemplates(action, rows) {
const ok = await this.$refs.deleteModal.show()
const ok = await this.$refs.deleteModal.show();
if (ok) {
axios.delete(action.path, { data: { selected_ids: rows.map((row) => row.id) } }).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
}
}
}
},
},
};
</script>

View file

@ -12,7 +12,9 @@
<div>{{ params.code }}</div>
<RowMenuRenderer :params="{data: params, dtComponent: dtComponent}" class="ml-auto"/>
</div>
<a :href="params.urls.show" :class="{'pointer-events-none text-sn-grey': !params.urls.show}" class="font-bold mb-4 text-sn-black hover:no-underline hover:text-sn-black">
<a :href="params.urls.show"
:class="{'pointer-events-none text-sn-grey': !params.urls.show}"
class="font-bold mb-4 text-sn-black hover:no-underline hover:text-sn-black">
{{ params.name }}
</a>
<div class="grid gap-2 grid-cols-[80px_auto] mt-auto">
@ -24,8 +26,9 @@
<span class="font-bold">{{ params.archived_on }}</span>
</template>
<span class="text-sn-grey">{{ i18n.t('projects.index.card.visibility') }}</span>
<span class="font-bold">{{ params.hidden ? i18n.t('projects.index.hidden') : i18n.t('projects.index.visible') }}</span>
<span class="font-bold">
{{ params.hidden ? i18n.t('projects.index.hidden') : i18n.t('projects.index.visible') }}
</span>
<span class="text-sn-grey">{{ i18n.t('projects.index.card.users') }}</span>
<UsersRenderer :params="{data: params, value: params.users, dtComponent: dtComponent}" class="-mt-2.5" />
</div>
@ -45,7 +48,9 @@
<div class="flex-grow flex items-center justify-center min-h-[6rem] text-sn-blue">
<i class="sn-icon sn-icon-folder"></i>
</div>
<a :href="params.urls.show" class="flex items-center justify-center gap-1 font-bold mb-2 text-sn-black hover:no-underline hover:text-sn-black">
<a :href="params.urls.show"
class="flex items-center justify-center gap-1 font-bold mb-2
text-sn-black hover:no-underline hover:text-sn-black">
<i class="sn-icon mini sn-icon-mini-folder-left"></i>
{{ params.name }}
</a>
@ -57,20 +62,20 @@
<script>
import RowMenuRenderer from '../shared/datatable/row_menu_renderer.vue'
import UsersRenderer from './renderers/users.vue'
import CardSelectorMixin from '../shared/datatable/mixins/card_selector.js'
import RowMenuRenderer from '../shared/datatable/row_menu_renderer.vue';
import UsersRenderer from './renderers/users.vue';
import CardSelectorMixin from '../shared/datatable/mixins/card_selector.js';
export default {
name: "ProjectCard",
name: 'ProjectCard',
props: {
params: Object,
dtComponent: Object
dtComponent: Object,
},
components: {
RowMenuRenderer,
UsersRenderer
UsersRenderer,
},
mixins: [CardSelectorMixin]
}
mixins: [CardSelectorMixin],
};
</script>

View file

@ -27,7 +27,8 @@
<ProjectCard :params="data.params" :dtComponent="data.dtComponent" ></ProjectCard>
</template>
</DataTable>
<a href="#" ref="commentButton" class="open-comments-sidebar hidden" data-turbolinks="false" data-object-type="Project" data-object-id=""></a>
<a href="#" ref="commentButton" class="open-comments-sidebar hidden"
data-turbolinks="false" data-object-type="Project" data-object-id=""></a>
<ConfirmationModal
:title="i18n.t('projects.index.archive_confirm_title')"
:description="i18n.t('projects.index.archive_confirm')"
@ -49,27 +50,38 @@
:confirmText="i18n.t('projects.export_projects.export_button')"
ref="exportModal"
></ConfirmationModal>
<EditProjectModal v-if="editProject" :userRolesUrl="userRolesUrl" :project="editProject" @close="editProject = null" @update="updateTable" />
<EditFolderModal v-if="editFolder" :folder="editFolder" @close="editFolder = null" @update="updateTable" />
<NewProjectModal v-if="newProject" :createUrl="createUrl" :currentFolderId="currentFolderId" :userRolesUrl="userRolesUrl" @close="newProject = false" @create="updateTable" />
<NewFolderModal v-if="newFolder" :createFolderUrl="createFolderUrl" :currentFolderId="currentFolderId" :viewMode="currentViewMode" @close="newFolder = false" @create="updateTable" />
<MoveModal v-if="objectsToMove" :moveToUrl="moveToUrl" :selectedObjects="objectsToMove" :foldersTreeUrl="foldersTreeUrl" @close="objectsToMove = null" @move="updateTable" />
<AccessModal v-if="accessModalParams" :params="accessModalParams" @close="accessModalParams = null" @refresh="this.reloadingTable = true" />
<EditProjectModal v-if="editProject" :userRolesUrl="userRolesUrl"
:project="editProject" @close="editProject = null" @update="updateTable" />
<EditFolderModal v-if="editFolder" :folder="editFolder"
@close="editFolder = null" @update="updateTable" />
<NewProjectModal v-if="newProject" :createUrl="createUrl"
:currentFolderId="currentFolderId" :userRolesUrl="userRolesUrl"
@close="newProject = false" @create="updateTable" />
<NewFolderModal v-if="newFolder" :createFolderUrl="createFolderUrl"
:currentFolderId="currentFolderId" :viewMode="currentViewMode"
@close="newFolder = false" @create="updateTable" />
<MoveModal v-if="objectsToMove" :moveToUrl="moveToUrl"
:selectedObjects="objectsToMove" :foldersTreeUrl="foldersTreeUrl"
@close="objectsToMove = null" @move="updateTable" />
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="this.reloadingTable = true" />
</template>
<script>
/* global HelperModule */
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue'
import UsersRenderer from './renderers/users.vue'
import ProjectCard from './card.vue'
import ConfirmationModal from '../shared/confirmation_modal.vue'
import EditProjectModal from './modals/edit.vue'
import EditFolderModal from './modals/edit_folder.vue'
import NewProjectModal from './modals/new.vue'
import NewFolderModal from './modals/new_folder.vue'
import MoveModal from './modals/move.vue'
import AccessModal from '../shared/access_modal/modal.vue'
import DataTable from '../shared/datatable/table.vue';
import UsersRenderer from './renderers/users.vue';
import ProjectCard from './card.vue';
import ConfirmationModal from '../shared/confirmation_modal.vue';
import EditProjectModal from './modals/edit.vue';
import EditFolderModal from './modals/edit_folder.vue';
import NewProjectModal from './modals/new.vue';
import NewFolderModal from './modals/new_folder.vue';
import MoveModal from './modals/move.vue';
import AccessModal from '../shared/access_modal/modal.vue';
export default {
name: 'ProjectsList',
@ -83,7 +95,7 @@ export default {
NewProjectModal,
NewFolderModal,
MoveModal,
AccessModal
AccessModal,
},
props: {
dataSource: { type: String, required: true },
@ -111,23 +123,49 @@ export default {
folderDeleteDescription: '',
exportDescription: '',
columnDefs: [
{ field: "name", flex: 1, headerName: this.i18n.t('projects.index.card.name'), sortable: true, cellRenderer: this.nameRenderer },
{ field: "code", headerName: this.i18n.t('projects.index.card.id'), sortable: true },
{ field: "created_at", headerName: this.i18n.t('projects.index.card.start_date'), sortable: true },
{ field: "hidden", headerName: this.i18n.t('projects.index.card.visibility'), cellRenderer: this.visibiltyRenderer, sortable: false },
{ field: "users", headerName: this.i18n.t('projects.index.card.users'), cellRenderer: 'UsersRenderer', sortable: false, minWidth: 210, notSelectable: true }
]
}
{
field: 'name',
flex: 1,
headerName: this.i18n.t('projects.index.card.name'),
sortable: true,
cellRenderer: this.nameRenderer,
},
{
field: 'code',
headerName: this.i18n.t('projects.index.card.id'),
sortable: true,
},
{
field: 'created_at',
headerName: this.i18n.t('projects.index.card.start_date'),
sortable: true,
},
{
field: 'hidden',
headerName: this.i18n.t('projects.index.card.visibility'),
cellRenderer: this.visibiltyRenderer,
sortable: false,
},
{
field: 'users',
headerName: this.i18n.t('projects.index.card.users'),
cellRenderer: 'UsersRenderer',
sortable: false,
minWidth: 210,
notSelectable: true,
},
],
};
},
computed: {
viewRenders() {
return [
{type: 'table'},
{type: 'cards'}
]
{ type: 'table' },
{ type: 'cards' },
];
},
toolbarActions() {
let left = []
const left = [];
if (this.createUrl && this.currentViewMode !== 'archived') {
left.push({
name: 'create',
@ -135,8 +173,8 @@ export default {
label: this.i18n.t('projects.index.new'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary'
})
buttonStyle: 'btn btn-primary',
});
}
if (this.createFolderUrl) {
left.push({
@ -146,30 +184,32 @@ export default {
type: 'emit',
path: this.createFolderUrl,
buttonStyle: 'btn btn-light',
})
});
}
return {
left: left,
right: []
}
left,
right: [],
};
},
filters() {
let filters = [{
key: 'query',
type: 'Text'
},
{
key: 'created_at',
type: 'DateRange',
label: this.i18n.t("filters_modal.created_on.label"),
}]
const filters = [
{
key: 'query',
type: 'Text',
},
{
key: 'created_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.created_on.label'),
},
];
if (this.currentViewMode === 'archived') {
filters.push({
key: 'archived_at',
type: 'DateRange',
label: this.i18n.t("filters_modal.archived_on.label"),
})
label: this.i18n.t('filters_modal.archived_on.label'),
});
}
filters.push({
@ -178,35 +218,37 @@ export default {
optionsUrl: this.usersFilterUrl,
optionRenderer: this.usersFilterRenderer,
labelRenderer: this.usersFilterRenderer,
label: this.i18n.t("projects.index.filters_modal.members.label"),
placeholder: this.i18n.t("projects.index.filters_modal.members.placeholder"),
})
label: this.i18n.t('projects.index.filters_modal.members.label'),
placeholder: this.i18n.t('projects.index.filters_modal.members.placeholder'),
});
filters.push({
key: 'folder_search',
type: 'Checkbox',
label: this.i18n.t("projects.index.filters_modal.folders.label"),
})
label: this.i18n.t('projects.index.filters_modal.folders.label'),
});
return filters
}
return filters;
},
},
methods: {
usersFilterRenderer(option) {
return `<div class="flex items-center gap-2">
<img src="${option[2].avatar_url}" class="rounded-full w-6 h-6" />
<span>${option[1]}</span>
</div>`
</div>`;
},
nameRenderer(params) {
let showUrl = params.data.urls.show;
return `<a href="${showUrl}" class="flex items-center gap-1 hover:no-underline ${!showUrl ? 'pointer-events-none text-sn-grey' : ''}">
const showUrl = params.data.urls.show;
return `<a href="${showUrl}"
class="flex items-center gap-1 hover:no-underline
${!showUrl ? 'pointer-events-none text-sn-grey' : ''}">
${params.data.folder ? '<i class="sn-icon mini sn-icon-mini-folder-left"></i>' : ''}
${params.data.name}
</a>`
</a>`;
},
visibiltyRenderer(params) {
if (params.data.type !== 'projects') return ''
if (params.data.type !== 'projects') return '';
return params.data.hidden ? this.i18n.t('projects.index.hidden') : this.i18n.t('projects.index.visible');
},
openComments(_params, rows) {
@ -217,13 +259,13 @@ export default {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl,
}
};
},
async archive(event, rows) {
const ok = await this.$refs.archiveModal.show()
const ok = await this.$refs.archiveModal.show();
if (ok) {
axios.post(event.path, { project_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -232,7 +274,7 @@ export default {
},
restore(event, rows) {
axios.post(event.path, { project_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -240,10 +282,10 @@ export default {
},
edit(event, rows) {
if (rows[0].folder) {
this.editFolder = rows[0]
return
[this.editFolder] = rows;
return;
}
this.editProject = rows[0]
[this.editProject] = rows;
},
create() {
this.newProject = true;
@ -260,14 +302,14 @@ export default {
this.reloadingTable = true;
},
async deleteFolder(event, rows) {
const description =`
<p>${this.i18n.t('projects.index.modal_delete_folders.description_1_html', {number: rows.length}) }</p>
<p>${this.i18n.t('projects.index.modal_delete_folders.description_2')}</p>`
const description = `
<p>${this.i18n.t('projects.index.modal_delete_folders.description_1_html', { number: rows.length })}</p>
<p>${this.i18n.t('projects.index.modal_delete_folders.description_2')}</p>`;
this.folderDeleteDescription = description;
const ok = await this.$refs.deleteFolderModal.show();
if (ok) {
axios.post(event.path, { project_folder_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -276,13 +318,13 @@ export default {
},
async exportProjects(event, rows) {
this.exportDescription = event.message;
const ok = await this.$refs.exportModal.show()
const ok = await this.$refs.exportModal.show();
if (ok) {
axios.post(event.path, {
project_ids: rows.filter((row) => !row.folder).map((row) => row.id),
project_folder_ids: rows.filter((row) => row.folder).map((row) => row.id),
}).then((response) => {
this.reloadingTable = true
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
@ -291,8 +333,8 @@ export default {
},
move(event, rows) {
this.objectsToMove = rows;
}
}
}
},
},
};
</script>

View file

@ -3,7 +3,9 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="project.name">
{{ i18n.t('projects.index.modal_edit_project.modal_title', {project: project.name}) }}
</h4>
@ -12,7 +14,9 @@
<div class="mb-6">
<label class="sci-label">{{ i18n.t("projects.index.modal_new_project.name") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
<input type="text" v-model="name" class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
</div>
</div>
<div class="flex gap-2 text-xs items-center">
@ -29,7 +33,9 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_edit_project.submit') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('projects.index.modal_edit_project.submit') }}
</button>
</div>
</div>
</div>
@ -38,17 +44,17 @@
<script>
import SelectDropdown from "../../shared/select_dropdown.vue";
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import modalMixin from '../../shared/modal_mixin';
export default {
name: "EditProjectModal",
name: 'EditProjectModal',
props: {
project: Object,
userRolesUrl: String,
},
mixins: [modal_mixin],
mixins: [modalMixin],
components: {
SelectDropdown,
},
@ -67,17 +73,17 @@ export default {
name: this.name,
visibility: (this.visible ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole,
}
},
}).then(() => {
this.error = null;
this.$emit('update');
}).catch((error) => {
this.error = error.response.data.errors.name;
})
});
},
changeRole(role) {
this.defaultRole = role;
},
}
}
},
};
</script>

View file

@ -3,7 +3,9 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="folder.name">
{{ i18n.t('projects.index.modal_edit_folder.title', {folder: folder.name}) }}
</h4>
@ -12,13 +14,17 @@
<div class="mb-6">
<label class="sci-label">{{ i18n.t("projects.index.modal_edit_folder.folder_name_field") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
<input type="text" v-model="name"
class="sci-input-field" autofocus="true"
:placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_edit_folder.submit') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('projects.index.modal_edit_folder.submit') }}
</button>
</div>
</div>
</div>
@ -27,16 +33,16 @@
<script>
import SelectDropdown from "../../shared/select_dropdown.vue";
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import modalMixin from '../../shared/modal_mixin';
export default {
name: "EditFolderModal",
name: 'EditFolderModal',
props: {
folder: Object,
},
mixins: [modal_mixin],
mixins: [modalMixin],
components: {
SelectDropdown,
},
@ -51,14 +57,14 @@ export default {
axios.put(this.folder.urls.update, {
project_folder: {
name: this.name,
}
},
}).then(() => {
this.error = null;
this.$emit('update');
}).catch((error) => {
this.error = error.response.data.errors.name;
})
}
}
}
});
},
},
};
</script>

View file

@ -3,7 +3,9 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
{{ this.title }}
</h4>
@ -12,7 +14,11 @@
<div class="mb-4">{{ this.description }}</div>
<div class="mb-4">
<div class="sci-input-container-v2 left-icon">
<input type="text" v-model="query" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_move_folder.find_folder')" />
<input type="text"
v-model="query"
class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('projects.index.modal_move_folder.find_folder')" />
<i class="sn-icon sn-icon-search"></i>
</div>
</div>
@ -28,7 +34,9 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_move_folder.submit') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('projects.index.modal_move_folder.submit') }}
</button>
</div>
</div>
</div>
@ -36,19 +44,20 @@
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import modalMixin from '../../shared/modal_mixin';
import MoveTree from './move_tree.vue';
export default {
name: "NewProjectModal",
name: 'NewProjectModal',
props: {
selectedObjects: Array,
foldersTreeUrl: String,
moveToUrl: String,
},
mixins: [modal_mixin],
mixins: [modalMixin],
data() {
return {
selectedFolderId: null,
@ -57,7 +66,7 @@ export default {
};
},
components: {
MoveTree
MoveTree,
},
mounted() {
axios.get(this.foldersTreeUrl).then((response) => {
@ -66,39 +75,38 @@ export default {
},
computed: {
itemsName() {
return this.i18n.t('projects.index.modal_move_folder.items.' + this.itemsType);
return this.i18n.t(`projects.index.modal_move_folder.items.${this.itemsType}`);
},
title() {
return this.i18n.t('projects.index.modal_move_folder.title', {items: this.itemsName} );
return this.i18n.t('projects.index.modal_move_folder.title', { items: this.itemsName });
},
description() {
return this.i18n.t('projects.index.modal_move_folder.description', {items: this.itemsName} );
return this.i18n.t('projects.index.modal_move_folder.description', { items: this.itemsName });
},
itemsType() {
const allTypes = this.selectedObjects.map(obj => obj.type);
const allTypes = this.selectedObjects.map((obj) => obj.type);
const uniqueTypes = [...new Set(allTypes)];
if (uniqueTypes.length == 1) {
if (uniqueTypes.length === 1) {
return uniqueTypes[0];
} else {
return 'projects_and_folders';
}
return 'projects_and_folders';
},
filteredFoldersTree() {
if (this.query == '') {
if (this.query === '') {
return this.foldersTree;
} else {
return this.foldersTree.map((folder) => {
return {
folder: folder.folder,
children: folder.children.filter((child) => {
return child.folder.name.toLowerCase().includes(this.query.toLowerCase());
})
}
}).filter((folder) => {
return folder.folder.name.toLowerCase().includes(this.query.toLowerCase()) || folder.children.length > 0;
});
}
}
return this.foldersTree.map((folder) => (
{
folder: folder.folder,
children: folder.children.filter((child) => (
child.folder.name.toLowerCase().includes(this.query.toLowerCase())
)),
}
)).filter((folder) => (
folder.folder.name.toLowerCase().includes(this.query.toLowerCase())
|| folder.children.length > 0
));
},
},
methods: {
selectFolder(folderId) {
@ -107,19 +115,19 @@ export default {
submit() {
axios.post(this.moveToUrl, {
destination_folder_id: this.selectedFolderId || 'root_folder',
movables: this.selectedObjects.map((obj) => {
return {
movables: this.selectedObjects.map((obj) => (
{
id: obj.id,
type: obj.type,
}
})
)),
}).then((response) => {
this.$emit('move');
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
})
});
},
}
}
},
};
</script>

View file

@ -6,14 +6,20 @@
@click="opendedFolders[object.folder.id] = !opendedFolders[object.folder.id]"
class="sn-icon p-2 pr-1 cursor-pointer"></i>
<i v-else class="sn-icon sn-icon-up p-2 pr-1 opacity-0"></i>
<div @click="$emit('selectFolder', object.folder.id)" class="cursor-pointer flex items-center pl-1 flex-1 gap-2 text-sn-blue hover:bg-sn-super-light-grey" :class="{'!bg-sn-super-light-blue': object.folder.id == value}">
<div @click="$emit('selectFolder', object.folder.id)"
class="cursor-pointer flex items-center pl-1 flex-1 gap-2
text-sn-blue hover:bg-sn-super-light-grey"
:class="{'!bg-sn-super-light-blue': object.folder.id == value}">
<i class="sn-icon sn-icon-folder"></i>
<div class="flex-1 truncate p-2 pl-0" :title="object.folder.name">
{{ object.folder.name }}
</div>
</div>
</div>
<MoveTree v-if="opendedFolders[object.folder.id]" :objects="object.children" :value="value" @selectFolder="$emit('selectFolder', $event)" />
<MoveTree v-if="opendedFolders[object.folder.id]"
:objects="object.children"
:value="value"
@selectFolder="$emit('selectFolder', $event)" />
</div>
</template>
@ -31,7 +37,7 @@ export default {
data() {
return {
opendedFolders: {},
}
};
},
}
};
</script>

View file

@ -3,7 +3,9 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
{{ i18n.t('projects.index.modal_new_project.modal_title') }}
</h4>
@ -12,7 +14,10 @@
<div class="mb-6">
<label class="sci-label">{{ i18n.t("projects.index.modal_new_project.name") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
<input type="text" v-model="name"
class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
</div>
</div>
<div class="flex gap-2 text-xs items-center">
@ -29,7 +34,9 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_new_project.create') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('projects.index.modal_new_project.create') }}
</button>
</div>
</div>
</div>
@ -38,18 +45,18 @@
<script>
import SelectDropdown from "../../shared/select_dropdown.vue";
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import modalMixin from '../../shared/modal_mixin';
export default {
name: "NewProjectModal",
name: 'NewProjectModal',
props: {
createUrl: String,
userRolesUrl: String,
currentFolderId: String,
},
mixins: [modal_mixin],
mixins: [modalMixin],
components: {
SelectDropdown,
},
@ -69,17 +76,17 @@ export default {
visibility: (this.visible ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole,
project_folder_id: this.currentFolderId,
}
},
}).then(() => {
this.error = null;
this.$emit('create');
}).catch((error) => {
this.error = error.response.data.name;
})
});
},
changeRole(role) {
this.defaultRole = role;
},
}
}
},
};
</script>

View file

@ -3,7 +3,9 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
{{ i18n.t('projects.index.modal_new_project_folder.modal_title') }}
</h4>
@ -12,13 +14,17 @@
<div class="mb-6">
<label class="sci-label">{{ i18n.t('projects.index.modal_new_project_folder.name') }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
<input type="text" v-model="name"
class="sci-input-field" autofocus="true"
:placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_new_project.create') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('projects.index.modal_new_project.create') }}
</button>
</div>
</div>
</div>
@ -27,19 +33,19 @@
<script>
import SelectDropdown from "../../shared/select_dropdown.vue";
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import modalMixin from '../../shared/modal_mixin';
export default {
name: "NewProjectModal",
name: 'NewProjectModal',
props: {
createFolderUrl: String,
userRolesUrl: String,
viewMode: String,
currentFolderId: String,
},
mixins: [modal_mixin],
mixins: [modalMixin],
components: {
SelectDropdown,
},
@ -55,18 +61,18 @@ export default {
project_folder: {
name: this.name,
parent_folder_id: this.currentFolderId,
archived: (this.viewMode === 'archived')
}
archived: (this.viewMode === 'archived'),
},
}).then(() => {
this.error = null;
this.$emit('create');
}).catch((error) => {
this.error = error.response.data.name;
})
});
},
changeRole(role) {
this.defaultRole = role;
},
}
}
},
};
</script>

View file

@ -3,7 +3,8 @@
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
<img :src="user.avatar" class="w-7 h-7" />
</div>
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle" class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle"
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
+{{ hiddenUsers.length }}
</div>
<div class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full bg-sn-light-grey text-sn-dark-grey">
@ -17,28 +18,27 @@ export default {
name: 'UsersRenderer',
props: {
params: {
required: true
required: true,
},
},
computed: {
users() {
return this.params.value || []
return this.params.value || [];
},
visibleUsers() {
return this.users.slice(0, 4)
return this.users.slice(0, 4);
},
hiddenUsers() {
return this.users.slice(4)
return this.users.slice(4);
},
hiddenUsersTitle() {
return this.hiddenUsers.map((user) => user.full_name).join("\u000d")
}
return this.hiddenUsers.map((user) => user.full_name).join('\u000d');
},
},
methods: {
openAccessModal() {
this.params.dtComponent.$emit('access', {} ,[this.params.data]);
}
}
}
</script>
this.params.dtComponent.$emit('access', {}, [this.params.data]);
},
},
};
</script>

View file

@ -8,7 +8,8 @@
{{ i18n.t('action_toolbar.no_actions') }}
</div>
<div v-for="action in actions" :key="action.name" class="sn-action-toolbar__action shrink-0">
<a :class="`rounded flex gap-2 items-center py-1.5 px-2.5 bg-sn-white color-sn-blue hover:no-underline focus:no-underline ${action.button_class}`"
<a :class="`rounded flex gap-2 items-center py-1.5 px-2.5
bg-sn-white color-sn-blue hover:no-underline focus:no-underline ${action.button_class}`"
:href="(['link', 'remote-modal']).includes(action.type) ? action.path : '#'"
:id="action.button_id"
:title="action.label"
@ -22,51 +23,53 @@
</template>
<script>
export default {
name: 'ActionToolbar',
props: {
actionsUrl: { type: String, required: true },
params: { type: Object }
},
data() {
return {
actions: [],
multiple: false,
reloadCallback: null,
loaded: false,
loading: true
}
},
watch: {
params() {
this.loadActions()
}
},
created() {
export default {
name: 'ActionToolbar',
props: {
actionsUrl: { type: String, required: true },
params: { type: Object },
},
data() {
return {
actions: [],
multiple: false,
reloadCallback: null,
loaded: false,
loading: true,
};
},
watch: {
params() {
this.loadActions();
},
methods: {
loadActions() {
this.loading = true;
this.loaded = false;
$.get(`${this.actionsUrl}?${new URLSearchParams(this.params).toString()}`, (data) => {
this.actions = data.actions;
this.loading = false;
this.loaded = true;
});
},
doAction(action, event) {
switch(action.type) {
case 'emit':
event.preventDefault();
this.$emit('toolbar:action', action);
// do nothing, this is handled by legacy code based on the button class
break;
case 'link':
// do nothing, already handled by href
break;
}
},
created() {
this.loadActions();
},
methods: {
loadActions() {
this.loading = true;
this.loaded = false;
$.get(`${this.actionsUrl}?${new URLSearchParams(this.params).toString()}`, (data) => {
this.actions = data.actions;
this.loading = false;
this.loaded = true;
});
},
doAction(action, event) {
switch (action.type) {
case 'emit':
event.preventDefault();
this.$emit('toolbar:action', action);
// do nothing, this is handled by legacy code based on the button class
break;
case 'link':
// do nothing, already handled by href
break;
default:
break;
}
}
}
},
},
};
</script>

View file

@ -1,17 +1,14 @@
export default {
methods: {
itemSelected() {
let item = this.dtComponent.selectedRows.find((item) => {
return item.id == this.params.id;
});
const item = this.dtComponent.selectedRows.find((i) => (i.id === this.params.id));
if (item) {
this.dtComponent.selectedRows = this.dtComponent.selectedRows.filter((item) => {
return item.id != this.params.id;
});
this.dtComponent.selectedRows = this.dtComponent.selectedRows
.filter((i) => (i.id !== this.params.id));
} else {
this.dtComponent.selectedRows.push(this.params);
}
}
}
}
},
},
};

View file

@ -35,13 +35,12 @@ export default {
currentPage: {
type: Number,
required: true,
}
},
},
computed: {
pages() {
let pages = [];
for (let i = 1; i <= this.totalPage; i++) {
const pages = [];
for (let i = 1; i <= this.totalPage; i += 1) {
if (i >= this.currentPage - 2 || this.totalPage <= 5) {
pages.push(i);
}
@ -51,7 +50,7 @@ export default {
}
}
return pages;
}
},
},
}
};
</script>

View file

@ -13,54 +13,53 @@
</template>
<script>
import MenuDropdown from '../menu_dropdown.vue'
import MenuDropdown from '../menu_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default {
name: 'RowMenuRenderer',
props: {
params: {
required: true
}
required: true,
},
},
data() {
return {
actionsMenu: []
}
actionsMenu: [],
};
},
components: {
MenuDropdown
MenuDropdown,
},
computed: {
formattedList() {
return this.actionsMenu.map((item) => {
let newItem = { text: item.label }
if (item.type == 'emit') {
newItem.emit = item.name
const newItem = { text: item.label };
if (item.type === 'emit') {
newItem.emit = item.name;
}
if (item.type == 'link') {
newItem.url = item.path
if (item.type === 'link') {
newItem.url = item.path;
}
newItem.params = item
newItem.params = item;
return newItem
})
}
return newItem;
});
},
},
methods: {
loadActions() {
if (this.actionsMenu.length > 0) return
if (this.actionsMenu.length > 0) return;
axios.get(this.params.data.urls.actions)
.then((response) => {
this.actionsMenu = response.data.actions
})
this.actionsMenu = response.data.actions;
});
},
handleEvents(event, option) {
const dt = this.params.dtComponent
dt.$emit(event, option.params, [this.params.data])
}
}
}
const dt = this.params.dtComponent;
dt.$emit(event, option.params, [this.params.data]);
},
},
};
</script>

View file

@ -42,7 +42,11 @@
:CheckboxSelectionCallback="withCheckboxes"
>
</ag-grid-vue>
<ActionToolbar v-if="selectedRows.length > 0 && actionsUrl" :actionsUrl="actionsUrl" :params="actionsParams" @toolbar:action="emitAction" />
<ActionToolbar
v-if="selectedRows.length > 0 && actionsUrl"
:actionsUrl="actionsUrl"
:params="actionsParams"
@toolbar:action="emitAction" />
</div>
<div class="flex items-center py-4">
<div class="mr-auto">
@ -67,10 +71,10 @@
</template>
<script>
import { AgGridVue } from "ag-grid-vue3";
import { AgGridVue } from 'ag-grid-vue3';
import PerfectScrollbar from 'vue3-perfect-scrollbar';
import axios from '../../../packs/custom_axios.js';
import SelectDropdown from '../select_dropdown.vue';
import PerfectScrollbar from 'vue3-perfect-scrollbar';
import Pagination from './pagination.vue';
import CustomHeader from './tableHeader';
import ActionToolbar from './action_toolbar.vue';
@ -78,7 +82,7 @@ import Toolbar from './toolbar.vue';
import RowMenuRenderer from './row_menu_renderer.vue';
export default {
name: "App",
name: 'App',
props: {
withCheckboxes: {
type: Boolean,
@ -105,11 +109,11 @@ export default {
},
toolbarActions: {
type: Object,
required: true
required: true,
},
reloadingTable: {
type: Boolean,
default: false
default: false,
},
activePageUrl: {
type: String,
@ -119,15 +123,15 @@ export default {
},
currentViewMode: {
type: String,
default: 'active'
default: 'active',
},
viewRenders: {
type: Object,
},
filters: {
type: Array,
default: () => []
}
default: () => [],
},
},
data() {
return {
@ -135,7 +139,7 @@ export default {
gridApi: null,
columnApi: null,
defaultColDef: {
resizable: true
resizable: true,
},
perPage: 20,
page: 1,
@ -146,7 +150,7 @@ export default {
initializing: true,
activeFilters: {},
currentViewRender: 'table',
cardCheckboxes: []
cardCheckboxes: [],
};
},
components: {
@ -157,11 +161,11 @@ export default {
agColumnHeader: CustomHeader,
ActionToolbar,
Toolbar,
RowMenuRenderer
RowMenuRenderer,
},
computed: {
perPageOptions() {
return [10, 20, 50, 100].map(value => [ value, `${value} ${this.i18n.t('datatable.rows')}` ]);
return [10, 20, 50, 100].map((value) => ([value, `${value} ${this.i18n.t('datatable.rows')}`]));
},
tableState() {
if (!localStorage.getItem(`datatable:${this.tableId}_columns_state`)) return null;
@ -170,40 +174,38 @@ export default {
},
actionsParams() {
return {
items: JSON.stringify(this.selectedRows.map(row => { return {id: row.id, type: row.type} }))
}
items: JSON.stringify(this.selectedRows.map((row) => ({ id: row.id, type: row.type }))),
};
},
gridOptions() {
return {
suppressCellFocus: true
}
suppressCellFocus: true,
};
},
extendedColumnDefs() {
let columns = this.columnDefs.map(column => {
return {
...column,
cellRendererParams: {
dtComponent: this
}
}
});
const columns = this.columnDefs.map((column) => ({
...column,
cellRendererParams: {
dtComponent: this,
},
}));
if (this.withCheckboxes) {
columns.unshift({
field: "checkbox",
field: 'checkbox',
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
checkboxSelection: true,
width: 48,
minWidth: 48,
resizable: false,
pinned: 'left'
pinned: 'left',
});
}
if (this.withRowMenu) {
columns.push({
field: "rowMenu",
field: 'rowMenu',
headerName: '',
width: 42,
minWidth: 42,
@ -211,22 +213,28 @@ export default {
sortable: false,
cellRenderer: 'RowMenuRenderer',
cellRendererParams: {
dtComponent: this
dtComponent: this,
},
pinned: 'right',
cellStyle: {padding: 0, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'visible'}
cellStyle: {
padding: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'visible',
},
});
}
return columns;
}
},
},
watch: {
reloadingTable() {
if (this.reloadingTable) {
this.loadData();
}
}
},
},
mounted() {
this.loadData();
@ -237,7 +245,11 @@ export default {
},
methods: {
formatData(data) {
return data.map( (item) => Object.assign({}, item.attributes, { id: item.id, type: item.type }) );
return data.map((item) => ({
...item.attributes,
id: item.id,
type: item.type,
}));
},
resize() {
if (this.tableState) return;
@ -253,8 +265,8 @@ export default {
order: this.order,
search: this.searchValue,
view_mode: this.currentViewMode,
filters: this.activeFilters
}
filters: this.activeFilters,
},
})
.then((response) => {
this.selectedRows = [];
@ -262,7 +274,7 @@ export default {
this.rowData = this.formatData(response.data.data);
this.totalPage = response.data.meta.total_pages;
this.$emit('tableReloaded');
})
});
},
onGridReady(params) {
this.gridApi = params.api;
@ -271,14 +283,14 @@ export default {
if (this.tableState) {
this.columnApi.applyColumnState({
state: this.tableState,
applyOrder: true
applyOrder: true,
});
}
setTimeout(() => {
this.initializing = false;
}, 200);
},
onFirstDataRendered(params) {
onFirstDataRendered() {
this.resize();
},
setPerPage(value) {
@ -291,13 +303,14 @@ export default {
this.loadData();
},
setOrder() {
const orderState = this.columnApi.getColumnState().filter(column => column.sort).map(column => {
return {
const orderState = this.columnApi.getColumnState()
.filter((column) => column.sort)
.map((column) => ({
column: column.colId,
dir: column.sort
}
});
this.order = orderState[0];
dir: column.sort,
}));
const [order] = orderState;
this.order = order;
this.saveColumnsState();
this.loadData();
},
@ -319,7 +332,7 @@ export default {
},
clickCell(e) {
if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) {
e.node.setSelected(true);
e.node.setSelected(true);
}
},
applyFilters(filters) {
@ -332,7 +345,7 @@ export default {
this.currentViewRender = view;
this.initializing = true;
this.selectedRows = [];
}
}
},
},
};
</script>

View file

@ -1,6 +1,7 @@
export default {
template: `
<div class="w-full grid items-center gap-2 grid-cols-[auto_1.5rem] cursor-pointer" @click="onSortRequested((activeSort == 'asc' ? 'desc' : 'asc'), $event)">
<div class="w-full grid items-center gap-2 grid-cols-[auto_1.5rem] cursor-pointer"
@click="onSortRequested((activeSort == 'asc' ? 'desc' : 'asc'), $event)">
<div v-if="params.html" class="customHeaderLabel truncate" v-html="params.html"></div>
<div v-else class="customHeaderLabel truncate">{{ params.displayName }}</div>
<div v-if="activeSort == 'asc'" class="customSortDownLabel text-sn-sleepy-grey">

View file

@ -39,7 +39,9 @@
<i :class="action.icon"></i>
{{ action.label }}
</a>
<div v-if="filters.length == 0" class="sci-input-container-v2" :class="{'w-48': showSearch, 'w-11': !showSearch}">
<div v-if="filters.length == 0"
class="sci-input-container-v2"
:class="{'w-48': showSearch, 'w-11': !showSearch}">
<input
ref="searchInput"
class="sci-input-field !pr-8"
@ -58,7 +60,7 @@
</template>
<script>
import MenuDropdown from '../menu_dropdown.vue'
import MenuDropdown from '../menu_dropdown.vue';
import FilterDropdown from '../filters/filter_dropdown.vue';
export default {
@ -66,7 +68,7 @@ export default {
props: {
toolbarActions: {
type: Object,
required: true
required: true,
},
searchValue: {
type: String,
@ -79,57 +81,57 @@ export default {
},
currentViewMode: {
type: String,
default: 'active'
default: 'active',
},
filters: {
type: Array,
default: () => []
default: () => [],
},
viewRenders: {
type: Array,
required: true
required: true,
},
currentViewRender: {
type: String,
required: true
}
required: true,
},
},
components: {
MenuDropdown,
FilterDropdown
FilterDropdown,
},
computed: {
viewModesMenu() {
return [
{ text: this.i18n.t('projects.index.active'), url: this.activePageUrl},
{ text: this.i18n.t('projects.index.archived'), url: this.archivedPageUrl }
]
{ text: this.i18n.t('projects.index.active'), url: this.activePageUrl },
{ text: this.i18n.t('projects.index.archived'), url: this.archivedPageUrl },
];
},
viewRendersMenu() {
return this.viewRenders.map((view) => {
const type = view.type;
const { type } = view;
switch (type) {
case 'cards':
return { text: this.i18n.t('toolbar.cards_view'), emit: 'setCardsView'};
return { text: this.i18n.t('toolbar.cards_view'), emit: 'setCardsView' };
case 'table':
return { text: this.i18n.t('toolbar.table_view'), emit: 'setTableView'};
return { text: this.i18n.t('toolbar.table_view'), emit: 'setTableView' };
default:
return view;
}
})
}
});
},
},
data() {
return {
showSearch: false
}
showSearch: false,
};
},
watch: {
searchValue() {
if (this.searchValue.length > 0) {
this.openSearch();
}
}
},
},
methods: {
openSearch() {
@ -141,18 +143,20 @@ export default {
}
},
doAction(action, event) {
switch(action.type) {
switch (action.type) {
case 'emit':
event.preventDefault();
this.$emit('toolbar:action', action);
break;
case 'link':
break;
default:
break;
}
},
applyFilters(filters) {
this.$emit('applyFilters', filters);
},
}
}
},
};
</script>

View file

@ -50,150 +50,148 @@
</template>
<script>
import VueDatePicker from '@vuepic/vue-datepicker';
import VueDatePicker from '@vuepic/vue-datepicker';
export default {
name: 'DateTimePicker',
props: {
mode: { type: String, default: 'datetime' },
clearable: { type: Boolean, default: false },
teleport: { type: Boolean, default: true },
defaultValue: { type: Date, required: false },
placeholder: { type: String },
standAlone: { type: Boolean, default: false, required: false },
dateClassName: { type: String, default: '' },
timeClassName: { type: String, default: '' },
disabled: { type: Boolean, default: false }
},
data() {
return {
manualUpdate: false,
datetime: this.defaultValue,
time: null,
markers: [
{
date: new Date(),
type: 'dot',
color: '#3B99FD',
},
]
}
},
created() {
if (this.defaultValue) {
export default {
name: 'DateTimePicker',
props: {
mode: { type: String, default: 'datetime' },
clearable: { type: Boolean, default: false },
teleport: { type: Boolean, default: true },
defaultValue: { type: Date, required: false },
placeholder: { type: String },
standAlone: { type: Boolean, default: false, required: false },
dateClassName: { type: String, default: '' },
timeClassName: { type: String, default: '' },
disabled: { type: Boolean, default: false },
},
data() {
return {
manualUpdate: false,
datetime: this.defaultValue,
time: null,
markers: [
{
date: new Date(),
type: 'dot',
color: '#3B99FD',
},
],
};
},
created() {
if (this.defaultValue) {
this.time = {
hours: this.defaultValue.getHours(),
minutes: this.defaultValue.getMinutes(),
};
}
},
components: {
VueDatePicker,
},
watch: {
defaultValue() {
this.datetime = this.defaultValue;
if (this.defaultValue instanceof Date) {
this.time = {
hours: this.defaultValue.getHours(),
minutes:this.defaultValue.getMinutes(),
}
hours: this.defaultValue.getHours(),
minutes: this.defaultValue.getMinutes(),
};
}
},
components: {
VueDatePicker
},
watch: {
defaultValue: function () {
this.datetime = this.defaultValue;
if (this.defaultValue instanceof Date) {
this.time = {
hours: this.defaultValue.getHours(),
minutes: this.defaultValue.getMinutes()
}
}
},
datetime: function () {
if (this.mode == 'time') {
datetime() {
if (this.mode === 'time') {
this.time = null;
this.time = null;
if (this.datetime instanceof Date) {
if (this.datetime instanceof Date) {
this.time = {
hours: this.datetime.getHours(),
minutes: this.datetime.getMinutes()
}
}
return
}
if (this.manualUpdate) {
this.manualUpdate = false;
return;
}
if ( this.datetime == null) {
this.$emit('cleared');
}
if (this.defaultValue != this.datetime) {
this.$emit('change', this.datetime);
}
},
time: function () {
if (this.manualUpdate) {
this.manualUpdate = false;
return;
}
if (this.mode != 'time') return;
let newDate;
if (this.time) {
newDate = new Date();
newDate.setHours(this.time.hours);
newDate.setMinutes(this.time.minutes);
} else {
newDate = {
hours: null,
minutes: null
minutes: this.datetime.getMinutes(),
};
this.$emit('cleared');
}
if (this.defaultValue != newDate) {
this.$emit('change', newDate);
}
return;
}
},
computed: {
compDatetime: {
get () {
if (this.mode == 'time') {
return this.time
} else {
return this.datetime
}
},
set (val) {
if (this.mode == 'time') {
this.time = val
} else {
this.datetime = val
}
}
},
format() {
if (this.mode == 'time') return 'HH:mm'
if (this.mode == 'date') return document.body.dataset.datetimePickerFormatVue
return `${document.body.dataset.datetimePickerFormatVue} HH:mm`
if (this.manualUpdate) {
this.manualUpdate = false;
return;
}
},
mounted() {
window.addEventListener('resize', this.close);
},
unmounted() {
window.removeEventListener('resize', this.close);
},
methods: {
close() {
this.$refs.datetimePicker.closeMenu();
},
closedHandler() {
this.$emit('closed');
},
clearedHandler() {
if (this.datetime == null) {
this.$emit('cleared');
}
}
}
if (this.defaultValue !== this.datetime) {
this.$emit('change', this.datetime);
}
},
time() {
if (this.manualUpdate) {
this.manualUpdate = false;
return;
}
if (this.mode !== 'time') return;
let newDate;
if (this.time) {
newDate = new Date();
newDate.setHours(this.time.hours);
newDate.setMinutes(this.time.minutes);
} else {
newDate = {
hours: null,
minutes: null,
};
this.$emit('cleared');
}
if (this.defaultValue !== newDate) {
this.$emit('change', newDate);
}
},
},
computed: {
compDatetime: {
get() {
if (this.mode === 'time') {
return this.time;
}
return this.datetime;
},
set(val) {
if (this.mode === 'time') {
this.time = val;
} else {
this.datetime = val;
}
},
},
format() {
if (this.mode === 'time') return 'HH:mm';
if (this.mode === 'date') return document.body.dataset.datetimePickerFormatVue;
return `${document.body.dataset.datetimePickerFormatVue} HH:mm`;
},
},
mounted() {
window.addEventListener('resize', this.close);
},
unmounted() {
window.removeEventListener('resize', this.close);
},
methods: {
close() {
this.$refs.datetimePicker.closeMenu();
},
closedHandler() {
this.$emit('closed');
},
clearedHandler() {
this.$emit('cleared');
},
},
};
</script>

View file

@ -12,7 +12,9 @@
<template v-if="!isOpen || !searchable">
<div class="truncate" v-if="labelRenderer && label" v-html="label"></div>
<div class="truncate" v-else-if="label">{{ label }}</div>
<div class="text-sn-grey truncate" v-else>{{ placeholder || this.i18n.t('general.select_dropdown.placeholder') }}</div>
<div class="text-sn-grey truncate" v-else>
{{ placeholder || this.i18n.t('general.select_dropdown.placeholder') }}
</div>
</template>
<input type="text"
ref="search"
@ -21,12 +23,17 @@
:placeholder="label || placeholder || this.i18n.t('general.select_dropdown.placeholder')"
class="w-full border-0 outline-none pl-0 placeholder:text-sn-grey" />
<i v-if="canClear" @click="clear" class="sn-icon ml-auto sn-icon-close"></i>
<i v-else class="sn-icon ml-auto" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
<i v-else class="sn-icon ml-auto"
:class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
</div>
<teleport to="body">
<div v-if="isOpen" ref="flyout" class="bg-white inline-block sn-shadow-menu-sm rounded w-full fixed z-[3000]">
<div v-if="isOpen" ref="flyout"
class="sn-dropdown bg-white inline-block sn-shadow-menu-sm rounded w-full
fixed z-[3000]">
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
<div @click="selectAll" :class="sizeClass" class="border-x-0 border-transparent border-solid border-b-sn-light-grey py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
<div @click="selectAll" :class="sizeClass"
class="border-x-0 border-transparent border-solid border-b-sn-light-grey
py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
<div class="sn-checkbox-icon"
:class="selectAllState"
></div>
@ -62,8 +69,8 @@
</template>
<script>
import { vOnClickOutside } from '@vueuse/components';
import FixedFlyoutMixin from './mixins/fixed_flyout.js';
import { vOnClickOutside } from '@vueuse/components'
export default {
name: 'SelectDropdown',
@ -71,7 +78,7 @@ export default {
value: { type: [String, Number, Array] },
options: { type: Array, default: () => [] },
optionsUrl: { type: String },
placeholder: { type: String},
placeholder: { type: String },
noOptionsPlaceholder: { type: String },
fewOptionsPlaceholder: { type: String },
allOptionsPlaceholder: { type: String },
@ -85,7 +92,7 @@ export default {
clearable: { type: Boolean, default: false },
},
directives: {
'click-outside': vOnClickOutside
'click-outside': vOnClickOutside,
},
data() {
return {
@ -94,51 +101,50 @@ export default {
fetchedOptions: [],
selectAllState: 'unchecked',
query: '',
fixedWidth: true
}
fixedWidth: true,
};
},
mixins: [FixedFlyoutMixin],
computed: {
sizeClass() {
switch (this.size) {
case 'xs':
return 'min-h-[36px]'
return 'min-h-[36px]';
case 'sm':
return 'min-h-[40px]'
return 'min-h-[40px]';
case 'md':
return 'min-h-[44px]'
return 'min-h-[44px]';
default:
return 'min-h-[44px]';
}
},
canClear() {
return this.clearable && this.label && this.isOpen
return this.clearable && this.label && this.isOpen;
},
rawOptions() {
if (this.optionsUrl) {
return this.fetchedOptions
} else {
return this.options
return this.fetchedOptions;
}
return this.options;
},
filteredOptions() {
if (this.query.length > 0 && !this.optionsUrl ) {
return this.rawOptions.filter(option => {
return option[1].toLowerCase().includes(this.query.toLowerCase())
})
} else {
return this.rawOptions
if (this.query.length > 0 && !this.optionsUrl) {
return this.rawOptions.filter((option) => (
option[1].toLowerCase().includes(this.query.toLowerCase())
));
}
return this.rawOptions;
},
label() {
if (this.multiple) {
return this.multipleLabel
} else {
return this.singleLabel
return this.multipleLabel;
}
return this.singleLabel;
},
singleLabel() {
const option = this.rawOptions.find(option => option[0] === this.newValue)
return this.renderLabel(option)
const option = this.rawOptions.find((i) => i[0] === this.newValue);
return this.renderLabel(option);
},
multipleLabel() {
if (!this.newValue) return false;
@ -147,29 +153,31 @@ export default {
if (this.newValue.length === 0) {
return false;
} else if (this.newValue.length === 1) {
this.selectAllState = 'indeterminate'
return this.renderLabel(this.rawOptions.find(option => option[0] === this.newValue[0]))
} else if (this.newValue.length === this.rawOptions.length) {
this.selectAllState = 'checked';
return this.allOptionsPlaceholder || this.i18n.t('general.select_dropdown.all_options_placeholder')
} else {
this.selectAllState = 'indeterminate'
return `${this.newValue.length} ${this.fewOptionsPlaceholder || this.i18n.t('general.select_dropdown.few_options_placeholder')}`
}
if (this.newValue.length === 1) {
this.selectAllState = 'indeterminate';
return this.renderLabel(this.rawOptions.find((option) => option[0] === this.newValue[0]));
}
if (this.newValue.length === this.rawOptions.length) {
this.selectAllState = 'checked';
return this.allOptionsPlaceholder || this.i18n.t('general.select_dropdown.all_options_placeholder');
}
this.selectAllState = 'indeterminate';
return `${this.newValue.length} ${
this.fewOptionsPlaceholder || this.i18n.t('general.select_dropdown.few_options_placeholder')
}`;
},
valueChanged() {
if (this.multiple) {
return !this.compareArrays(this.newValue, this.value)
} else {
return this.newValue != this.value
return !this.compareArrays(this.newValue, this.value);
}
}
return this.newValue !== this.value;
},
},
mounted() {
this.newValue = this.value;
if (!this.newValue && this.multiple) {
this.newValue = []
this.newValue = [];
}
this.fetchOptions();
},
@ -179,7 +187,7 @@ export default {
this.$nextTick(() => {
this.setPosition();
this.$refs.search?.focus();
})
});
}
},
query() {
@ -191,77 +199,81 @@ export default {
if (!option) return false;
if (this.labelRenderer) {
return this.labelRenderer(option)
} else {
return option[1]
return this.labelRenderer(option);
}
return option[1];
},
valueSelected(value) {
if (!this.newValue) return false;
if (this.multiple) {
return this.newValue.includes(value);
} else {
return this.newValue == value;
}
return this.newValue === value;
},
open() {
if (!this.disabled) this.isOpen = true
if (!this.disabled) this.isOpen = true;
},
clear() {
this.newValue = this.multiple ? [] : null;
this.query = '';
this.$emit('change', this.newValue)
this.$emit('change', this.newValue);
},
close() {
close(e) {
if (e && e.target.closest('.sn-dropdown')) return;
if (!this.isOpen) return;
this.isOpen = false
if (this.valueChanged) {
this.$emit('change', this.newValue)
}
this.query = '';
this.$nextTick(() => {
this.isOpen = false;
if (this.valueChanged) {
this.$emit('change', this.newValue);
}
this.query = '';
});
},
setValue(value) {
if(this.multiple) {
if (this.multiple) {
if (this.newValue.includes(value)) {
this.newValue = this.newValue.filter(v => v != value)
this.newValue = this.newValue.filter((v) => v !== value);
} else {
this.newValue.push(value)
this.newValue.push(value);
}
} else {
this.newValue = value
this.close()
this.newValue = value;
this.$nextTick(() => {
this.close();
});
}
},
selectAll() {
if (this.selectAllState === 'checked') {
this.newValue = []
this.newValue = [];
} else {
this.newValue = this.rawOptions.map(option => option[0])
this.newValue = this.rawOptions.map((option) => option[0]);
}
this.$emit('change', this.newValue)
this.$emit('change', this.newValue);
},
fetchOptions() {
if (this.optionsUrl) {
fetch(`${this.optionsUrl}?query=${this.query || ''}`)
.then(response => response.json())
.then(data => {
.then((response) => response.json())
.then((data) => {
this.fetchedOptions = data.data;
this.$nextTick(() => {
this.setPosition();
})
})
});
});
}
},
compareArrays(arr1, arr2) {
if (!arr1 || !arr2) return false;
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
for (let i = 0; i < arr1.length; i += 1) {
if (!arr2.includes(arr1[i])) return false;
}
return true;
}
},
},
}
};
</script>

View file

@ -33,10 +33,14 @@ module Lists
end
def urls
urls_list = {}
urls_list[:show] = table_experiment_path(object)
urls_list
{
show: table_experiment_path(object),
actions: actions_toolbar_experiments_path(items: [{ id: object.id }].to_json),
projects_to_clone: projects_to_clone_experiment_path(object),
projects_to_move: projects_to_move_experiment_path(object),
clone: clone_experiment_path(object),
move: move_experiment_path(object)
}
end
def workflow_img

View file

@ -39,10 +39,8 @@ module Toolbars
name: 'restore',
label: I18n.t('experiments.toolbar.restore_button'),
icon: 'sn-icon sn-icon-restore',
button_class: 'restore-experiments-btn',
path: restore_group_project_experiments_path(project_id: @experiments.first.project_id),
type: :request,
request_method: :post
type: :emit
}
end
@ -57,9 +55,7 @@ module Toolbars
name: 'edit',
label: I18n.t('experiments.index.edit_option'),
icon: 'sn-icon sn-icon-edit',
button_class: 'edit-btn',
path: edit_experiment_path(experiment),
type: 'remote-modal'
type: :emit
}
end
@ -80,9 +76,7 @@ module Toolbars
name: 'access',
label: I18n.t('general.access'),
icon: 'sn-icon sn-icon-project-member-access',
button_class: 'access-btn',
path: path,
type: 'remote-modal'
type: :emit
}
end
@ -97,9 +91,7 @@ module Toolbars
name: 'move',
label: I18n.t('experiments.toolbar.move_button'),
icon: 'sn-icon sn-icon-move',
button_class: 'move-experiments-btn',
path: move_modal_experiments_path(id: experiment.id),
type: 'remote-modal'
type: :emit
}
end
@ -114,10 +106,7 @@ module Toolbars
name: 'duplicate',
label: I18n.t('experiments.toolbar.duplicate_button'),
icon: 'sn-icon sn-icon-duplicate',
button_class: 'clone-experiment-btn',
path: clone_modal_experiments_path(id: experiment.id),
type: 'remote-modal',
request_method: :get
type: :emit
}
end
@ -128,10 +117,8 @@ module Toolbars
name: 'archive',
label: I18n.t('experiments.toolbar.archive_button'),
icon: 'sn-icon sn-icon-archive',
button_class: 'archive-experiments-btn',
path: archive_group_project_experiments_path(project_id: @experiments.first.project_id),
type: :request,
request_method: :post
type: :emit
}
end
end

View file

@ -1467,7 +1467,8 @@ en:
clone_option: "Duplicate"
move_option: "Move"
archive_option: "Archive"
archive_confirm: "Are you sure you want to archive this project?"
archive_confirm_title: "Archive experiment"
archive_confirm: "Are you sure you want to archive this experiment?"
restore_option: "Restore"
more_link: "More"
experiment_access: "Access"

View file

@ -427,6 +427,8 @@ Rails.application.routes.draw do
post :archive_my_modules
post :batch_clone_my_modules
get :search_tags
get :projects_to_clone
get :projects_to_move
end
end