diff --git a/app/adapters/workflow-config.ts b/app/adapters/workflow-config.ts new file mode 100644 index 000000000..c8a013730 --- /dev/null +++ b/app/adapters/workflow-config.ts @@ -0,0 +1,33 @@ +import DS from 'ember-data'; +import config from 'ember-get-config'; +import OsfAdapter from './osf-adapter'; + +const { + OSF: { + url: host, + webApiNamespace: namespace, + }, +} = config; + +export default class WorkFlowConfigAdapter extends OsfAdapter { + host = host.replace(/\/+$/, ''); + namespace = namespace; + + buildURL( + _: string | undefined, + id: string | null, + __: DS.Snapshot | null, + ___: string, + ____?: {}, + ): string { + const nodeUrl = super.buildURL('node', null, null, 'findRecord', {}); + const url = nodeUrl.replace(/\/nodes\/$/, '/project/'); + return `${url}${id}/workflow/config`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'workflow-config': WorkFlowConfigAdapter; + } // eslint-disable-line semi +} diff --git a/app/guid-node/styles.scss b/app/guid-node/styles.scss new file mode 100644 index 000000000..177581dc4 --- /dev/null +++ b/app/guid-node/styles.scss @@ -0,0 +1,52 @@ +@media screen and (max-width: 768px) { + .WorkFlow { + margin: 0; + padding: 0; + } +} + +h4 { + margin-top: 40px; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.workflow-dialog { + background-color: #fff; + padding: 20px; + border-radius: 8px; + width: 500px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + position: relative; +} + +.tabs { + margin-top: 60px; +} + +.tab-content { + height: 400px; + width: 100%; + overflow-y: auto; + overflow-x: auto; + white-space: nowrap; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} diff --git a/app/guid-node/workflow/controller.ts b/app/guid-node/workflow/controller.ts new file mode 100644 index 000000000..9e1a399b4 --- /dev/null +++ b/app/guid-node/workflow/controller.ts @@ -0,0 +1,738 @@ +import Controller from '@ember/controller'; +import EmberError from '@ember/error'; +import { action, computed, set } from '@ember/object'; +import { reads } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import DS from 'ember-data'; +import Intl from 'ember-intl/services/intl'; +import Node from 'ember-osf-web/models/node'; +import WorkFlowConfigModel from 'ember-osf-web/models/workflow-config'; +import Analytics from 'ember-osf-web/services/analytics'; +import StatusMessages from 'ember-osf-web/services/status-messages'; +import Toast from 'ember-toastr/services/toast'; + +export default class GuidNodeWorkFlow extends Controller { + [x: string]: any; + @service toast!: Toast; + @service intl!: Intl; + @service statusMessages!: StatusMessages; + @service analytics!: Analytics; + + @reads('model.taskInstance.value') + node?: Node; + + isPageDirty = false; + configCache?: DS.PromiseObject; + + @computed('config.isFulfilled') + get loading(): boolean { + return !this.config || !this.config.get('isFulfilled'); + } + + activeTab = ''; + isStopProcess = false; + selectedWorkflow = {}; + filePath = {}; + isRegistering = false; + isEditing = false; + isProcessDialogVisible = false; + workflowEngines: string[] = []; + @tracked selectedWorkflowEngine: string | null = null; + @tracked workflowName = ''; + @tracked workflowID: string | null = null; + @tracked creatorToken = 'none'; + @tracked adminToken = 'none'; + @tracked executorToken = 'none'; + @tracked editingWorkflowId: string | null = null; + + workflowsManage: Array<{ id: string; name: string; suspended: boolean }> = []; + workflowsProcess: Array<{ + engine: string; + processDefinitionName: string; + id: string; + startUserId: string; + startTime: string; + endTime: string; + statusofprocessing: string; + ended: string; + }> = []; + + workflowsTask: Array<{ + id: string; + processInstanceId: string; + name: string; + startTime: string; + endTime: string; + assignee: string; + }> = []; + + workflowsManageInfo: Array<{ + engine: string; + name: string; + id: string; + creatorToken: string; + adminToken: string; + executorToken: string; + processId: string; + filePath: string; + }> = []; + + workflowsProcessInfo: Array<{ + processDefinitionName: string; + id: string; + startUserId: string; + startTime: string; + endTime: string; + }> = []; + + taskDetails: Array<{ + id: string; + name: string; + startTime: string; + endTime: string; + }> = []; + + constructor(...args: any[]) { + super(...args); + this.executeWorkflow(); + } + + async executeWorkflow() { + await this.loadWorkflowConfig(); + await this.loadDataManage(); + await this.loadDataProcess(); + await this.loadDataTask(); + await this.loadDataManageInfo(); + await this.loadDataProcessInfo(); + } + + async loadWorkflowConfig() { + const currentUrl = window.location.href; + const parts = currentUrl.split('/'); + const pid = parts[3]; + const response = await fetch(`/api/v1/project/${pid}/workflow/workflow_connection`); + const data = await response.json(); + const engineNames: string[] = data.data.map((engine: { name: string }) => engine.name); + const engineUrls: string[] = data.data.map((engine: { url: string }) => engine.url); + const engineAccounts: string[] = data.data.map((engine: { account: string }) => engine.account); + const enginePasswords: string[] = data.data.map((engine: { password: string }) => engine.password); + + this.set('workflowEngines', engineNames); + this.set('workflowUrls', engineUrls); + this.set('workflowAccounts', engineAccounts); + this.set('workflowPasswords', enginePasswords); + } + + async fetchWorkflowData( + url: string, + method: string = 'GET', + body: any = null, + ) { + try { + const username = this.get('workflowAccounts')[0]; + const password = this.get('workflowPasswords')[0]; + const encodedCredentials = btoa(`${username}:${password}`); + + const headers: HeadersInit = { + Authorization: `Basic ${encodedCredentials}`, + }; + + if (method === 'POST' && body instanceof FormData) { + delete headers['Content-Type']; + } else if (body) { + headers['Content-Type'] = 'application/json'; + } + + let requestBody = null; + if (body) { + if (body instanceof FormData) { + requestBody = body; + } else { + requestBody = JSON.stringify(body); + } + } + + const response = await fetch(url, { + method, + headers, + body: requestBody, + }); + + if (!response.ok) { + if (response.status === 404) { + return null; + } + throw new Error(`Failed to ${method} request`); + } + + if (response.status === 204) { + return null; + } + + return await response.json(); + } catch (error) { + // Error during request + this.toast.error('Failed to load workflow data'); + throw error; + } + } + + async loadDataManage() { + const workflowUrls = this.get('workflowUrls'); + const pathToAppend = '/process-api/repository/process-definitions'; + + if (workflowUrls && workflowUrls.length > 0) { + const fullUrl = `${workflowUrls[0]}${pathToAppend}`; + const workflowData = await this.fetchWorkflowData(fullUrl); + + const processedData = workflowData.data.map((item: any) => ({ + id: item.id, + name: item.name, + suspended: item.suspended, + })); + + if (!this.isDestroyed && !this.isDestroying) { + this.set('workflowsManage', processedData); + } + } + } + + async loadDataProcess() { + const workflowUrls = this.get('workflowUrls'); + const pathToAppend = '/process-api/history/historic-process-instances'; + + if (workflowUrls && workflowUrls.length > 0) { + const fullUrl = `${workflowUrls[0]}${pathToAppend}`; + const workflowData2 = await this.fetchWorkflowData(fullUrl); + + const processedData2 = workflowData2.data.map((item: any) => ({ + id: item.id, + processDefinitionName: item.processDefinitionName, + startUserId: item.startUserId || '', + startTime: item.startTime || '', + endTime: item.endTime || '', + statusofprocessing: item.status || '', + ended: item.ended || '', + })); + + if (!this.isDestroyed && !this.isDestroying) { + this.set('workflowsProcess', processedData2); + } + } + } + + async loadDataTask() { + const workflowUrls = this.get('workflowUrls'); + const runtimePath = '/process-api/runtime/process-instances'; + const historicPath = '/process-api/history/historic-task-instances'; + + if (workflowUrls && workflowUrls.length > 0) { + try { + const [runtimeData, historicData] = await Promise.all([ + this.fetchWorkflowData(`${workflowUrls[0]}${runtimePath}`), + this.fetchWorkflowData(`${workflowUrls[0]}${historicPath}`), + ]); + + const validIds = new Set(runtimeData.data.map((item: any) => item.id)); + + const processedData = historicData.data + .filter((item: any) => validIds.has(item.processInstanceId)) + .map((item: any) => ({ + id: item.id, + processInstanceId: item.processInstanceId, + name: item.name, + startTime: item.startTime || '', + endTime: item.endTime || '', + assignee: item.assignee || '', + })); + + if (!this.isDestroyed && !this.isDestroying) { + this.set('workflowsTask', processedData); + } + + if (processedData.length > 0) { + this.set('activeTab', 'tab1'); + } + } catch (error) { + // Data acquisition error + } + } + } + + async loadDataManageInfo() { + const response = await fetch('/api/v1/addons/workflow/registered_workflows/'); + const data = await response.json(); + this.set('workflowsManageInfo', data.data); + } + + async loadDataProcessInfo() { + const workflowUrls = this.get('workflowUrls'); + const pathToAppend = '/process-api/history/historic-process-instances'; + + if (workflowUrls && workflowUrls.length > 0) { + try { + const workflowData5 = await this.fetchWorkflowData(`${workflowUrls[0]}${pathToAppend}`); + const workflowsProcess = this.get('workflowsProcess') || []; + const validIds = new Set(workflowsProcess.map((item: any) => item.id)); + + const processedData5 = workflowData5.data + .filter((item: any) => validIds.has(item.id)) + .map((item: any) => ({ + processDefinitionName: item.processDefinitionName, + id: item.id, + startUserId: item.startUserId || '', + startTime: item.startTime || '', + endTime: item.endTime || '', + })); + + if (!this.isDestroyed && !this.isDestroying) { + this.set('workflowsProcessInfo', processedData5); + } + } catch (error) { + // Data acquisition error + } + } + } + + @action + async toggleActivate(workflowId: string) { + const workflowUrls = this.get('workflowUrls'); + const pathToAppend = `/process-api/repository/process-definitions/${workflowId}`; + + if (workflowUrls && workflowUrls.length > 0) { + try { + const url = `${workflowUrls[0]}${pathToAppend}`; + const body = { action: 'activate' }; + + await this.fetchWorkflowData(url, 'PUT', body); + + const workflow = this.workflowsManage.find(w => w.id === workflowId); + if (workflow) { + if (!this.isDestroyed && !this.isDestroying) { + set(workflow, 'suspended', false); + } + } + } catch (error) { + // Error activating workflow + } + } + } + + @action + async toggleDeactivate(workflowId: string) { + const workflowUrls = this.get('workflowUrls'); + const pathToAppend = `/process-api/repository/process-definitions/${workflowId}`; + + if (workflowUrls && workflowUrls.length > 0) { + try { + const url = `${workflowUrls[0]}${pathToAppend}`; + const body = { action: 'suspend' }; + + await this.fetchWorkflowData(url, 'PUT', body); + + const workflow = this.workflowsManage.find(w => w.id === workflowId); + if (workflow) { + if (!this.isDestroyed && !this.isDestroying) { + set(workflow, 'suspended', true); + } + } + } catch (error) { + // Error suspending workflow + } + } + } + + @action + async stopProcess(workflowId: string) { + this.set('isStopProcess', true); + const workflowUrls = this.get('workflowUrls'); + const runtimePath = `/process-api/runtime/process-instances/${workflowId}`; + const historyPath = `/process-api/history/historic-process-instances/${workflowId}`; + + if (workflowUrls && workflowUrls.length > 0) { + try { + let processExists = true; + + try { + await this.fetchWorkflowData(`${workflowUrls[0]}${runtimePath}`, 'GET'); + } catch (error) { + if (error.status === 404) { + processExists = false; + } else { + throw error; + } + } + + if (processExists) { + await this.fetchWorkflowData(`${workflowUrls[0]}${runtimePath}`, 'DELETE'); + } + + await this.fetchWorkflowData(`${workflowUrls[0]}${historyPath}`, 'DELETE'); + + await this.loadDataProcess(); + await this.loadDataProcessInfo(); + } catch (error) { + // Error deleting process + } + + if (!this.isDestroyed && !this.isDestroying) { + this.set('isStopProcess', false); + } + } + } + + @action + async registerWorkflow2() { + const currentUrl = window.location.href; + const parts = currentUrl.split('/'); + const pid = parts[3]; + + const data = { + workflowEngine: this.selectedWorkflowEngine, + workflowName: this.workflowName, + workflowID: this.workflowID, + creatorToken: this.creatorToken, + adminToken: this.adminToken, + executorToken: this.executorToken, + }; + + const missingFields = Object.keys(data).filter(key => { + const value = data[key as keyof typeof data]; + return !value; + }); + + if (missingFields.length > 0) { + this.toast.error(`Missing required fields: ${missingFields.join(', ')}`); + return; + } + + try { + const response = await fetch(`/api/v1/project/${pid}/workflow/register_workflow`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Failed to register workflow: ${JSON.stringify(errorData)}`); + } + + this.toast.success('Workflow registered successfully.'); + this.set('isRegistering', false); + await this.loadDataManageInfo(); + } catch (error) { + // Error registering workflow + this.toast.error(`Failed to register workflow: ${error.message}`); + this.set('isRegistering', false); + } + } + + @action + async processRegisterWorkflow() { + const currentUrl = window.location.href; + const parts = currentUrl.split('/'); + const pid = parts[3]; + + const data = { + workflowEngine: this.selectedWorkflowEngine, + workflowName: this.workflowName, + workflowID: this.workflowID, + creatorToken: this.creatorToken, + adminToken: this.adminToken, + executorToken: this.executorToken, + }; + + const missingFields = Object.keys(data).filter(key => { + const value = data[key as keyof typeof data]; + return !value; + }); + + if (missingFields.length > 0) { + this.toast.error(`Missing required fields: ${missingFields.join(', ')}`); + return; + } + + try { + const response = await fetch(`/api/v1/project/${pid}/workflow/register_workflow`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Failed to register workflow: ${JSON.stringify(errorData)}`); + } + + this.toast.success('Workflow registered successfully.'); + this.set('isRegistering', false); + await this.loadDataManageInfo(); + } catch (error) { + // Error registering workflow + this.toast.error(`Failed to register workflow: ${error.message}`); + this.set('isRegistering', false); + } + } + + @action + async removeworkflow(workflowId: string) { + const currentUrl = window.location.href; + const parts = currentUrl.split('/'); + const pid = parts[3]; + try { + const response = await fetch(`/api/v1/project/${pid}/workflow/remove_workflow/${workflowId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Failed to remove workflow: ${JSON.stringify(errorData)}`); + } + + this.toast.success('Workflow removed successfully.'); + await this.loadDataManageInfo(); + } catch (error) { + // Error removing workflow + this.toast.error(`Failed to remove workflow: ${error.message}`); + } + } + + @action + async updateWorkflow() { + const currentUrl = window.location.href; + const parts = currentUrl.split('/'); + const pid = parts[3]; + const data = { + workflowName: this.workflowName, + creatorToken: this.creatorToken, + adminToken: this.adminToken, + executorToken: this.executorToken, + }; + + const missingFields = Object.keys(data).filter(key => { + const value = data[key as keyof typeof data]; + return !value; + }); + + if (missingFields.length > 0) { + this.toast.error(`Missing required fields: ${missingFields.join(', ')}`); + return; + } + + try { + const response = await fetch( + `/api/v1/project/${pid}/workflow/register_workflow/${this.editingWorkflowId}`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`Failed to update workflow: ${JSON.stringify(errorData)}`); + } + + this.toast.success('Workflow updated successfully.'); + this.set('isRegistering', false); + this.set('isEditing', false); + await this.loadDataManageInfo(); + } catch (error) { + // Error updating workflow + this.toast.error(`Failed to update workflow: ${error.message}`); + this.set('isRegistering', false); + this.set('isEditing', false); + } + } + + @action + editWorkflow(workflowId: string) { + const workflow = this.workflowsManageInfo.find((w: any) => w.id === workflowId); + if (workflow) { + this.set('isRegistering', true); + this.set('isEditing', true); + this.editingWorkflowId = workflowId; + this.selectedWorkflowEngine = workflow.engine; + this.workflowName = workflow.name; + this.workflowID = workflow.id; + this.creatorToken = workflow.creatorToken; + this.adminToken = workflow.adminToken; + this.executorToken = workflow.executorToken; + } + } + + @action + setActiveTab(tab: string) { + this.set('activeTab', tab); + } + + @action + registerWorkflow() { + this.set('isRegistering', true); + this.set('isEditing', false); + this.selectedWorkflowEngine = null; + this.workflowName = ''; + this.workflowID = null; + this.creatorToken = 'none'; + this.adminToken = 'none'; + this.executorToken = 'none'; + } + + @action + closeRegisterDialog() { + this.set('isRegistering', false); + this.set('isEditing', false); + } + + @action + async openProcessDialog(workflowId: string) { + if (this.isStopProcess) { + return; + } + + const workflowUrls = this.get('workflowUrls'); + const pathToProcess = `/process-api/history/historic-task-instances?processInstanceId=${workflowId}`; + const pathToTaskDetail = '/process-api/history/historic-task-instances/'; + + const selectedWorkflow = this.get('workflowsProcessInfo').find(workflow => workflow.id === workflowId); + this.set('selectedWorkflow', selectedWorkflow); + this.set('isProcessDialogVisible', true); + + try { + const response = await fetch('/api/v1/addons/workflow/registered_workflows/'); + const data = await response.json(); + this.set('workflowsManageInfo', data.data); + + const tasks = await this.fetchWorkflowData(`${workflowUrls[0]}${pathToProcess}`); + + if (!tasks || !tasks.data || tasks.data.length === 0) { + this.set('taskDetails', []); + return; + } + + const taskDetails = await Promise.all( + tasks.data.map(async (task: any) => { + const detail = await this.fetchWorkflowData( + `${workflowUrls[0]}${pathToTaskDetail}${task.id}`, 'GET', null, + ); + return { + id: detail.id, + name: detail.name || '', + startTime: detail.startTime || '', + endTime: detail.endTime || '', + }; + }), + ); + + this.set('taskDetails', taskDetails); + + const matchingWorkflow = this.get('workflowsManageInfo').find( + workflow => workflow.processId === workflowId, + ); + if (matchingWorkflow) { + this.set('filePath', matchingWorkflow); + } else { + this.set('filePath', ''); + } + } catch (error) { + // Failed to load task details + this.set('taskDetails', []); + } + } + + @action + closeProcessDialog() { + this.set('isProcessDialogVisible', false); + } + + @action + async updatebutton() { + await this.loadDataProcess(); + await this.loadDataProcessInfo(); + } + + @action + async updatebuttonTask() { + await this.loadDataProcess(); + await this.loadDataTask(); + } + + @action + navigateToTasks(taskId: string): void { + const workflowUrls = this.get('workflowUrls'); + const baseUrl = workflowUrls && workflowUrls.length > 0 ? workflowUrls[0] : ''; + + if (!this.isDestroyed && !this.isDestroying && baseUrl) { + const taskUrl = `/workflow/#/apps/Workflow_Task_App/tasks/${taskId}`; + window.open(`${baseUrl}${taskUrl}`, '_blank'); + } + } + + @computed('config.param1') + get param1() { + if (!this.config || !this.config.get('isFulfilled')) { + return ''; + } + const config = this.config.content as WorkFlowConfigModel; + return config.param1; + } + + set param1(v: string) { + if (!this.config) { + throw new EmberError('Illegal config'); + } + const config = this.config.content as WorkFlowConfigModel; + config.set('param1', v); + this.set('isPageDirty', true); + } + + @action + updateWorkflowEngine(event: Event) { + this.selectedWorkflowEngine = (event.target as HTMLSelectElement).value; + } + + @action + updateToken(tokenType: string, event: Event) { + (this as any)[tokenType] = (event.target as HTMLInputElement).value; + } + + @action + updateWorkflowName(event: Event) { + this.workflowName = (event.target as HTMLInputElement).value; + } + + @action + updateWorkflowID(event: Event) { + this.workflowID = (event.target as HTMLInputElement).value; + } + + @computed('node') + get config(): DS.PromiseObject | undefined { + if (this.configCache) { + return this.configCache; + } + if (!this.node) { + return undefined; + } + this.configCache = this.store.findRecord('workflow-config', this.node.id); + return this.configCache!; + } +} + +declare module '@ember/controller' { + interface Registry { + 'guid-node/workflow': GuidNodeWorkFlow; + } +} diff --git a/app/guid-node/workflow/route.ts b/app/guid-node/workflow/route.ts new file mode 100644 index 000000000..1acc8ac67 --- /dev/null +++ b/app/guid-node/workflow/route.ts @@ -0,0 +1,33 @@ +import { action, computed } from '@ember/object'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; + +import GuidNodeWorkFlow from 'ember-osf-web/guid-node/workflow/controller'; +import Node from 'ember-osf-web/models/node'; +import { GuidRouteModel } from 'ember-osf-web/resolve-guid/guid-route'; +import Analytics from 'ember-osf-web/services/analytics'; + +export default class GuidNodeWorkFlowRoute extends Route.extend(ConfirmationMixin, {}) { + @service analytics!: Analytics; + + model(this: GuidNodeWorkFlowRoute) { + return this.modelFor('guid-node'); + } + + @action + async didTransition() { + const { taskInstance } = this.controller.model as GuidRouteModel; + await taskInstance; + const node = taskInstance.value; + + this.analytics.trackPage(node ? node.public : undefined, 'nodes'); + } + + // This tells ember-onbeforeunload's ConfirmationMixin whether or not to stop transitions + @computed('controller.isPageDirty') + get isPageDirty() { + const controller = this.controller as GuidNodeWorkFlow; + return () => controller.isPageDirty; + } +} diff --git a/app/guid-node/workflow/styles.scss b/app/guid-node/workflow/styles.scss new file mode 100644 index 000000000..177581dc4 --- /dev/null +++ b/app/guid-node/workflow/styles.scss @@ -0,0 +1,52 @@ +@media screen and (max-width: 768px) { + .WorkFlow { + margin: 0; + padding: 0; + } +} + +h4 { + margin-top: 40px; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.workflow-dialog { + background-color: #fff; + padding: 20px; + border-radius: 8px; + width: 500px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + position: relative; +} + +.tabs { + margin-top: 60px; +} + +.tab-content { + height: 400px; + width: 100%; + overflow-y: auto; + overflow-x: auto; + white-space: nowrap; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} diff --git a/app/guid-node/workflow/template.hbs b/app/guid-node/workflow/template.hbs new file mode 100644 index 000000000..2cda01873 --- /dev/null +++ b/app/guid-node/workflow/template.hbs @@ -0,0 +1,319 @@ +{{title (t 'workflow.page_title' nodeTitle=this.model.taskInstance.value.unsafeTitle)}} + +
+ {{#if this.loading}} + {{t 'workflow.loading'}} + {{else}} +
+
+ +
+ +
+
+

{{t 'workflow.tab1_content'}}

+ + {{t 'workflow.update_button2'}} + + +

{{t 'workflow.workflow_task_list'}}

+ + + + + + + + + + + + + {{#each (sortAndFilterData this.workflowsTask) as |workflowsTask|}} + + + + + + + + + {{/each}} + +
{{t 'workflow.processid'}}{{t 'workflow.workflow2_name'}}{{t 'workflow.status'}}{{t 'workflow.starting time'}}{{t 'workflow.completion time'}}{{t 'workflow.request_targer'}}
{{workflowsTask.processInstanceId}}{{workflowsTask.name}} + {{#if workflowsTask.endTime}} + 完了 + {{else}} + 実行中 + {{/if}} + {{formatDateToJST workflowsTask.startTime}}{{formatDateToJST workflowsTask.endTime}}{{workflowsTask.assignee}}
+ +
+ +
+

{{t 'workflow.tab2_content'}}

+ + + {{t 'workflow.update_button'}} + + +

{{t 'workflow.available workflow processes'}}

+ + + + + + + + + + + + + + {{#each this.workflowsProcessInfo as |workflowsProcessInfo|}} + + + + + + + + + + + {{/each}} + +
{{t 'workflow.workflow2_name'}}{{t 'workflow.workflow_processid'}}{{t 'workflow.initiator'}}{{t 'workflow.starting time'}}{{t 'workflow.completion time'}}{{t 'workflow.status of processing'}}{{t 'workflow.execution state'}}
{{workflowsProcessInfo.processDefinitionName}}{{workflowsProcessInfo.id}}{{workflowsProcessInfo.startUserId}}{{formatDateToJST workflowsProcessInfo.startTime}}{{formatDateToJST workflowsProcessInfo.endTime}} + {{#if workflowsProcessInfo.endTime}} + 終了 + {{else}} + 開始 + {{/if}} + + {{#if workflowsProcessInfo.endTime}} + 完了 + {{else}} + 実行中 + {{/if}} + + {{#if workflowsProcessInfo}} + + {{t 'workflow.stop_button'}} + + {{/if}} +
+ + {{#if this.isProcessDialogVisible}} +
+
+

{{t 'workflow.workflowprocessinformation'}}


+ + {{this.selectedWorkflow.processDefinitionName}}
+ + {{this.selectedWorkflow.id}}
+
+ {{this.filePath.filePath}}
+ +
+ {{#each this.taskDetails as |task|}} + {{#if task.endTime}} +
{{task.name}}
+ {{/if}} + {{/each}} + +
+ {{#each this.taskDetails as |task|}} + {{#unless task.endTime}} +
{{task.name}}
+ {{/unless}} + {{/each}} + +
+ + {{t 'workflow.cancel_button'}} + +
+
+
+ {{/if}} + +
+ +
+

{{t 'workflow.tab3_content'}}

+ + {{t 'workflow.register_button'}} + + +

{{t 'workflow.Available_Workflows'}}

+
    + {{#each this.workflowsManage as |workflowsManage|}} + {{#each this.workflowsManageInfo as |match|}} + {{#if (eq workflowsManage.id match.id)}} +
  • + {{match.name}} + {{#if workflowsManage.suspended}} + + {{t 'workflow.activate'}} + + {{else}} + + {{t 'workflow.deactivate'}} + + {{/if}} + + {{t 'workflow.edit'}} + + + {{t 'workflow.remove'}} + +
  • + {{break}} + {{/if}} + {{/each}} + {{/each}} +
+ + {{#if this.isRegistering}} +
+
+

{{if this.isEditing (t 'workflow.workflow_edit') (t 'workflow.workflow_registration')}}

+
+
+
+ +
+
+ +
+
+ +
+ ReadWriteで使用する + Readで使用する + 使用しない
+ +
+ ReadWriteで使用する + Readで使用する + 使用しない
+ +
+ ReadWriteで使用する + Readで使用する + 使用しない
+ +
+ + {{t 'workflow.cancel_button'}} + + + {{if this.isEditing (t 'workflow.update_button') (t 'workflow.register_button2')}} + +
+
+
+
+ {{/if}} +
+
+
+ {{/if}} +
\ No newline at end of file diff --git a/app/helpers/extractOuterParentheses.js b/app/helpers/extractOuterParentheses.js new file mode 100644 index 000000000..edd1d3fe5 --- /dev/null +++ b/app/helpers/extractOuterParentheses.js @@ -0,0 +1,25 @@ +import { helper } from '@ember/component/helper'; + +export function extractOuterParentheses([name]) { + if (typeof name !== 'string') return ''; + let start = -1; + let depth = 0; + let outerContent = ''; + for (let i = 0; i < name.length; i++) { + const char = name[i]; + if (char === '' || char === '(') { + if (depth === 0) { + start = i + 1; + } + depth++; + } else if (char === '' || char === ')') { + depth--; + if (depth === 0 && start !== -1) { + outerContent = name.slice(start, i); + break; + } + } + } + return outerContent; +} +export default helper(extractOuterParentheses); diff --git a/app/helpers/formatDateToJST.js b/app/helpers/formatDateToJST.js new file mode 100644 index 000000000..114caa299 --- /dev/null +++ b/app/helpers/formatDateToJST.js @@ -0,0 +1,17 @@ +import { helper } from '@ember/component/helper'; + +export function formatDateToJST([date]) { + if (!date) { + return ''; + } + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + + return `${year}/${month}/${day} ${hours}:${minutes}`; +} + +export default helper(formatDateToJST); diff --git a/app/helpers/sortAndFilterData.js b/app/helpers/sortAndFilterData.js new file mode 100644 index 000000000..09d2637bc --- /dev/null +++ b/app/helpers/sortAndFilterData.js @@ -0,0 +1,20 @@ +import { helper } from '@ember/component/helper'; + +export function sortAndFilterData([data = []]) { + if (!Array.isArray(data)) { + return []; + } + + const sortedData = [...data].sort((a, b) => { + const timeA = a.startTime ? new Date(a.startTime).getTime() : 0; + const timeB = b.startTime ? new Date(b.startTime).getTime() : 0; + return timeA - timeB; + }); + + const inProgressData = sortedData.filter(item => !item.endTime); + const completedData = sortedData.filter(item => item.endTime); + + return [...inProgressData, ...completedData]; +} + +export default helper(sortAndFilterData); diff --git a/app/models/registration-schema.ts b/app/models/registration-schema.ts index 25194f299..c264e3c09 100644 --- a/app/models/registration-schema.ts +++ b/app/models/registration-schema.ts @@ -31,6 +31,7 @@ export interface Page { questions: Question[]; type?: 'object'; description?: string; + clipboardCopyPaste: boolean; } export interface Schema { diff --git a/app/models/schema-block.ts b/app/models/schema-block.ts index ede4f06f5..e9213e04a 100644 --- a/app/models/schema-block.ts +++ b/app/models/schema-block.ts @@ -22,9 +22,15 @@ export default class SchemaBlockModel extends OsfModel implements SchemaBlock { @attr('number') index?: number; @attr('string') pattern?: string; @attr('boolean') spaceNormalization?: boolean; - @attr('boolean') autoDate?: boolean; - @attr('boolean') autoTitle?: boolean; - @attr('boolean') hideProjectmetadata?: boolean; + @attr('string') retrievalTitle?: string; + @attr('string') retrievalDate?: string; + @attr('boolean') concealmentPageNavigator?: boolean; + @attr('string') requiredAllCheck?: string; + @attr('boolean') multiLanguage?: boolean; + @attr('string') retrievalVersion?: string; + @attr('boolean') readonly?: boolean; + @attr('boolean') sentence?: boolean; + @attr('string') rowAdditionCaption?: string; @belongsTo('registration-schema', { inverse: 'schemaBlocks', async: false }) schema?: RegistrationSchemaModel; diff --git a/app/models/workflow-config.ts b/app/models/workflow-config.ts new file mode 100644 index 000000000..4f1471861 --- /dev/null +++ b/app/models/workflow-config.ts @@ -0,0 +1,14 @@ +import DS from 'ember-data'; +import OsfModel from './osf-model'; + +const { attr } = DS; + +export default class WorkFlowConfigModel extends OsfModel { + @attr('string') param1!: string; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'workflow-config': WorkFlowConfigModel; + } // eslint-disable-line semi +} diff --git a/app/packages/registration-schema/get-pages.ts b/app/packages/registration-schema/get-pages.ts index c458942be..de58f2762 100644 --- a/app/packages/registration-schema/get-pages.ts +++ b/app/packages/registration-schema/get-pages.ts @@ -3,15 +3,16 @@ import { SchemaBlock } from 'ember-osf-web/packages/registration-schema'; export function getPages(blocks: SchemaBlock[]) { const pageArray = blocks.reduce( (pages, block) => { + // instantiate first page if the schema doesn't start with a page-heading if (pages.length === 0 && block.blockType !== 'page-heading' - && (block.hideProjectmetadata === true || block.hideProjectmetadata === undefined)) { + && (block.concealmentPageNavigator === true || block.concealmentPageNavigator === undefined)) { const blankPage: SchemaBlock[] = []; pages.push(blankPage); } - const lastPage: SchemaBlock[] = pages.slice(-1)[0] || []; + const lastPage: SchemaBlock[] = pages.slice(-1)[0]; if (block.blockType === 'page-heading' - && (block.hideProjectmetadata === false || block.hideProjectmetadata === undefined)) { + && (block.concealmentPageNavigator === false || block.concealmentPageNavigator === undefined)) { pages.push([block]); } else { lastPage.push(block); diff --git a/app/packages/registration-schema/get-schema-block-group.ts b/app/packages/registration-schema/get-schema-block-group.ts index d459c262f..207275d62 100644 --- a/app/packages/registration-schema/get-schema-block-group.ts +++ b/app/packages/registration-schema/get-schema-block-group.ts @@ -43,6 +43,7 @@ export function getSchemaBlockGroups(blocks: SchemaBlock[] | undefined) { case 'jgn-program-name-ja-input': case 'jgn-program-name-en-input': case 'e-rad-award-funder-input': + case 'single-select-pulldown-input': case 'pulldown-input': case 'e-rad-award-number-input': case 'e-rad-award-title-ja-input': @@ -53,6 +54,7 @@ export function getSchemaBlockGroups(blocks: SchemaBlock[] | undefined) { case 'e-rad-researcher-name-en-input': case 'e-rad-bunnya-input': case 'file-metadata-input': + case 'ad-metadata-input': case 'date-input': case 'section-heading': case 'subsection-heading': diff --git a/app/packages/registration-schema/index.ts b/app/packages/registration-schema/index.ts index b09e7121f..d8f93c645 100644 --- a/app/packages/registration-schema/index.ts +++ b/app/packages/registration-schema/index.ts @@ -2,7 +2,12 @@ export { getPages } from './get-pages'; export { getSchemaBlockGroups } from './get-schema-block-group'; export { SchemaBlock, SchemaBlockType } from './schema-block'; export { SchemaBlockGroup } from './schema-block-group'; -export { buildValidation, buildMetadataValidations, setupEventForSyncValidation } from './validations'; +export { + buildValidation, + buildMetadataValidations, + setupEventForSyncValidation, + setupEventForSyncValidation2, +} from './validations'; export { FileReference, RegistrationResponse, diff --git a/app/packages/registration-schema/page-manager.ts b/app/packages/registration-schema/page-manager.ts index b801a28ce..a7a099372 100644 --- a/app/packages/registration-schema/page-manager.ts +++ b/app/packages/registration-schema/page-manager.ts @@ -10,6 +10,7 @@ import { SchemaBlock, SchemaBlockGroup, setupEventForSyncValidation, + setupEventForSyncValidation2, } from 'ember-osf-web/packages/registration-schema'; import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema/registration-response'; @@ -17,14 +18,14 @@ export class PageManager { changeset?: ChangesetDef; schemaBlockGroups?: SchemaBlockGroup[]; pageHeadingText?: string; - hideProjectmetadata?: boolean; + concealmentPageNavigator?: boolean; isVisited?: boolean; constructor(pageSchemaBlocks: SchemaBlock[], registrationResponses: RegistrationResponse, node?: NodeModel) { this.schemaBlockGroups = getSchemaBlockGroups(pageSchemaBlocks); if (this.schemaBlockGroups) { this.pageHeadingText = this.schemaBlockGroups[0].labelBlock!.displayText!; - this.hideProjectmetadata = this.schemaBlockGroups[0].labelBlock!.hideProjectmetadata!; + this.concealmentPageNavigator = this.schemaBlockGroups[0].labelBlock!.concealmentPageNavigator!; this.isVisited = this.schemaBlockGroups.some( ({ registrationResponseKey: key }) => Boolean(key && (key in registrationResponses)), @@ -36,8 +37,11 @@ export class PageManager { lookupValidator(validations), validations, ) as ChangesetDef; + setupEventForSyncValidation(this.changeset, this.schemaBlockGroups); + setupEventForSyncValidation2(this.changeset, this.schemaBlockGroups); + if (this.isVisited) { this.changeset.validate(); } diff --git a/app/packages/registration-schema/schema-block.ts b/app/packages/registration-schema/schema-block.ts index b99203c94..3c7aa46e9 100644 --- a/app/packages/registration-schema/schema-block.ts +++ b/app/packages/registration-schema/schema-block.ts @@ -17,6 +17,7 @@ export type SchemaBlockType = 'jgn-program-name-ja-input' | 'jgn-program-name-en-input' | 'e-rad-award-funder-input' | + 'single-select-pulldown-input' | 'pulldown-input' | 'e-rad-award-number-input' | 'e-rad-award-title-ja-input' | @@ -27,6 +28,7 @@ export type SchemaBlockType = 'e-rad-researcher-name-en-input' | 'e-rad-bunnya-input' | 'file-metadata-input' | + 'ad-metadata-input' | 'date-input' | 'array-input'; @@ -44,7 +46,12 @@ export interface SchemaBlock { index?: number; pattern?: string; spaceNormalization?: boolean; - autoDate?: boolean; - autoTitle?: boolean; hideProjectmetadata?: boolean; + retrievalTitle?: string; + retrievalDate?: string; + concealmentPageNavigator?: boolean; + requiredAllCheck?: string; + multiLanguage?: boolean; + retrievalVersion?: string; + rowAdditionCaption?: string; } diff --git a/app/packages/registration-schema/validations.ts b/app/packages/registration-schema/validations.ts index e844e3476..362a21ca9 100644 --- a/app/packages/registration-schema/validations.ts +++ b/app/packages/registration-schema/validations.ts @@ -161,6 +161,47 @@ export function setupEventForSyncValidation(changeset: ChangesetDef, groups: Sch }); } +export function setupEventForSyncValidation2(changeset: ChangesetDef, groups: SchemaBlockGroup[]) { + const requiredAllCheckGroups = groups + // ignore GRDM file specific fields + .filter((group: SchemaBlockGroup) => !group.registrationResponseKey + || !group.registrationResponseKey.match(/^__responseKey_grdm-file:.+$/)) + .filter((group: SchemaBlockGroup) => group.inputBlock && group.inputBlock.requiredAllCheck); + + let isProcesing = false; + + changeset.on('afterValidation', () => { + if (isProcesing) { + return; + } + isProcesing = true; + + try { + const checkboxList = requiredAllCheckGroups.map(group => { + const registrationResponseKey: string = group.registrationResponseKey || ''; + const value = changeset.get(registrationResponseKey); + return Array.isArray(value) && value.length === 1; + }); + + requiredAllCheckGroups + .forEach(group => { + if (!checkboxList.includes(false)) { + const todayDate = `${new Date().getFullYear()}/${ + String(new Date().getMonth() + 1).padStart(2, '0') + }/${ + String(new Date().getDate()).padStart(2, '0') + }`; + changeset.set(`__responseKey_${group.inputBlock!.requiredAllCheck}`, todayDate); + } else { + changeset.set(`__responseKey_${group.inputBlock!.requiredAllCheck}`, ''); + } + }); + } finally { + isProcesing = false; + } + }); +} + export function validateNodeLicense() { return async (_: unknown, __: unknown, ___: unknown, changes: DraftRegistration, content: DraftRegistration) => { let validateLicenseTarget = await content.license; diff --git a/app/router.ts b/app/router.ts index af203a45c..0b4285cf8 100644 --- a/app/router.ts +++ b/app/router.ts @@ -142,6 +142,7 @@ Router.map(function() { this.mount('analytics-page', { as: 'analytics' }); this.route('forks'); this.route('iqbrims'); + this.route('workflow'); this.route('binderhub'); this.route('metadata'); this.route('registrations'); diff --git a/app/serializers/workflow-config.ts b/app/serializers/workflow-config.ts new file mode 100644 index 000000000..41ddd2503 --- /dev/null +++ b/app/serializers/workflow-config.ts @@ -0,0 +1,10 @@ +import OsfSerializer from './osf-serializer'; + +export default class WorkFlowConfigSerializer extends OsfSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'workflow-config': WorkFlowConfigSerializer; + } // eslint-disable-line semi +} diff --git a/lib/osf-components/addon/components/contributor-list/component.ts b/lib/osf-components/addon/components/contributor-list/component.ts index 595e5bc45..ff6795b13 100644 --- a/lib/osf-components/addon/components/contributor-list/component.ts +++ b/lib/osf-components/addon/components/contributor-list/component.ts @@ -60,7 +60,15 @@ export default class ContributorList extends Component { this.set('totalContributors', nextPage.meta.total); } else { this.set('page', 1); - const firstPage = yield this.node.bibliographicContributors; + const firstPage = yield this.node.queryHasMany( + 'bibliographicContributors', + { + fields: { + users: 'full_name,given_name,family_name,id,links', + }, + page: { size: 10 }, + }, + ); this.setProperties({ displayedContributors: firstPage.toArray(), totalContributors: firstPage.meta.total, diff --git a/lib/osf-components/addon/components/form-controls/template.hbs b/lib/osf-components/addon/components/form-controls/template.hbs index d8cb46a9f..70454fe6e 100644 --- a/lib/osf-components/addon/components/form-controls/template.hbs +++ b/lib/osf-components/addon/components/form-controls/template.hbs @@ -14,13 +14,6 @@ shouldShowMessages=this.shouldShowMessages disabled=this.disabled ) - autodate=( - component - 'validated-input/autodate' - changeset=@changeset - shouldShowMessages=this.shouldShowMessages - disabled=this.disabled - ) recaptcha=( component 'validated-input/recaptcha' diff --git a/lib/osf-components/addon/components/node-navbar/component.ts b/lib/osf-components/addon/components/node-navbar/component.ts index c7bcf1127..55064a2a6 100644 --- a/lib/osf-components/addon/components/node-navbar/component.ts +++ b/lib/osf-components/addon/components/node-navbar/component.ts @@ -106,6 +106,22 @@ export default class NodeNavbar extends Component { return result; } + @computed('node.addons.[]') + get workflowEnabled(): Promise | null { + if (!this.node) { + return null; + } + const { node } = this; + return (async () => { + const addons = await node.addons; + if (!addons) { + return false; + } + const workflow = addons.filter(addon => addon.id === 'workflow'); + return workflow.length > 0; + })(); + } + @action toggleNav() { this.toggleProperty('collapsedNav'); diff --git a/lib/osf-components/addon/components/node-navbar/template.hbs b/lib/osf-components/addon/components/node-navbar/template.hbs index a107122f9..59055b937 100644 --- a/lib/osf-components/addon/components/node-navbar/template.hbs +++ b/lib/osf-components/addon/components/node-navbar/template.hbs @@ -35,6 +35,9 @@ {{/node-navbar/link}} {{node-navbar/link node=@node useLinkTo=false destination='files'}} + {{#if this.workflowEnabled }} + {{node-navbar/link node=@node useLinkTo=false destination='workflow'}} + {{/if}} {{#if this.iqbrimsEnabled }} {{node-navbar/link node=@node useLinkTo=false destination='iqbrims'}} {{/if}} diff --git a/lib/osf-components/addon/components/osf-layout/registries-side-nav/component.ts b/lib/osf-components/addon/components/osf-layout/registries-side-nav/component.ts index 4aeeb128b..abc2898d6 100644 --- a/lib/osf-components/addon/components/osf-layout/registries-side-nav/component.ts +++ b/lib/osf-components/addon/components/osf-layout/registries-side-nav/component.ts @@ -5,7 +5,11 @@ import { and, or } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import Media from 'ember-responsive'; +import DS from 'ember-data'; +import Intl from 'ember-intl/services/intl'; import { layout } from 'ember-osf-web/decorators/component'; +import DraftRegistration from 'ember-osf-web/models/draft-registration'; +import Toast from 'ember-toastr/services/toast'; import styles from './styles'; import template from './template'; @@ -13,7 +17,15 @@ import template from './template'; @tagName('') @layout(template, styles) export default class RegistriesSideNav extends Component { + @service store!: DS.Store; + draftRegistrations: DraftRegistration[] = []; + // registrationSchema: RegistrationSchema[] = []; + @service toast!: Toast; + format?: string = ''; + @service media!: Media; + @service intl!: Intl; + // changeset!: ChangesetDef; // Optional params onLinkClicked?: () => void; @@ -27,8 +39,84 @@ export default class RegistriesSideNav extends Component { @and('isCollapseAllowed', 'shouldCollapse') isCollapsed!: boolean; + @service router!: any; + disableButtons = false; + pageTitle = ''; + jsonData = ''; + + constructor(...args: any[]) { + super(...args); + this.handleRouteChange(); + if (this.router && typeof this.router.on === 'function') { + this.router.on('routeDidChange', this.handleRouteChange.bind(this)); + } + } + @action toggle() { this.toggleProperty('shouldCollapse'); } + + @action + async handleRouteChange() { + const currentUrl = window.location.href; + const urlParts = currentUrl.split('/'); + const lastPartWithQuery = urlParts[urlParts.length - 1]; + const metadataTitle = lastPartWithQuery.split('?')[0]; + const cleanedTitle = metadataTitle.replace(/^\d+-/, ''); + const title = decodeURIComponent(cleanedTitle.replace(/-/g, ' ')); + this.set('pageTitle', title.trim().toLowerCase()); + const allSchemas = await this.store.findAll('registration-schema'); + const matchedSchema = allSchemas.find((schema: any) => schema.schema.pages.some((page: any) => ( + page.title.trim().toLowerCase() === title.trim().toLowerCase() + && page.clipboardCopyPaste === false))); + + if (matchedSchema) { + this.set('disableButtons', true); + } else { + this.set('disableButtons', false); + } + } + + @action + async copyToClipboard() { + const currentUrl = window.location.href; + const idRegex = /\/drafts\/([a-f0-9]{24})/; + const match = currentUrl.match(idRegex); + if (match && match[1]) { + const draftId = match[1]; + + const draftRegistration = await this.store.findRecord('draft-registration', draftId); + + if (draftRegistration) { + const registrationMetadata = await draftRegistration.registrationMetadata; + const metadata: { [key: string]: any } = {}; + const registrationSchema = await draftRegistration.registrationSchema; + for (const page of registrationSchema.schema.pages) { + if (page.title.trim().toLowerCase() !== this.pageTitle && !page.clipboardCopyPaste) { + continue; + } + + for (const question of page.questions) { + Object.keys(registrationMetadata).forEach(key => { + if (key === question.qid && !key.startsWith('grdm-')) { + if (question.format === 'multiselect' && registrationMetadata[key].value === '') { + metadata[key] = []; + } else { + metadata[key] = registrationMetadata[key].value; + } + } + }); + } + } + this.jsonData = JSON.stringify(metadata); + if (this.jsonData === '{}' || Object.keys(JSON.parse(this.jsonData)).length === 0) { + this.toast.warning(this.intl.t('registries.drafts.draft.form.warning_noautosave')); + } else { + await navigator.clipboard.writeText(this.jsonData); + this.toast.success(this.intl.t('registries.drafts.draft.form.clipboard_copied')); + } + } + } + } } diff --git a/lib/osf-components/addon/components/osf-layout/registries-side-nav/label/styles.scss b/lib/osf-components/addon/components/osf-layout/registries-side-nav/label/styles.scss index 7725fff7f..8610e153d 100644 --- a/lib/osf-components/addon/components/osf-layout/registries-side-nav/label/styles.scss +++ b/lib/osf-components/addon/components/osf-layout/registries-side-nav/label/styles.scss @@ -1,10 +1,11 @@ .LabelWrapper { justify-content: space-between; - display: inline-flex; + display: flex; width: 100%; padding-left: 10px; font-weight: 700; - white-space: inherit; + white-space: normal; + word-wrap: break-word; overflow: hidden; &:hover { @@ -16,6 +17,8 @@ float: left; overflow: hidden; text-overflow: ellipsis; + white-space: normal; + word-wrap: break-word; &:hover { overflow: visible; @@ -24,4 +27,7 @@ .Count { float: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } diff --git a/lib/osf-components/addon/components/osf-layout/registries-side-nav/template.hbs b/lib/osf-components/addon/components/osf-layout/registries-side-nav/template.hbs index 3c41565ce..44348815a 100644 --- a/lib/osf-components/addon/components/osf-layout/registries-side-nav/template.hbs +++ b/lib/osf-components/addon/components/osf-layout/registries-side-nav/template.hbs @@ -38,4 +38,23 @@ {{/let}} {{/if}} + + {{!-- model1 :{{@model}} + model2 : {{model}} + {{#each @model as |item index|}} + {{#if (eq index 1)}} +

test >>{{item}}

+ {{/if}} + {{/each}} + test --}} + + {{t 'registries.drafts.draft.form.copy_to_clipboard'}} + + diff --git a/lib/osf-components/addon/components/registries/registration-form-navigation-dropdown/template.hbs b/lib/osf-components/addon/components/registries/registration-form-navigation-dropdown/template.hbs index 2e0df5bd2..576899fe8 100644 --- a/lib/osf-components/addon/components/registries/registration-form-navigation-dropdown/template.hbs +++ b/lib/osf-components/addon/components/registries/registration-form-navigation-dropdown/template.hbs @@ -60,16 +60,18 @@ {{/each}} {{/if}} {{#each this.localizedBlocksWithAnchor as |block|}} -
  • - - {{block.localizedDisplayText}} - -
  • + {{#unless block.model.concealmentPageNavigator}} +
  • + + {{block.localizedDisplayText}} + +
  • + {{/unless}} {{/each}} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs index e9f22edca..0a53fb0d4 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs @@ -1,13 +1,15 @@ -

    - {{t 'osf-components.registries.schema-block-renderer/editable/files.instructions' - projectOrComponent=(if @node.isRoot 'project' 'component') - nodeUrl=this.nodeUrl - htmlSafe=true - }} -

    +{{#if (not @schemaBlock.sentence)}} +

    + {{t 'osf-components.registries.schema-block-renderer/editable/files.instructions' + projectOrComponent=(if @node.isRoot 'project' 'component') + nodeUrl=this.nodeUrl + htmlSafe=true + }} +

    +{{/if}} void; + + @alias('schemaBlock.schema.id') + schemaId!: any; + + folderExpands: {[key: string]: boolean} = {}; + + item: File[] = []; + @service store!: DS.Store; + currentFolder!: File; + + fileProvider!: FileProvider; + @requiredAction openFile!: (file: File, show: string) => void; + + @task + getCurrentFolderItems = task(function *(this: AdMetadataInput, targetFolder: File) { + this.set('currentFolder', targetFolder); + const folderItems = yield this.currentFolder.files; + const itemsArray = folderItems.toArray(); + this.set('item', this.item.concat(itemsArray)); + + const paths = this.get('projectFilePaths'); + const folderExpands = this.get('folderExpands'); + if (!Object.values(folderExpands).length) { + paths.forEach(path => { + if (path.endsWith('/') && path.split('/').length === 2) { + folderExpands[path] = true; + } + }); + this.set('folderExpands', folderExpands); + } + + for (const item of itemsArray) { + if (item.materializedPath.match(/.+\/$/)) { + yield this.getCurrentFolderItems.perform(item); + } + } + }); + + async didInsertElement() { + super.didInsertElement(); + const fileProviders = await this.node.files; + const fileProvider = fileProviders.findBy('name', 'osfstorage') as FileProvider; + const rootFolder = await fileProvider.rootFolder; + this.getCurrentFolderItems.perform(rootFolder); + } + + @computed('node') + get nodeUrl() { + return this.node && pathJoin(baseURL, this.node.id); + } + + @computed('changeset', 'valuePath') + get adMetadatas(): FileMetadata[] { + const value = this.changeset.get(this.valuePath); + if (!value) { + return []; + } + const metadatas: FileMetadata[] = JSON.parse(value); + metadatas.sort((a, b) => a.path.localeCompare(b.path)); + return metadatas; + } + + @computed('item') + get projectFileMetadata(): FileMetadata[] { + const res: FileMetadata[] = []; + this.get('item').forEach(item => { + res.push({ + path: item.provider + item.materializedPath, + urlpath: '', + metadata: {}, + }); + }); + return res; + } + + @computed('projectFileMetadata') + get projectFilePaths(): string[] { + const projectFileMetadatas = this.get('projectFileMetadata'); + const pathSet = new Set(); + projectFileMetadatas.forEach(projectFileMetadata => { + let path = ''; + const parts = projectFileMetadata.path.split('/'); + parts.forEach((part, i) => { + if (!part.length) { + return; + } + path += part; + if (i + 1 < parts.length) { + path += '/'; + } + pathSet.add(path); + }); + }); + return Array.from(pathSet).sort((a, b) => a.localeCompare(b)); + } + + @computed('adMetadatas', 'projectFileMetadata', 'projectFilePaths', 'folderExpands') + get fileEntries(): FileEntry[] { + const metadataMap: {[key: string]: FileMetadata} = {}; + this.get('adMetadatas').forEach(metadata => { + metadataMap[metadata.path] = metadata; + }); + const projectFileMetadataMap: {[key: string]: FileMetadata} = {}; + + this.get('projectFileMetadata').forEach(projectFileMetadata => { + projectFileMetadataMap[projectFileMetadata.path] = projectFileMetadata; + }); + + const paths = this.get('projectFilePaths'); + const folderExpands = this.get('folderExpands'); + const res = paths.map(path => { + const metadata = metadataMap[path] || projectFileMetadataMap[path]; + const parts = path.split('/'); + if (!parts[parts.length - 1].length) { + parts.pop(); + } + const folder = path.match(/.+\/$/) !== null; + // 18 + return { + path, + parts, + lastPart: parts[parts.length - 1], + lastPartDepth: parts.length, + folder, + title: metadata ? this.extractTitleFromMetadata(metadata) : null, + manager: metadata ? this.extractManagerFromMetadata(metadata) : null, + url: metadata ? this.extractUrlFromMetadata(metadata) : null, + fileUrl: metadata && metadata.urlpath ? `${pathJoin(baseURL, metadata.urlpath)}#edit-metadata` : null, + metadata, + added: metadataMap[path] != null, + hasProject: projectFileMetadataMap[path] != null, + style: `margin: 0 0 0 ${parts.length * 16 + (folder ? 0 : 18)}px`, + visible: [...parts.slice(0, parts.length - 1).keys()] + .every(i => folderExpands[`${parts.slice(0, i + 1).join('/')}/`]), + folderExpanded: folderExpands[path], + } as FileEntry; + }); + return res; + } + + didReceiveAttrs() { + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a changeset to render', + Boolean(this.changeset), + ); + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a node to render', + Boolean(this.node), + ); + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a valuePath to render', + Boolean(this.valuePath), + ); + } + + saveFileMetadatas(metadatas: FileMetadata[]) { + metadatas.sort((a, b) => a.path.localeCompare(b.path)); + this.changeset.set( + this.valuePath, + metadatas.length ? JSON.stringify(metadatas) : null, + ); + this.onInput(); + this.notifyPropertyChange('adMetadatas'); + } + + @action + addFileMetadata(this: AdMetadataInput, entry: FileEntry) { + const metadatas = this.get('adMetadatas'); + if (entry.metadata) { + metadatas.push(entry.metadata); + } + this.saveFileMetadatas(metadatas); + } + + @action + removeFileMetadata(this: AdMetadataInput, entry: FileEntry) { + const metadatas = this.get('adMetadatas'); + const metadata = metadatas.find(m => m.path === entry.path); + if (metadata) { + metadatas.splice(metadatas.indexOf(metadata), 1); + } + this.saveFileMetadatas(metadatas); + } + + extractTitleFromMetadata(metadata: FileMetadata): string | null { + const titleJa = metadata.metadata['grdm-file:title-ja']; + const titleEn = metadata.metadata['grdm-file:title-en']; + if (!titleJa && !titleEn) { + return null; + } + if (titleJa && !titleEn) { + return `${titleJa.value}`; + } + if (!titleJa && titleEn) { + return `${titleEn.value}`; + } + if (!titleJa.value && !titleEn.value) { + return null; + } + if (this.intl.locale.includes('ja')) { + return `${titleJa.value}`; + } + return `${titleEn.value}`; + } + + extractManagerFromMetadata(metadata: FileMetadata): string | null { + const managerJa = metadata.metadata['grdm-file:data-man-name-ja']; + const managerEn = metadata.metadata['grdm-file:data-man-name-en']; + if (!managerJa && !managerEn) { + return null; + } + let value; + if (managerJa && !managerEn) { + value = managerJa.value; + } else if (!managerJa && managerEn) { + value = managerEn.value; + } else if (this.intl.locale.includes('ja')) { + value = managerJa.value; + } else { + value = managerEn.value; + } + if (value && typeof value === 'object') { + return ( + this.intl.locale.includes('ja') + ? [value.last, value.middle, value.first] + : [value.first, value.middle, value.last] + ).filter(v => v).join(' '); + } + return value; + } + + extractUrlFromMetadata(metadata: FileMetadata): string | null { + const url = metadata.metadata['grdm-file:repo-url-doi-link']; + if (!url) { + return null; + } + return `${url.value}`; + } + + @action + expandFolder(this: AdMetadataInput, entry: FileEntry, expand: boolean) { + const folderExpands = this.get('folderExpands'); + folderExpands[entry.path] = expand; + this.set('folderExpands', folderExpands); + this.notifyPropertyChange('folderExpands'); + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/styles.scss new file mode 100644 index 000000000..f29e4bdb2 --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/styles.scss @@ -0,0 +1,49 @@ +.file-metadata-input-container { + width: 100%; + border: 1px solid #eee; + padding: 0.5em; +} + +.file-metadata-input-files { + border: 1px solid #eee; +} + +.file-metadata-input-files th { + border: 1px solid #eee; + background: #f5f5f5; + height: 35px; +} + +.file-metadata-input-files td { + border-top: 1px solid #eee; + height: 35px; +} + +.file-metadata-input-files .file-metadata-path { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-files .file-metadata-title { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-files .file-metadata-manager { + max-width: 50px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-edit-button { + padding: 0 !important; +} + +.file-metadata-input-buttons { + margin-bottom: 4px; +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/template.hbs new file mode 100644 index 000000000..0551275fe --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/template.hbs @@ -0,0 +1,75 @@ + + {{#if this.fileEntries}} + + + + + + + + + {{#each this.fileEntries as |fileEntry|}} + {{#if fileEntry.visible}} + + + + + {{/if}} + {{/each}} + +
    + + {{t 'metadata.file-metadata-input.columns.path'}} +
    + {{#if fileEntry.added}} + + {{else if fileEntry.hasProject}} + + {{/if}} + +

    + {{#if fileEntry.folder}} + {{#if fileEntry.folderExpanded}} + + {{else}} + + {{/if}} + + {{else}} + + {{/if}} + {{fileEntry.lastPart}} +

    +
    + {{else}} + + {{/if}} +
    \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/component.ts index 4b2a45a66..e151ca58b 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/component.ts +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/component.ts @@ -4,13 +4,17 @@ import Component from '@ember/component'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; + import Changeset from 'ember-changeset'; import lookupValidator from 'ember-changeset-validations'; import { ChangesetDef } from 'ember-changeset/types'; +import Intl from 'ember-intl/services/intl'; import { layout } from 'ember-osf-web/decorators/component'; import NodeModel from 'ember-osf-web/models/node'; -import { buildValidation, SchemaBlockGroup } from 'ember-osf-web/packages/registration-schema'; +import { buildValidation, SchemaBlock, SchemaBlockGroup } from 'ember-osf-web/packages/registration-schema'; import DraftRegistrationManager from 'registries/drafts/draft/draft-registration-manager'; + import styles from './styles'; import template from './template'; @@ -21,17 +25,23 @@ export default class ArrayInput extends Component { changeset!: ChangesetDef; metadataChangeset!: ChangesetDef; draftManager!: DraftRegistrationManager; + @service intl!: Intl; @alias('schemaBlock.registrationResponseKey') valuePath!: string; onInput!: () => void; onMetadataInput!: () => void; schemaBlockGroup!: SchemaBlockGroup; + schemaBlock!: SchemaBlock; node!: NodeModel; subChangesets: ChangesetDef[] = []; didReceiveAttrs() { + const rowAdditionCaption = this.schemaBlock.rowAdditionCaption || ''; + + this.schemaBlock.rowAdditionCaption = this.getLocalizedText(rowAdditionCaption); + const raw = this.changeset.get(this.valuePath); if (raw) { const prefix = `${this.valuePath}|`; @@ -98,4 +108,15 @@ export default class ArrayInput extends Component { this.set('subChangesets', newSubChangesets); this.save(newSubChangesets); } + + getLocalizedText(text: string) { + if (!text.includes('|')) { + return text; + } + const texts = text.split('|'); + if (this.intl.locale.includes('ja')) { + return texts[0]; + } + return texts[1]; + } } diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/template.hbs index 3bf4592c3..6c41fd193 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/array-input/template.hbs @@ -23,7 +23,7 @@ - {{t 'metadata.array-input.remove-item'}} + {{this.schemaBlock.rowAdditionCaption}}{{t 'metadata.array-input.remove-item'}} {{/each}} @@ -32,7 +32,7 @@ @type='primary' @onClick={{action this.onAdd}} > - {{t 'metadata.array-input.add-item'}} + {{this.schemaBlock.rowAdditionCaption}}{{t 'metadata.array-input.add-item'}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-number-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-number-input/component.ts index 8756ce0e8..440b07045 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-number-input/component.ts +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-number-input/component.ts @@ -54,6 +54,7 @@ export default class ERadAwardNumberInput extends Component { if (eradRecord) { kadaiId = eradRecord.kadai_id; this.updateCode('e-rad-award-funder-input', eradRecord.haibunkikan_cd); + this.updateCode('single-select-pulldown-input', eradRecord.haibunkikan_cd); this.updateCode('e-rad-award-field-input', eradRecord.bunya_cd); this.changeset.set( this.draftManager.getResponseKeyByBlockType('e-rad-award-title-ja-input'), diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-en-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-en-input/template.hbs index 23e459a95..25af56ad4 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-en-input/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-en-input/template.hbs @@ -10,7 +10,6 @@ local-class='schema-block-input' @uniqueID={{@uniqueID}} @valuePath={{@schemaBlock.registrationResponseKey}} - @onChange={{action this.onInput2}} @onKeyUp={{action this.onInput2}} @placeholder=' ' /> diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-ja-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-ja-input/template.hbs index 55c15c85c..283351ef3 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-ja-input/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/e-rad-award-title-ja-input/template.hbs @@ -10,7 +10,6 @@ local-class='schema-block-input' @uniqueID={{@uniqueID}} @valuePath={{@schemaBlock.registrationResponseKey}} - @onChange={{action this.onInput2}} @onKeyUp={{action this.onInput2}} @placeholder=' ' /> diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/japan-grant-number-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/japan-grant-number-input/component.ts index eb931644c..5bc8f34db 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/japan-grant-number-input/component.ts +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/japan-grant-number-input/component.ts @@ -54,7 +54,7 @@ export default class JapanGrantNumberInput extends Component { if (eradRecord) { kadaiId = eradRecord.japan_grant_number; this.updateCode('e-rad-award-funder-input', eradRecord.haibunkikan_cd); - this.updateCode('pulldown-input', eradRecord.haibunkikan_cd); + this.updateCode('single-select-pulldown-input', eradRecord.haibunkikan_cd); this.updateCode('e-rad-award-field-input', eradRecord.bunya_cd); if (eradRecord.funding_stream_code) { const key = this.getResponseKeyByBlockType('funding-stream-code-input'); diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.ts new file mode 100644 index 000000000..6562416a5 --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.ts @@ -0,0 +1,85 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { assert } from '@ember/debug'; +import { action, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; + +import { ChangesetDef } from 'ember-changeset/types'; +import Intl from 'ember-intl/services/intl'; + +import { layout } from 'ember-osf-web/decorators/component'; +import { SchemaBlock } from 'ember-osf-web/packages/registration-schema'; + +import template from './template'; + +@layout(template) +@tagName('') +export default class SingleSelectPulldownInput extends Component { + @service intl!: Intl; + // Required param + optionBlocks!: SchemaBlock[]; + changeset!: ChangesetDef; + + @alias('schemaBlock.registrationResponseKey') + valuePath!: string; + onInput!: () => void; + onMetadataInput!: () => void; + + anotherOption?: string; + + didReceiveAttrs() { + assert( + 'SchemaBlockRenderer::Editable::SingleSelectPulldownInput requires optionBlocks to render', + Boolean(this.optionBlocks), + ); + } + + @computed('optionBlocks.[]', 'anotherOption') + get optionBlockValues() { + const options = this.optionBlocks + .map(item => this.getLocalizedItemText(item)); + if (this.anotherOption) { + options.push(this.anotherOption); + } + return options; + } + + @action + onChange(option: string) { + const code = (option || '').trim(); + const item = this.optionBlocks.find(b => code === b.displayText); + const result = item ? item.displayText : option; + this.changeset.set(this.valuePath, result); + this.onMetadataInput(); + this.onInput(); + this.set('anotherOption', null); + } + + @action + onInputSearch(text: string) { + if (!this.optionBlocks.find(item => item.displayText === text || this.getLocalizedItemText(item) === text)) { + this.set('anotherOption', text); + } + return true; + } + + getLocalizedItemText(item: SchemaBlock) { + const text = item.helpText || item.displayText; + if (text === undefined) { + return item.displayText; + } + return `${item.displayText}`; + } + + getLocalizedText(text: string) { + if (!text.includes('|')) { + return text; + } + const texts = text.split('|'); + if (this.intl.locale.includes('ja')) { + return texts[0]; + } + return texts[1]; + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/styles.scss new file mode 100644 index 000000000..462216f6a --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/styles.scss @@ -0,0 +1,9 @@ +.schema-block-input { + :global(input) { + height: 40px; + background-color: $color-bg-gray-blue-light; + color: $color-text-gray-blue; + border-radius: 2px; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/template.hbs new file mode 100644 index 000000000..4fef23b8f --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/template.hbs @@ -0,0 +1,26 @@ + + + + {{option}} + + + + \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.ts new file mode 100644 index 000000000..6562416a5 --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.ts @@ -0,0 +1,85 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { assert } from '@ember/debug'; +import { action, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; + +import { ChangesetDef } from 'ember-changeset/types'; +import Intl from 'ember-intl/services/intl'; + +import { layout } from 'ember-osf-web/decorators/component'; +import { SchemaBlock } from 'ember-osf-web/packages/registration-schema'; + +import template from './template'; + +@layout(template) +@tagName('') +export default class SingleSelectPulldownInput extends Component { + @service intl!: Intl; + // Required param + optionBlocks!: SchemaBlock[]; + changeset!: ChangesetDef; + + @alias('schemaBlock.registrationResponseKey') + valuePath!: string; + onInput!: () => void; + onMetadataInput!: () => void; + + anotherOption?: string; + + didReceiveAttrs() { + assert( + 'SchemaBlockRenderer::Editable::SingleSelectPulldownInput requires optionBlocks to render', + Boolean(this.optionBlocks), + ); + } + + @computed('optionBlocks.[]', 'anotherOption') + get optionBlockValues() { + const options = this.optionBlocks + .map(item => this.getLocalizedItemText(item)); + if (this.anotherOption) { + options.push(this.anotherOption); + } + return options; + } + + @action + onChange(option: string) { + const code = (option || '').trim(); + const item = this.optionBlocks.find(b => code === b.displayText); + const result = item ? item.displayText : option; + this.changeset.set(this.valuePath, result); + this.onMetadataInput(); + this.onInput(); + this.set('anotherOption', null); + } + + @action + onInputSearch(text: string) { + if (!this.optionBlocks.find(item => item.displayText === text || this.getLocalizedItemText(item) === text)) { + this.set('anotherOption', text); + } + return true; + } + + getLocalizedItemText(item: SchemaBlock) { + const text = item.helpText || item.displayText; + if (text === undefined) { + return item.displayText; + } + return `${item.displayText}`; + } + + getLocalizedText(text: string) { + if (!text.includes('|')) { + return text; + } + const texts = text.split('|'); + if (this.intl.locale.includes('ja')) { + return texts[0]; + } + return texts[1]; + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/styles.scss new file mode 100644 index 000000000..462216f6a --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/styles.scss @@ -0,0 +1,9 @@ +.schema-block-input { + :global(input) { + height: 40px; + background-color: $color-bg-gray-blue-light; + color: $color-text-gray-blue; + border-radius: 2px; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/template.hbs new file mode 100644 index 000000000..4fef23b8f --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/template.hbs @@ -0,0 +1,26 @@ + + + + {{option}} + + + + \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/text/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/text/template.hbs index 9322a3674..1718d0582 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/text/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/text/template.hbs @@ -11,9 +11,14 @@ @uniqueID={{@uniqueID}} @valuePath={{@schemaBlock.registrationResponseKey}} @onKeyUp={{@onInput}} - @autoTitle={{@schemaBlock.autoTitle}} - @autoDate={{@schemaBlock.autoDate}} + @retrievalTitle={{@schemaBlock.retrievalTitle}} + @retrievalDate={{@schemaBlock.retrievalDate}} + @retrievalVersion={{@schemaBlock.retrievalVersion}} + @readonly={{@schemaBlock.readonly}} @title={{@draftManager.node.title}} + @nodeId={{@node.id}} + @datetimeInitiated={{@draftManager.draftRegistration.datetimeInitiated}} + @datetimeUpdated={{@draftManager.draftRegistration.datetimeUpdated}} @placeholder=' ' /> diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts index 42b7e43f0..6ae178992 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/component.ts @@ -49,6 +49,25 @@ export default class LabelContent extends Component { return this.getLocalizedText(text); } + @computed('localizedHelpText') + get localizedHelpTextLines() { + const text = this.localizedHelpText; + if (!text) { + return []; + } + + return text.split('\n').map(line => { + const urlRegex = /(https?:\/\/[^\s]+)/g; + + const parts = line.split(urlRegex).map(part => ({ + content: part, + isLink: part.startsWith('https://'), + })); + + return parts; + }); + } + getLocalizedText(text: string) { if (!text.includes('|')) { return text; diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss index 873f7ddf6..30c9d8563 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss @@ -1,7 +1,6 @@ .DisplayText { white-space: pre-wrap; display: inline-block; - margin: 0; } .Required { @@ -13,8 +12,8 @@ .HelpText { composes: Element from '../../styles'; - white-space: pre-wrap; font-weight: 400; + margin-top: 0; } .ExampleButton { @@ -37,3 +36,9 @@ white-space: pre-wrap; font-weight: 400; } + + +.HelpTextLine { + font-weight: normal !important; + margin-bottom: 0; +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs index 3901cadcb..0af64917a 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs @@ -8,9 +8,26 @@ {{~/if~}}

    {{#if @isEditableForm}} -

    - {{~this.localizedHelpText~}} -

    +
    + {{#each this.localizedHelpTextLines as |lineParts|}} +

    + {{#each lineParts as |part|}} + {{#if part.isLink}} + + {{part.content}} + + {{else}} + {{part.content}} + {{/if}} + {{/each}} +

    + {{/each}} +
    + {{#if @schemaBlock.exampleText}} { + const parts = line.split(urlPattern).map(part => ( + urlPattern.test(part) + ? { type: 'link', content: part.trim() } + : { type: 'text', content: part.trim() } + )); + return parts; + }); + + return lines; } getLocalizedText(text: string) { diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/styles.scss index cff4b6753..907b577b0 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/styles.scss +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/styles.scss @@ -5,5 +5,5 @@ } .PageHeading_helper { - margin-top: 10px; + margin-bottom: 0; } diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/template.hbs index 4e43c5f7f..814ec79eb 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/page-heading/template.hbs @@ -1,4 +1,4 @@ -{{#unless @schemaBlock.hideProjectmetadata}} +{{#if (or (not @schemaBlock.concealmentPageNavigator) (eq @schemaBlock.concealmentPageNavigator undefined)) }}

    {{this.localizedDisplayText}}

    -{{/unless}} +{{/if}} {{#if this.isEditableForm}} -

    {{this.localizedHelpText}}

    +
    + {{#each this.localizedHelpText as |line|}} +

    + {{#each line as |part|}} + {{#if (eq part.type 'link')}} + + {{part.content}} + + {{else}} + {{part.content}} + {{/if}} + {{/each}} +

    + {{/each}} +
    {{/if}} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/mapper/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/mapper/template.hbs index 9e9e2e9cd..3c47bbd32 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/mapper/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/mapper/template.hbs @@ -129,6 +129,13 @@ changeset=@changeset node=@node ) + ad-metadata-input=( + component + 'registries/schema-block-renderer/read-only/rdm/ad-metadata-input' + registrationResponses=@registrationResponses + changeset=@changeset + node=@node + ) array-input=( component 'registries/schema-block-renderer/read-only/rdm/array-input' diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.ts new file mode 100644 index 000000000..8e3980ab5 --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.ts @@ -0,0 +1,79 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { assert } from '@ember/debug'; + +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { ChangesetDef } from 'ember-changeset/types'; +import Intl from 'ember-intl/services/intl'; +import { layout } from 'ember-osf-web/decorators/component'; +import NodeModel from 'ember-osf-web/models/node'; +import styles from './styles'; +import template from './template'; + +interface FileMetadataEntity { + comments?: any[]; + extra?: any[]; + value: any; +} + +interface FileMetadata { + path: string; + urlpath: string | null; + metadata: { + [key: string]: FileMetadataEntity, + }; +} + +interface FileEntry { + path: string; +} + +@layout(template, styles) +@tagName('') +export default class AdMetadataInput extends Component { + @service intl!: Intl; + + // Required param + changeset!: ChangesetDef; + node!: NodeModel; + + @alias('schemaBlock.registrationResponseKey') + valuePath!: string; + onInput!: () => void; + + didReceiveAttrs() { + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a changeset to render', + Boolean(this.changeset), + ); + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a node to render', + Boolean(this.node), + ); + assert( + 'Registries::SchemaBlockRenderer::Editable::Rdm::AdMetadataInput requires a valuePath to render', + Boolean(this.valuePath), + ); + } + + @computed('changeset', 'valuePath') + get adMetadatas(): FileMetadata[] { + if (!this.changeset) { + return []; + } + const value = this.changeset.get(this.valuePath); + if (!value) { + return []; + } + return JSON.parse(value) as FileMetadata[]; + } + + @computed('adMetadatas') + get fileEntries(): FileEntry[] { + return this.get('adMetadatas').map(metadata => ({ + path: metadata.path, + }) as FileEntry); + } +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/styles.scss new file mode 100644 index 000000000..f29e4bdb2 --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/styles.scss @@ -0,0 +1,49 @@ +.file-metadata-input-container { + width: 100%; + border: 1px solid #eee; + padding: 0.5em; +} + +.file-metadata-input-files { + border: 1px solid #eee; +} + +.file-metadata-input-files th { + border: 1px solid #eee; + background: #f5f5f5; + height: 35px; +} + +.file-metadata-input-files td { + border-top: 1px solid #eee; + height: 35px; +} + +.file-metadata-input-files .file-metadata-path { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-files .file-metadata-title { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-files .file-metadata-manager { + max-width: 50px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-metadata-input-edit-button { + padding: 0 !important; +} + +.file-metadata-input-buttons { + margin-bottom: 4px; +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/template.hbs new file mode 100644 index 000000000..0bdf64fdc --- /dev/null +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/template.hbs @@ -0,0 +1,24 @@ + + {{#if this.fileEntries }} + + + + + + + + {{#each this.fileEntries as |fileEntry|}} + + + + {{/each}} + +
    + {{t 'metadata.file-metadata-input.columns.path'}} +
    + {{fileEntry.path}} +
    + {{else}} + + {{/if}} +
    \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/styles.scss index 24b104d74..fa7090685 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/styles.scss +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/styles.scss @@ -3,3 +3,19 @@ color: $color-text-gray-blue; } + +.HelpText { + composes: Element from '../styles'; + + // white-space: pre-wrap; + font-weight: 400; + margin-top: 0; +} + + +.HelpTextLine { + font-weight: normal !important; + margin-bottom: 0; + font-size: 14px; + margin-top: 17px; +} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/template.hbs index 94bc882bb..58636f61b 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/section-heading/template.hbs @@ -4,6 +4,9 @@ ...attributes > {{this.localizedDisplayText}} +
    +

    {{this.schemaBlock.helpText}}

    +
    {{#if this.isEditableForm}} diff --git a/lib/osf-components/addon/components/validated-input/text/component.ts b/lib/osf-components/addon/components/validated-input/text/component.ts index 13cf6c92f..0f0338ec0 100644 --- a/lib/osf-components/addon/components/validated-input/text/component.ts +++ b/lib/osf-components/addon/components/validated-input/text/component.ts @@ -1,9 +1,8 @@ +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; import DS, { AttributesFor } from 'ember-data'; - import { layout } from 'ember-osf-web/decorators/component'; import defaultTo from 'ember-osf-web/utils/default-to'; - -import { action } from '@ember/object'; import BaseValidatedComponent from '../base-component'; import template from './template'; @@ -11,12 +10,54 @@ import template from './template'; export default class ValidatedText extends BaseValidatedComponent { valuePath!: AttributesFor; + @service store!: DS.Store; + // Additional arguments password: boolean = defaultTo(this.password, false); onKeyUp?: () => void; // Action - onChange?: () => void; // Action title?: string = this.title; + retrievalTitle: string = defaultTo(this.retrievalTitle, ''); + retrievalDate: string = defaultTo(this.retrievalDate, ''); + retrievalVersion: string = defaultTo(this.retrievalVersion, ''); + + datetimeInitiated: Date = defaultTo(this.datetimeInitiated, new Date()); + datetimeUpdated: Date = defaultTo(this.datetimeUpdated, new Date()); + + readonly: boolean = defaultTo(this.readonly, false); + + didInsertElement() { + if (this.datetimeUpdated !== undefined || this.datetimeInitiated !== undefined) { + const diffInMs = this.datetimeUpdated.getTime() - this.datetimeInitiated.getTime(); + const diffInMinutes = diffInMs / 1000; + if ( + (this.retrievalTitle === 'auto_retrieval' || this.retrievalTitle === 'dual_retrieval') + && diffInMinutes <= 1 + ) { + this.set('value', this.title); + } + + if ( + (this.retrievalDate === 'auto_retrieval' || this.retrievalDate === 'dual_retrieval') + && diffInMinutes <= 1 + ) { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const date = String(now.getDate()).padStart(2, '0'); + + this.set('value', `${year}-${month}-${date}`); + } + } else { + // Invalid date values for datetimeInitiated or datetimeUpdated. + } + + this.set('readonly', this.readonly === true); + + if (this.retrievalVersion !== '') { + this.set('value', this.retrievalVersion); + } + } @action getTitle() { @@ -25,10 +66,17 @@ export default class ValidatedText extends BaseValidatedComp @action getDate() { - this.set( - 'value', - `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new - Date().getDate()).padStart(2, '0')}`, - ); + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const date = String(now.getDate()).padStart(2, '0'); + + this.set('value', `${year}-${month}-${date}`); + } + + @action + onChange(event: Event) { + const target = event.target as HTMLInputElement; + this.set('value', target.value); } } diff --git a/lib/osf-components/addon/components/validated-input/text/template.hbs b/lib/osf-components/addon/components/validated-input/text/template.hbs index a5f3c442c..d150262ac 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -2,6 +2,9 @@ model=this.model changeset=this.changeset title=this.title + datetimeInitiated=this.datetimeInitiated + datetimeUpdated=this.datetimeUpdated + nodeId=this.nodeId errors=this.errors label=this.label valuePath=this.valuePath @@ -17,25 +20,25 @@ @class='form-control' @name={{@valuePath}} @keyUp={{@onKeyUp}} - @change={{@onChange}} - @disabled={{this.disabled}} + @change={{action this.onChange}} + @disabled={{this.getEdit}} /> - {{#if @autoTitle}} + {{#if (or (eq @retrievalTitle 'button_retrieval') (eq @retrievalTitle 'dual_retrieval'))}} - {{t 'registries.drafts.draft.form.auto_button_label'}} + {{t 'registries.drafts.draft.form.get_retrieval_label'}} {{/if}} - {{#if @autoDate}} + {{#if (or (eq @retrievalDate 'button_retrieval') (eq @retrievalDate 'dual_retrieval'))}} - {{t 'registries.drafts.draft.form.auto_button_label'}} + {{t 'registries.drafts.draft.form.get_retrieval_label'}} {{/if}} {{/validated-input/x-input-wrapper}} diff --git a/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/component.js b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/component.js new file mode 100644 index 000000000..804d31e86 --- /dev/null +++ b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/component.js @@ -0,0 +1,2 @@ +export { default } from + 'osf-components/components/registries/schema-block-renderer/editable/rdm/ad-metadata-input/component'; diff --git a/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.js b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.js new file mode 100644 index 000000000..91d59780a --- /dev/null +++ b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component.js @@ -0,0 +1,2 @@ +export { default } from + 'osf-components/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component'; diff --git a/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.js b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.js new file mode 100644 index 000000000..354b25160 --- /dev/null +++ b/lib/osf-components/app/components/registries/schema-block-renderer/editable/rdm/singleselect-pulldown-input/component.js @@ -0,0 +1,2 @@ +export { default } from + 'osf-components/components/registries/schema-block-renderer/editable/rdm/single-select-pulldown-input/component'; diff --git a/lib/osf-components/app/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.js b/lib/osf-components/app/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.js new file mode 100644 index 000000000..4bd11e191 --- /dev/null +++ b/lib/osf-components/app/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component.js @@ -0,0 +1,2 @@ +export { default } from + 'osf-components/components/registries/schema-block-renderer/read-only/rdm/ad-metadata-input/component'; diff --git a/lib/registries/addon/drafts/draft/-components/right-nav/component.ts b/lib/registries/addon/drafts/draft/-components/right-nav/component.ts new file mode 100644 index 000000000..6c2693bce --- /dev/null +++ b/lib/registries/addon/drafts/draft/-components/right-nav/component.ts @@ -0,0 +1,97 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import DS from 'ember-data'; +import Intl from 'ember-intl/services/intl'; +import { layout } from 'ember-osf-web/decorators/component'; +import DraftRegistration from 'ember-osf-web/models/draft-registration'; +import Toast from 'ember-toastr/services/toast'; +import template from './template'; + +@tagName('') +@layout(template) +export default class RightNav extends Component { + @service store!: DS.Store; + draftRegistrations: DraftRegistration[] = []; + @service toast!: Toast; + + @service router!: any; + disableButtons: boolean = false; + @service intl!: Intl; + + constructor(...args: any[]) { + super(...args); + this.handleRouteChange(); + this.router.on('routeDidChange', this.handleRouteChange); + } + + willDestroy() { + super.willDestroy(); + this.router.off('routeDidChange', this.handleRouteChange); + } + + @action + async handleRouteChange() { + const currentUrl = window.location.href; + const urlParts = currentUrl.split('/'); + const lastPartWithQuery = urlParts[urlParts.length - 1]; + const metadataTitle = lastPartWithQuery.split('?')[0]; + const cleanedTitle = metadataTitle.replace(/^\d+-/, ''); + const title = decodeURIComponent(cleanedTitle.replace(/-/g, ' ')); + const allSchemas = await this.store.findAll('registration-schema'); + + const matchedSchema = allSchemas.find( + (schema: any) => schema.schema.pages.some( + (page: any) => page.title.trim().toLowerCase() === title.trim().toLowerCase() + && page.clipboardCopyPaste === false, + ), + ); + + if (!this.isDestroyed && !this.isDestroying) { + if (matchedSchema) { + this.set('disableButtons', true); + } else { + this.set('disableButtons', false); + } + } + } + + @action + async pasteFromClipboard() { + try { + const clipboardText = await navigator.clipboard.readText(); + try { + const parsedJson = JSON.parse(clipboardText); + const structuredJson: {[key: string]: any } = {}; + Object.entries(parsedJson).forEach(([key, value]) => { + structuredJson[key] = { + extra: [], + value, + comments: [], + }; + }); + + const currentUrl = window.location.href; + const idRegex = /\/drafts\/([a-f0-9]{24})/; + const match = currentUrl.match(idRegex); + if (match && match[1]) { + try { + const draftId = match[1]; + const draftRegistration = await this.store.findRecord('draft-registration', draftId); + draftRegistration.set('registrationMetadata', structuredJson); + await draftRegistration.save(); + window.location.reload(); + this.toast.success(this.intl.t('registries.drafts.draft.form.clipboard_pasted')); + } catch (error) { + this.toast.success(this.intl.t('registries.drafts.draft.form.json_invalid'), error); + } + } + } catch (error) { + this.toast.success(this.intl.t('registries.drafts.draft.form.clipboard_unread'), error); + } + } catch (error) { + this.toast.error('Failed to read from clipboard: ', error); + } + } +} diff --git a/lib/registries/addon/drafts/draft/-components/right-nav/styles.scss b/lib/registries/addon/drafts/draft/-components/right-nav/styles.scss index e69de29bb..a9a447dbc 100644 --- a/lib/registries/addon/drafts/draft/-components/right-nav/styles.scss +++ b/lib/registries/addon/drafts/draft/-components/right-nav/styles.scss @@ -0,0 +1,11 @@ +.Label { + float: left; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + word-wrap: break-word; + + &:hover { + overflow: visible; + } +} diff --git a/lib/registries/addon/drafts/draft/-components/right-nav/template.hbs b/lib/registries/addon/drafts/draft/-components/right-nav/template.hbs index 0fc70271c..4a126874e 100644 --- a/lib/registries/addon/drafts/draft/-components/right-nav/template.hbs +++ b/lib/registries/addon/drafts/draft/-components/right-nav/template.hbs @@ -60,4 +60,15 @@ - \ No newline at end of file + + + + {{t 'registries.drafts.draft.form.from_clipboard_paste'}} + \ No newline at end of file diff --git a/mirage/config.ts b/mirage/config.ts index 562608270..ef1fee050 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -22,6 +22,7 @@ import { summaryMetrics } from './views/institution'; import { iqbrimsStatus } from './views/iqbrims-status'; import { metadataNodeErad } from './views/metadata-node-erad'; import { metadataNodeProject } from './views/metadata-node-project'; +// import { workflowConfig } from './views/workflow-config'; import { createNode } from './views/node'; import { osfNestedResource, osfResource, osfToManyRelationship } from './views/osf-resource'; import { getProviderSubjects } from './views/provider-subjects'; @@ -263,6 +264,7 @@ export default function(this: Server) { this.del('/project/:pid/binderhub/server_annotation/:aid', serverAnnotation.del); this.get('/project/:id/metadata/erad/candidates', metadataNodeErad); this.get('/project/:id/metadata/project', metadataNodeProject); + // this.get('/project/:id/workflow/config', workflowConfig); this.urlPrefix = apiUrl; this.namespace = apiNamespace; diff --git a/mirage/factories/workflow-config.ts b/mirage/factories/workflow-config.ts new file mode 100644 index 000000000..40a8fba22 --- /dev/null +++ b/mirage/factories/workflow-config.ts @@ -0,0 +1,12 @@ +import { Factory } from 'ember-cli-mirage'; + +import WorkFlowConfigModel from 'ember-osf-web/models/iqbrims-status'; + +export default Factory.extend({ +}); + +declare module 'ember-cli-mirage/types/registries/schema' { + export default interface MirageSchemaRegistry { + workflowConfigs: WorkFlowConfigModel; + } // eslint-disable-line semi +} diff --git a/mirage/fixture-data/registration-schemas/prereg-challenge.ts b/mirage/fixture-data/registration-schemas/prereg-challenge.ts index d42e7e2b6..a77ba2026 100644 --- a/mirage/fixture-data/registration-schemas/prereg-challenge.ts +++ b/mirage/fixture-data/registration-schemas/prereg-challenge.ts @@ -428,6 +428,7 @@ export default { { type: 'object', id: 'page7', + clipboardCopyPaste: false, questions: [ { help: '', diff --git a/public/workflow_connection.json b/public/workflow_connection.json new file mode 100644 index 000000000..7fe59d415 --- /dev/null +++ b/public/workflow_connection.json @@ -0,0 +1,3 @@ +{ + "one": "abc" +} \ No newline at end of file diff --git a/tests/integration/components/node-navbar/component-test.ts b/tests/integration/components/node-navbar/component-test.ts index 0bdeb3271..93c703c29 100644 --- a/tests/integration/components/node-navbar/component-test.ts +++ b/tests/integration/components/node-navbar/component-test.ts @@ -9,6 +9,7 @@ import { OsfLinkRouterStub } from '../../helpers/osf-link-router-stub'; enum NavCondition { HasParent, IQBRIMSEnabled, + WorkFlowEnabled, BinderHubEnabled, IsRegistration = 'isRegistration', IsPublic = 'public', @@ -23,6 +24,7 @@ enum NavLink { ThisNode, Files = 'files', IQBRIMS = 'iqbrims', + WorkFlow = 'workflow', BinderHub = 'binderhub', Wiki = 'wiki', Analytics = 'analytics', @@ -51,12 +53,14 @@ export class FakeNode { html: 'http://localhost:4200/fak3d', }; + [key: string]: any; + constructor(conditions: NavCondition[] = []) { for (const condition of conditions) { if (condition === NavCondition.HasParent) { this.parentId = faker.random.uuid(); } else if (condition !== NavCondition.IQBRIMSEnabled - && condition !== NavCondition.BinderHubEnabled) { + && condition !== NavCondition.BinderHubEnabled && condition !== NavCondition.WorkFlowEnabled) { this[condition] = true; } } @@ -278,6 +282,16 @@ module('Integration | Component | node-navbar', () => { NavLink.BinderHub, ], }, + { + conditions: [ + NavCondition.WorkFlowEnabled, + ], + links: [ + NavLink.ThisNode, + NavLink.Files, + NavLink.WorkFlow, + ], + }, ]; testCases.forEach((testCase, i) => { @@ -290,9 +304,12 @@ module('Integration | Component | node-navbar', () => { this.set('iqbrimsEnabled', iqbrimsEnabled.length > 0); const binderhubEnabled = testCase.conditions.filter(c => c === NavCondition.BinderHubEnabled); this.set('binderhubEnabled', binderhubEnabled.length > 0); + const workflowEnabled = testCase.conditions.filter(c => c === NavCondition.WorkFlowEnabled); + this.set('workflowEnabled', workflowEnabled.length > 0); await render( hbs`{{node-navbar node=this.node iqbrimsEnabled=this.iqbrimsEnabled + workflowEnabled=this.workflowEnabled binderhubEnabled=this.binderhubEnabled renderInPlace=true}}`, ); diff --git a/tests/integration/components/registries-side-nav/component-test.ts b/tests/integration/components/registries-side-nav/component-test.ts index 2a59dbd68..88f5cb89a 100644 --- a/tests/integration/components/registries-side-nav/component-test.ts +++ b/tests/integration/components/registries-side-nav/component-test.ts @@ -21,6 +21,9 @@ class RouterStub extends Service { } class CurrentUserStub extends Service { + ajaxHeaders() { + return {}; + } } /* tslint:disable:only-arrow-functions */ diff --git a/tests/integration/components/registries/schema-block-group-renderer/component-test.ts b/tests/integration/components/registries/schema-block-group-renderer/component-test.ts index 3c655f981..ff29b2523 100644 --- a/tests/integration/components/registries/schema-block-group-renderer/component-test.ts +++ b/tests/integration/components/registries/schema-block-group-renderer/component-test.ts @@ -172,6 +172,8 @@ module('Integration | Component | schema-block-group-renderer', hooks => { 'page-one_single-select-two': '', 'page-one_multi-select': [], 'page-one_file-input': [testFile], + datetimeInitiated: new Date(), + datetimeUpdated: new Date(), }; const registrationResponseChangeset = new Changeset(registrationResponse); const node = await this.store.findRecord('node', mirageNode.id, { diff --git a/translations/en-us.yml b/translations/en-us.yml index 19d25ad2a..c37de3466 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -546,6 +546,7 @@ node_navbar: toggle: 'Toggle navigation' project_nav: 'Project Navigation' iqbrims: 'IQB-RIMS' + workflow: 'Work Flow' wiki: Wiki analytics: Statistics registrations: Registrations @@ -1025,7 +1026,15 @@ registries: last_saved: 'Auto-saved: ' save_failed: 'Save failed. Unsaved changes present.' failed_auto_save: 'Failed to auto-save draft registration form' - auto_button_label: 'Fill' + get_retrieval_label: 'Fill' + copy_to_clipboard: 'Copy To Clipboard' + from_clipboard_paste: 'From Clipboard Paste' + clipboard_copied: 'Clipboard copied!' + warning_noautosave: 'Not autosaved. Please try again later!' + clipboard_pasted: 'Clipboard pasted!' + clipboard_fail: 'Failed to save from clipboard: ' + json_invalid: 'Clipboard does not contain valid JSON: ' + clipboard_unread: 'Failed to read from clipboard: ' review: title: 'Review registration before submitting' page_label: Review @@ -1768,6 +1777,57 @@ iqbrims: has_checklist: 'Checklist' files_comment: 'Comment' uploader_comment: 'Comment' + +workflow: + page_title: '{nodeTitle} Workscreen' + header: 'Workscreen' + loading: 'Loading Workflow config...' + tab1: 'task' + tab2: 'process' + tab3: 'Workflow Management' + tab1_content: 'Running Tasks' + tab2_content: 'Running Process' + tab3_content: 'Workflow Management' + start_button: 'Start Workflow' + Available_Workflows: 'Available Workflows' + register_button: 'Register Workflow' + activate: 'valid' + deactivate: 'invalid' + edit: 'edit' + remove: 'remove' + workflow_registration: 'Workflow Registration' + workflow_edit: 'Worlflow Edit' + workflow_engine: 'Workflow Engine' + workflow_name: 'Workflow Name' + workflow_id: 'Workflow ID' + creatorToken: 'Settings for worklaw creator token disbursement' + adminToken: 'Settings for dispensing tokens for project managers' + executorToken: 'Settings for dispensing tokens for the workflow executor' + cancel_button: 'Cancel' + register_button2: 'Registration' + update_button: 'Update' + available workflow processes: 'Available Workflow Processes' + workflow2_name: 'Name' + workflow_processid: 'Workflow_ProcessID' + initiator: 'Initiator' + starting time: 'Starting Time' + completion time: 'Completion Time' + status of processing: 'Status of Processing' + execution state: 'Execution State' + operation: 'Operation' + workflowprocessinformation: 'Workflow Process Information' + completed tasks: 'Completed Task' + workflowprocess: 'Workflow Process: ' + delprocess: 'Del Process' + running tasks: 'Running Tasks' + related files: 'Related Files' + stop_button: 'Delete_Process' + update_button2: 'Reload' + workflow_task_list: 'Workflow Task List' + request_targer: 'Request Target' + status: 'Status' + processid: 'ProcessID' + binderhub: page_title: '{nodeTitle} BinderHub' host_info: @@ -1887,4 +1947,4 @@ metadata: description: 'Select a destination.' array-input: add-item: 'Add item' - remove-item: 'Remove item' + remove-item: 'Remove item' \ No newline at end of file diff --git a/translations/ja.yml b/translations/ja.yml index 9a98c01ab..6d3bfb740 100644 --- a/translations/ja.yml +++ b/translations/ja.yml @@ -546,6 +546,7 @@ node_navbar: toggle: ナビゲーションを切り替える project_nav: プロジェクトナビゲーション iqbrims: IQB-RIMS + workflow: ワークフロー wiki: Wiki analytics: 統計 registrations: 登録 @@ -641,7 +642,7 @@ node: metadata: new_report_modal: title: メタデータ様式を選択 - info: 'デフォルトは「公的資金による研究データのメタデータ登録」です。
    • 「ムーンショット目標2未病データベース-メタデータ」は該当する人のみ選択してください
    新規に作成したいプロジェクトメタデータの様式を以下から選択してください。
    • メタデータ作成では、様式で定義された各項目を入力することができます。
    • メタデータ作成では、このプロジェクトに含まれるファイルのメタデータを登録することができます。
    • メタデータ作成から報告書様式に準拠したファイルをダウンロードし、報告書等の提出に利用することができます。
    ' + info: 'デフォルトは「公的資金による研究データのメタデータ登録」です。
    査読付き論文著者最終稿の登録も可能です。
    • 「ムーンショット目標2未病データベース-メタデータ」は該当する人のみ選択してください
    新規に作成したいプロジェクトメタデータの様式を以下から選択してください。
    • メタデータ作成では、様式で定義された各項目を入力することができます。
    • メタデータ作成では、このプロジェクトに含まれるファイルのメタデータを登録することができます。
    • メタデータ作成から報告書様式に準拠したファイルをダウンロードし、報告書等の提出に利用することができます。
    ' note: *今後、対応する事業や機関の増加に合わせて、メタデータの様式は随時追加されていきます。 create: メタデータを作成 page_title: '{nodeTitle} メタデータ' @@ -1025,7 +1026,15 @@ registries: last_saved: '自動保存済み: ' save_failed: '保存に失敗しました。保存できていない変更があります。' failed_auto_save: '下書きの自動保存に失敗しました。' - auto_button_label: '再取得' + get_retrieval_label: '自動取得' + copy_to_clipboard: 'クリップボードにコピー' + from_clipboard_paste: 'クリップボードから貼り付け' + clipboard_copied: 'クリップボードコピーされました!' + warning_noautosave: '自動保存されていません。少し時間をたってから、もう一度お試しください!' + clipboard_pasted: 'クリップボード貼り付けされました!' + clipboard_fail: 'クリップボードからの保存に失敗しました: ' + json_invalid: 'クリップボードには有効な JSON が含まれていません: ' + clipboard_unread: 'クリップボードからの読み取りに失敗しました: ' review: title: 送信前に登録を確認する page_label: 内容確認 @@ -1888,3 +1897,53 @@ metadata: array-input: add-item: 'アイテム追加' remove-item: 'アイテム削除' + +workflow: + page_title: '{nodeTitle} ワークフロー' + header: 'ワークフロー' + loading: 'ロード中...' + tab1: 'タスク' + tab2: 'プロセス' + tab3: 'ワークフロー管理' + tab1_content: 'タスク一覧' + tab2_content: 'プロセス一覧' + tab3_content: 'ワークフロー管理' + start_button: 'ワークフローの開始' + Available_Workflows: '利用可能なワークフロー一覧' + register_button: 'ワークフローの登録' + activate: '有効' + deactivate: '無効' + edit: '情報の編集' + remove: '登録解除' + workflow_registration: 'ワークフローの登録' + workflow_edit: 'ワークフロー情報の編集' + workflow_engine: 'ワークフローエンジン' + workflow_name: 'ワークフロー名' + workflow_id: 'ワークフローID' + creatorToken: 'ワークフロー作成者のトークンの払い出しに関する設定' + adminToken: 'プロジェクト管理者のトークンの払い出しに関する設定' + executorToken: 'ワークフロー実行者のトークンの払い出しに関する設定' + cancel_button: 'キャンセル' + register_button2: '登録' + update_button: '更新' + available workflow processes: 'ワークフロープロセスの一覧' + workflow2_name: '名前' + workflow_processid: 'ワークフロープロセスID' + initiator: '開始者' + starting time: '開始時刻' + completion time: '完了時刻' + status of processing: '処理状況' + execution state: '実行状況' + operation: '操作' + workflowprocessinformation: 'ワークフロープロセスの情報' + related files: '関連ファイル' + completed tasks: '完了済タスク' + update_button2: '再読み込み' + workflow_task_list: 'ワークフロータスクの一覧' + request_targer: '依頼対象' + status: '状態' + processid: 'プロセスID' + stop_button: 'プロセス削除' + workflowprocess: 'ワークフロープロセス' + delprocess: 'プロセス削除' + running tasks: '実行中タスク'