mirror of
https://github.com/nextcloud/tables.git
synced 2025-08-16 15:17:20 +00:00
feat(import): change column format during import
Signed-off-by: Luka Trovic <luka@nextcloud.com>
This commit is contained in:

committed by
Julius Härtl

parent
ee361f0217
commit
a7f9c804b3
@ -103,9 +103,13 @@ return [
|
||||
['name' => 'share#destroy', 'url' => '/share/{id}', 'verb' => 'DELETE'],
|
||||
|
||||
// import
|
||||
['name' => 'import#previewImportTable', 'url' => '/import-preview/table/{tableId}', 'verb' => 'POST'],
|
||||
['name' => 'import#importInTable', 'url' => '/import/table/{tableId}', 'verb' => 'POST'],
|
||||
['name' => 'import#previewImportView', 'url' => '/import-preview/view/{viewId}', 'verb' => 'POST'],
|
||||
['name' => 'import#importInView', 'url' => '/import/view/{viewId}', 'verb' => 'POST'],
|
||||
['name' => 'import#previewUploadImportTable', 'url' => '/importupload-preview/table/{tableId}', 'verb' => 'POST'],
|
||||
['name' => 'import#importUploadInTable', 'url' => '/importupload/table/{tableId}', 'verb' => 'POST'],
|
||||
['name' => 'import#previewUploadImportView', 'url' => '/importupload-preview/view/{viewId}', 'verb' => 'POST'],
|
||||
['name' => 'import#importUploadInView', 'url' => '/importupload/view/{viewId}', 'verb' => 'POST'],
|
||||
|
||||
// search
|
||||
|
@ -22,6 +22,10 @@ describe('Import csv', () => {
|
||||
cy.get('.file-picker__files').contains('test-import').click()
|
||||
cy.get('.file-picker button span').contains('Choose test-import.csv').click()
|
||||
cy.get('.modal__content .import-filename', { timeout: 5000 }).should('be.visible')
|
||||
|
||||
cy.get('.modal__content button').contains('Preview').click()
|
||||
cy.get('.file_import__preview tbody tr').should('have.length', 4)
|
||||
|
||||
cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*'}).as('importUploadReq')
|
||||
cy.get('.modal__content button').contains('Import').click()
|
||||
cy.wait('@importUploadReq')
|
||||
@ -38,6 +42,10 @@ describe('Import csv', () => {
|
||||
cy.clickOnTableThreeDotMenu('Import')
|
||||
cy.get('.modal__content button').contains('Upload from device').click()
|
||||
cy.get('input[type="file"]').selectFile('cypress/fixtures/test-import.csv', { force: true })
|
||||
|
||||
cy.get('.modal__content button').contains('Preview').click()
|
||||
cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 4)
|
||||
|
||||
cy.intercept({ method: 'POST', url: '**/apps/tables/importupload/table/*'}).as('importUploadReq')
|
||||
cy.get('.modal__content button').contains('Import').click()
|
||||
cy.wait('@importUploadReq')
|
||||
@ -104,6 +112,6 @@ if (!['stable27'].includes(Cypress.env('ncVersion'))) {
|
||||
cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
|
||||
cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -45,8 +45,10 @@ describe('Manage a table', () => {
|
||||
cy.get('.modal__content button').contains('Select from Files').click()
|
||||
cy.get('.file-picker__files').contains('test-import').click()
|
||||
cy.get('.file-picker button span').contains('Choose test-import.csv').click()
|
||||
cy.get('.modal__content button').contains('Preview').click()
|
||||
cy.get('.file_import__preview tbody tr').should('have.length', 4)
|
||||
cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*'}).as('importUploadReq')
|
||||
cy.get('.modal__content button').contains('Import').click()
|
||||
cy.get('.modal__content button').contains('Import').scrollIntoView().click()
|
||||
cy.wait('@importUploadReq')
|
||||
cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
|
||||
cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '0')
|
||||
|
@ -48,33 +48,50 @@ class ImportController extends Controller {
|
||||
$this->l10n = $l10n;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importInTable(int $tableId, String $path, bool $createMissingColumns = true): DataResponse {
|
||||
return $this->handleError(function () use ($tableId, $path, $createMissingColumns) {
|
||||
return $this->service->import($tableId, null, $path, $createMissingColumns);
|
||||
public function previewImportTable(int $tableId, String $path): DataResponse {
|
||||
return $this->handleError(function () use ($tableId, $path) {
|
||||
return $this->service->previewImport($tableId, null, $path);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importInView(int $viewId, String $path, bool $createMissingColumns = true): DataResponse {
|
||||
return $this->handleError(function () use ($viewId, $path, $createMissingColumns) {
|
||||
return $this->service->import(null, $viewId, $path, $createMissingColumns);
|
||||
public function importInTable(int $tableId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse {
|
||||
return $this->handleError(function () use ($tableId, $path, $createMissingColumns, $columnsConfig) {
|
||||
return $this->service->import($tableId, null, $path, $createMissingColumns, $columnsConfig);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importUploadInTable(int $tableId, bool $createMissingColumns = true): DataResponse {
|
||||
public function previewImportView(int $viewId, String $path): DataResponse {
|
||||
return $this->handleError(function () use ($viewId, $path) {
|
||||
return $this->service->previewImport(null, $viewId, $path);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importInView(int $viewId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse {
|
||||
return $this->handleError(function () use ($viewId, $path, $createMissingColumns, $columnsConfig) {
|
||||
return $this->service->import(null, $viewId, $path, $createMissingColumns, $columnsConfig);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function previewUploadImportTable(int $tableId): DataResponse {
|
||||
try {
|
||||
$file = $this->getUploadedFile('uploadfile');
|
||||
return $this->handleError(function () use ($tableId, $file, $createMissingColumns) {
|
||||
return $this->service->import($tableId, null, $file['tmp_name'], $createMissingColumns);
|
||||
return $this->handleError(function () use ($tableId, $file) {
|
||||
return $this->service->previewImport($tableId, null, $file['tmp_name']);
|
||||
});
|
||||
} catch (UploadException | NotPermittedException $e) {
|
||||
$this->logger->error('Upload error', ['exception' => $e]);
|
||||
@ -85,11 +102,43 @@ class ImportController extends Controller {
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importUploadInView(int $viewId, bool $createMissingColumns = true): DataResponse {
|
||||
public function importUploadInTable(int $tableId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse {
|
||||
try {
|
||||
$columnsConfigArray = json_decode($columnsConfig, true);
|
||||
$file = $this->getUploadedFile('uploadfile');
|
||||
return $this->handleError(function () use ($tableId, $file, $createMissingColumns, $columnsConfigArray) {
|
||||
return $this->service->import($tableId, null, $file['tmp_name'], $createMissingColumns, $columnsConfigArray);
|
||||
});
|
||||
} catch (UploadException | NotPermittedException $e) {
|
||||
$this->logger->error('Upload error', ['exception' => $e]);
|
||||
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function previewUploadImportView(int $viewId): DataResponse {
|
||||
try {
|
||||
$file = $this->getUploadedFile('uploadfile');
|
||||
return $this->handleError(function () use ($viewId, $file, $createMissingColumns) {
|
||||
return $this->service->import(null, $viewId, $file['tmp_name'], $createMissingColumns);
|
||||
return $this->handleError(function () use ($viewId, $file) {
|
||||
return $this->service->previewImport(null, $viewId, $file['tmp_name']);
|
||||
});
|
||||
} catch (UploadException | NotPermittedException $e) {
|
||||
$this->logger->error('Upload error', ['exception' => $e]);
|
||||
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function importUploadInView(int $viewId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse {
|
||||
try {
|
||||
$columnsConfigArray = json_decode($columnsConfig, true);
|
||||
$file = $this->getUploadedFile('uploadfile');
|
||||
return $this->handleError(function () use ($viewId, $file, $createMissingColumns, $columnsConfigArray) {
|
||||
return $this->service->import(null, $viewId, $file['tmp_name'], $createMissingColumns, $columnsConfigArray);
|
||||
});
|
||||
} catch (UploadException | NotPermittedException $e) {
|
||||
$this->logger->error('Upload error', ['exception' => $e]);
|
||||
|
@ -227,9 +227,28 @@ class ColumnService extends SuperService {
|
||||
throw new PermissionError('create column for the table id = '.$table->getId().' is not allowed.');
|
||||
}
|
||||
|
||||
// Add number to title to avoid duplicate
|
||||
$columns = $this->mapper->findAllByTable($table->getId());
|
||||
$i = 1;
|
||||
$newTitle = $title;
|
||||
while (true) {
|
||||
$found = false;
|
||||
foreach ($columns as $column) {
|
||||
if ($column->getTitle() === $newTitle) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
break;
|
||||
}
|
||||
$newTitle = $title . ' (' . $i . ')';
|
||||
$i++;
|
||||
}
|
||||
|
||||
$time = new DateTime();
|
||||
$item = new Column();
|
||||
$item->setTitle($title);
|
||||
$item->setTitle($newTitle);
|
||||
$item->setTableId($table->getId());
|
||||
$item->setType($type);
|
||||
$item->setSubtype($subtype !== null ? $subtype: '');
|
||||
@ -572,7 +591,9 @@ class ColumnService extends SuperService {
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$this->enhanceColumn($column);
|
||||
if ($column instanceof Column) {
|
||||
$this->enhanceColumn($column);
|
||||
}
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ class ImportService extends SuperService {
|
||||
private int $countErrors = 0;
|
||||
private int $countParsingErrors = 0;
|
||||
|
||||
private array $rawColumnTitles = [];
|
||||
private array $rawColumnDataTypes = [];
|
||||
private array $columnsConfig = [];
|
||||
|
||||
public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId,
|
||||
IRootFolder $rootFolder, ColumnService $columnService, RowService $rowService, TableService $tableService, ViewService $viewService, IUserManager $userManager) {
|
||||
parent::__construct($logger, $userId, $permissionsService);
|
||||
@ -57,6 +61,126 @@ class ImportService extends SuperService {
|
||||
$this->userManager = $userManager;
|
||||
}
|
||||
|
||||
public function previewImport(?int $tableId, ?int $viewId, string $path): array {
|
||||
if ($viewId !== null) {
|
||||
$this->viewId = $viewId;
|
||||
} elseif ($tableId) {
|
||||
$this->tableId = $tableId;
|
||||
} else {
|
||||
$e = new \Exception('Neither tableId nor viewId is given.');
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage());
|
||||
}
|
||||
|
||||
$this->createUnknownColumns = false;
|
||||
$previewData = [];
|
||||
|
||||
try {
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||
$error = false;
|
||||
if ($userFolder->nodeExists($path)) {
|
||||
$file = $userFolder->get($path);
|
||||
$tmpFileName = $file->getStorage()->getLocalFile($file->getInternalPath());
|
||||
if($tmpFileName) {
|
||||
$spreadsheet = IOFactory::load($tmpFileName);
|
||||
$previewData = $this->getPreviewData($spreadsheet->getActiveSheet());
|
||||
} else {
|
||||
$error = true;
|
||||
}
|
||||
} elseif (\file_exists($path)) {
|
||||
$spreadsheet = IOFactory::load($path);
|
||||
$previewData = $this->getPreviewData($spreadsheet->getActiveSheet());
|
||||
} else {
|
||||
$error = true;
|
||||
}
|
||||
|
||||
if($error) {
|
||||
throw new NotFoundError('File for import could not be found.');
|
||||
}
|
||||
|
||||
} catch (NotFoundException|NotPermittedException|NoUserException|InternalError|PermissionError $e) {
|
||||
$this->logger->warning('Storage for user could not be found', ['exception' => $e]);
|
||||
throw new NotFoundError('Storage for user could not be found');
|
||||
}
|
||||
|
||||
return $previewData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Worksheet $worksheet
|
||||
* @throws DoesNotExistException
|
||||
* @throws InternalError
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws NotFoundError
|
||||
* @throws PermissionError
|
||||
*/
|
||||
private function getPreviewData(Worksheet $worksheet): array {
|
||||
$firstRow = $worksheet->getRowIterator()->current();
|
||||
$secondRow = $worksheet->getRowIterator()->seek(2)->current();
|
||||
|
||||
// Prepare columns data
|
||||
$columns = [];
|
||||
$this->getColumns($firstRow, $secondRow);
|
||||
|
||||
foreach ($this->rawColumnTitles as $colIndex => $title) {
|
||||
if ($this->columns[$colIndex] !== '') {
|
||||
/** @var Column $column */
|
||||
$column = $this->columns[$colIndex];
|
||||
$columns[] = $column;
|
||||
} else {
|
||||
$columns[] = [
|
||||
'title' => $title,
|
||||
'type' => $this->rawColumnDataTypes[$colIndex]['type'],
|
||||
'subtype' => $this->rawColumnDataTypes[$colIndex]['subtype'],
|
||||
'numberDecimals' => $this->rawColumnDataTypes[$colIndex]['number_decimals'] ?? 0,
|
||||
'numberPrefix' => $this->rawColumnDataTypes[$colIndex]['number_prefix'] ?? '',
|
||||
'numberSuffix' => $this->rawColumnDataTypes[$colIndex]['number_suffix'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare rows data
|
||||
$count = 0;
|
||||
$maxCount = 3;
|
||||
$rows = [];
|
||||
|
||||
foreach ($worksheet->getRowIterator(2) as $row) {
|
||||
$rowData = [];
|
||||
$cellIterator = $row->getCellIterator();
|
||||
$cellIterator->setIterateOnlyExistingCells(false);
|
||||
|
||||
foreach ($cellIterator as $cellIndex => $cell) {
|
||||
$value = $cell->getValue();
|
||||
$colIndex = (int) $cellIndex;
|
||||
$column = $this->columns[$colIndex];
|
||||
|
||||
if (($column && $column->getType() === 'datetime') || (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'datetime')) {
|
||||
$value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i');
|
||||
} elseif (($column && $column->getType() === 'number' && $column->getNumberSuffix() === '%')
|
||||
|| (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'number' && $columns[$colIndex]['numberSuffix'] === '%')) {
|
||||
$value = $value * 100;
|
||||
} elseif (($column && $column->getType() === 'selection' && $column->getSubtype() === 'check')
|
||||
|| (is_array($columns[$colIndex]) && $columns[$colIndex]['type'] === 'selection' && $columns[$colIndex]['subtype'] === 'check')) {
|
||||
$value = $cell->getFormattedValue() === 'TRUE' ? 'true' : 'false';
|
||||
}
|
||||
|
||||
$rowData[] = $value;
|
||||
}
|
||||
|
||||
$rows[] = $rowData;
|
||||
$count++;
|
||||
|
||||
if ($count >= $maxCount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'columns' => $columns,
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $tableId
|
||||
* @param int|null $viewId
|
||||
@ -69,7 +193,7 @@ class ImportService extends SuperService {
|
||||
* @throws NotFoundError
|
||||
* @throws PermissionError
|
||||
*/
|
||||
public function import(?int $tableId, ?int $viewId, string $path, bool $createMissingColumns = true): array {
|
||||
public function import(?int $tableId, ?int $viewId, string $path, bool $createMissingColumns = true, array $columnsConfig = []): array {
|
||||
if ($viewId !== null) {
|
||||
$view = $this->viewService->find($viewId);
|
||||
if (!$this->permissionsService->canCreateRows($view)) {
|
||||
@ -101,6 +225,7 @@ class ImportService extends SuperService {
|
||||
}
|
||||
|
||||
$this->createUnknownColumns = $createMissingColumns;
|
||||
$this->columnsConfig = $columnsConfig;
|
||||
|
||||
try {
|
||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||
@ -274,9 +399,45 @@ class ImportService extends SuperService {
|
||||
$secondRowCellIterator = $secondRow->getCellIterator();
|
||||
$titles = [];
|
||||
$dataTypes = [];
|
||||
$index = 0;
|
||||
$countMatchingColumnsFromConfig = 0;
|
||||
$countCreatedColumnsFromConfig = 0;
|
||||
foreach ($cellIterator as $cell) {
|
||||
if ($cell && $cell->getValue() !== null && $cell->getValue() !== '') {
|
||||
$titles[] = $cell->getValue();
|
||||
$title = $cell->getValue();
|
||||
|
||||
if (isset($this->columnsConfig[$index]) && $this->columnsConfig[$index]['action'] === 'exist' && $this->columnsConfig[$index]['existColumn']) {
|
||||
$title = $this->columnsConfig[$index]['existColumn']['label'];
|
||||
$countMatchingColumnsFromConfig++;
|
||||
}
|
||||
if (isset($this->columnsConfig[$index]) && $this->columnsConfig[$index]['action'] === 'new' && $this->createUnknownColumns) {
|
||||
$column = $this->columnService->create(
|
||||
$this->userId,
|
||||
$this->tableId,
|
||||
$this->viewId,
|
||||
$this->columnsConfig[$index]['type'],
|
||||
$this->columnsConfig[$index]['subtype'],
|
||||
$this->columnsConfig[$index]['title'],
|
||||
$this->columnsConfig[$index]['mandatory'] ?? false,
|
||||
$this->columnsConfig[$index]['description'] ?? '',
|
||||
$this->columnsConfig[$index]['textDefault'] ?? '',
|
||||
$this->columnsConfig[$index]['textAllowedPattern'] ?? '',
|
||||
$this->columnsConfig[$index]['textMaxLength'] ?? null,
|
||||
$this->columnsConfig[$index]['numberPrefix'] ?? '',
|
||||
$this->columnsConfig[$index]['numberSuffix'] ?? '',
|
||||
$this->columnsConfig[$index]['numberDefault'] ?? null,
|
||||
$this->columnsConfig[$index]['numberMin'] ?? null,
|
||||
$this->columnsConfig[$index]['numberMax'] ?? null,
|
||||
$this->columnsConfig[$index]['numberDecimals'] ?? 0,
|
||||
$this->columnsConfig[$index]['selectionOptions'] ?? '',
|
||||
$this->columnsConfig[$index]['selectionDefault'] ?? '',
|
||||
$this->columnsConfig[$index]['datetimeDefault'] ?? '',
|
||||
$this->columnsConfig[$index]['selectedViewIds'] ?? []
|
||||
);
|
||||
$title = $column->getTitle();
|
||||
$countCreatedColumnsFromConfig++;
|
||||
}
|
||||
$titles[] = $title;
|
||||
|
||||
// Convert data type to our data type
|
||||
$dataTypes[] = $this->parseColumnDataType($secondRowCellIterator->current());
|
||||
@ -285,9 +446,18 @@ class ImportService extends SuperService {
|
||||
$this->countErrors++;
|
||||
}
|
||||
$secondRowCellIterator->next();
|
||||
$index++;
|
||||
}
|
||||
|
||||
$this->rawColumnTitles = $titles;
|
||||
$this->rawColumnDataTypes = $dataTypes;
|
||||
|
||||
try {
|
||||
$this->columns = $this->columnService->findOrCreateColumnsByTitleForTableAsArray($this->tableId, $this->viewId, $titles, $dataTypes, $this->userId, $this->createUnknownColumns, $this->countCreatedColumns, $this->countMatchingColumns);
|
||||
if (!empty($this->columnsConfig)) {
|
||||
$this->countMatchingColumns = $countMatchingColumnsFromConfig;
|
||||
$this->countCreatedColumns = $countCreatedColumnsFromConfig;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw new InternalError($e->getMessage());
|
||||
}
|
||||
|
162
src/App.vue
162
src/App.vue
@ -144,107 +144,111 @@ export default {
|
||||
</style>
|
||||
<style lang="scss">
|
||||
|
||||
h1 {
|
||||
font-size: 1.98em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.98em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: larger;
|
||||
}
|
||||
h2 {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: large;
|
||||
}
|
||||
h3 {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
}
|
||||
h4 {
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
p, .p {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
p, .p {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.editor-wrapper p, .editor-wrapper .p {
|
||||
padding: 0;
|
||||
}
|
||||
.editor-wrapper p, .editor-wrapper .p {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p span, .p span, p .span, .p .span, .p.span, p.span, .light {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
p span, .p span, p .span, .p .span, .p.span, p.span, .light {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
p code {
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.8em 1em;
|
||||
margin-bottom: 0.8em;
|
||||
font-family: 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', monospace;
|
||||
}
|
||||
p code {
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--color-background-dark);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.8em 1em;
|
||||
margin-bottom: 0.8em;
|
||||
font-family: 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', monospace;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.light {
|
||||
font-weight: 100;
|
||||
color: var(--color-text-lighter);
|
||||
/*
|
||||
.light {
|
||||
font-weight: 100;
|
||||
color: var(--color-text-lighter);
|
||||
/*
|
||||
background-color: var(--color-background-hover);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
button[class^='icon-'] {
|
||||
min-width: 36px !important;
|
||||
}
|
||||
button[class^='icon-'] {
|
||||
min-width: 36px !important;
|
||||
}
|
||||
|
||||
[class^='col-'] > span {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
[class^='col-'] > span {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.icon-left {
|
||||
background-position: left;
|
||||
padding-left: 22px;
|
||||
}
|
||||
.icon-left {
|
||||
background-position: left;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
font-weight: bold;
|
||||
}
|
||||
.mandatory {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.v-popover button {
|
||||
height: 25px;
|
||||
margin-left: 10px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.v-popover button {
|
||||
height: 25px;
|
||||
margin-left: 10px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.popover__inner p, .v-popper__inner table {
|
||||
padding: 15px;
|
||||
}
|
||||
.popover__inner p, .v-popper__inner table {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.v-popper__inner table td {
|
||||
padding-right: 15px;
|
||||
}
|
||||
.v-popper__inner table td {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
.error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.error input {
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
.error input {
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
.icon-loading:first-child {
|
||||
top: 10vh;
|
||||
}
|
||||
.icon-loading:first-child {
|
||||
top: 10vh;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block !important;
|
||||
}
|
||||
.block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -132,6 +132,9 @@ export default {
|
||||
this.combinedType = this.columnId
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.combinedType = this.columnId
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<NcModal v-if="showModal" size="large" @close="actionCancel">
|
||||
<div class="modal__content">
|
||||
<div class="modal__content create-column">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<h2>{{ t('tables', 'Create column') }}</h2>
|
||||
@ -142,6 +142,14 @@ export default {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isCustomSave: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
preset: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -211,6 +219,33 @@ export default {
|
||||
watch: {
|
||||
combinedType() {
|
||||
this.reset(false, false)
|
||||
if (this.preset) {
|
||||
for (const key in this.preset) {
|
||||
if (['type', 'subtype'].includes(key)) {
|
||||
continue
|
||||
}
|
||||
if (Object.hasOwn(this.column, key)) {
|
||||
this.column[key] = this.preset[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
preset: {
|
||||
handler() {
|
||||
if (this.preset) {
|
||||
const combinedType = this.preset.type + (this.preset.subtype ? `-${this.preset.subtype}` : '')
|
||||
if (combinedType !== this.combinedType) {
|
||||
this.combinedType = combinedType
|
||||
} else {
|
||||
for (const key in this.preset) {
|
||||
if (Object.hasOwn(this.column, key)) {
|
||||
this.column[key] = this.preset[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
showModal() {
|
||||
this.$nextTick(() => {
|
||||
@ -237,6 +272,12 @@ export default {
|
||||
showInfo(t('tables', 'You need to select a type for the new column.'))
|
||||
this.typeMissingError = true
|
||||
} else {
|
||||
this.$emit('save', this.prepareSubmitData())
|
||||
if (this.isCustomSave) {
|
||||
this.reset()
|
||||
this.$emit('close')
|
||||
return
|
||||
}
|
||||
await this.sendNewColumnToBE()
|
||||
if (this.addNewAfterSave) {
|
||||
this.reset(true, true, false)
|
||||
@ -250,42 +291,46 @@ export default {
|
||||
this.reset()
|
||||
this.$emit('close')
|
||||
},
|
||||
prepareSubmitData() {
|
||||
const data = {
|
||||
type: this.column.type,
|
||||
subtype: this.column.subtype,
|
||||
title: this.column.title,
|
||||
description: this.column.description,
|
||||
selectedViewIds: this.column.selectedViews.map(view => view.id),
|
||||
mandatory: this.column.mandatory,
|
||||
viewId: this.isView ? this.element.id : null,
|
||||
tableId: !this.isView ? this.element.id : null,
|
||||
}
|
||||
if (this.combinedType === ColumnTypes.TextLine || this.combinedType === ColumnTypes.TextLong) {
|
||||
data.textDefault = this.column.textDefault
|
||||
data.textMaxLength = this.column.textMaxLength
|
||||
} else if (this.combinedType === ColumnTypes.TextRich) {
|
||||
data.textDefault = this.column.textDefault
|
||||
} else if (this.combinedType === ColumnTypes.TextLink) {
|
||||
data.textAllowedPattern = this.column.textAllowedPattern
|
||||
} else if (this.column.type === 'selection') {
|
||||
data.selectionDefault = typeof this.column.selectionDefault !== 'string' ? JSON.stringify(this.column.selectionDefault) : this.column.selectionDefault
|
||||
if (this.column.subtype !== 'check') {
|
||||
data.selectionOptions = JSON.stringify(this.column.selectionOptions)
|
||||
}
|
||||
} else if (this.column.type === 'datetime') {
|
||||
data.datetimeDefault = this.column.datetimeDefault ? this.column.subtype === 'date' ? 'today' : 'now' : ''
|
||||
} else if (this.column.type === 'number') {
|
||||
data.numberDefault = this.column.numberDefault
|
||||
if (this.column.subtype === '') {
|
||||
data.numberDecimals = this.column.numberDecimals
|
||||
data.numberMin = this.column.numberMin
|
||||
data.numberMax = this.column.numberMax
|
||||
data.numberPrefix = this.column.numberPrefix
|
||||
data.numberSuffix = this.column.numberSuffix
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
async sendNewColumnToBE() {
|
||||
try {
|
||||
const data = {
|
||||
type: this.column.type,
|
||||
subtype: this.column.subtype,
|
||||
title: this.column.title,
|
||||
description: this.column.description,
|
||||
selectedViewIds: this.column.selectedViews.map(view => view.id),
|
||||
mandatory: this.column.mandatory,
|
||||
viewId: this.isView ? this.element.id : null,
|
||||
tableId: !this.isView ? this.element.id : null,
|
||||
}
|
||||
if (this.combinedType === ColumnTypes.TextLine || this.combinedType === ColumnTypes.TextLong) {
|
||||
data.textDefault = this.column.textDefault
|
||||
data.textMaxLength = this.column.textMaxLength
|
||||
} else if (this.combinedType === ColumnTypes.TextRich) {
|
||||
data.textDefault = this.column.textDefault
|
||||
} else if (this.combinedType === ColumnTypes.TextLink) {
|
||||
data.textAllowedPattern = this.column.textAllowedPattern
|
||||
} else if (this.column.type === 'selection') {
|
||||
data.selectionDefault = typeof this.column.selectionDefault !== 'string' ? JSON.stringify(this.column.selectionDefault) : this.column.selectionDefault
|
||||
if (this.column.subtype !== 'check') {
|
||||
data.selectionOptions = JSON.stringify(this.column.selectionOptions)
|
||||
}
|
||||
} else if (this.column.type === 'datetime') {
|
||||
data.datetimeDefault = this.column.datetimeDefault ? this.column.subtype === 'date' ? 'today' : 'now' : ''
|
||||
} else if (this.column.type === 'number') {
|
||||
data.numberDefault = this.column.numberDefault
|
||||
if (this.column.subtype === '') {
|
||||
data.numberDecimals = this.column.numberDecimals
|
||||
data.numberMin = this.column.numberMin
|
||||
data.numberMax = this.column.numberMax
|
||||
data.numberPrefix = this.column.numberPrefix
|
||||
data.numberSuffix = this.column.numberSuffix
|
||||
}
|
||||
}
|
||||
const data = this.prepareSubmitData()
|
||||
const res = await this.$store.dispatch('insertNewColumn', { isView: this.isView, elementId: this.element.id, data })
|
||||
if (res) {
|
||||
showSuccess(t('tables', 'The column "{column}" was created.', { column: this.column.title }))
|
||||
|
@ -3,12 +3,12 @@
|
||||
<div class="modal__content">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<h2>{{ t('tables', 'Import table') }}</h2>
|
||||
<h2>{{ title }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Starting -->
|
||||
<div v-if="!loading && result === null && !waitForReload">
|
||||
<div v-if="!loading && result === null && preview === null && !waitForReload">
|
||||
<div class="row space-T">
|
||||
{{ t('tables', 'Add data to the table from a file') }}
|
||||
</div>
|
||||
@ -58,7 +58,20 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="fix-col-4 end">
|
||||
<NcButton :aria-label="t('tables', 'Import')" type="primary" @click="actionSubmit">
|
||||
<NcButton :aria-label="t('tables', 'Preview')" type="primary" @click="actionPreview">
|
||||
{{ t('tables', 'Preview') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- show preview -->
|
||||
<div v-if="!loading && preview !== null && !result && !waitForReload">
|
||||
<ImportPreview :preview-data="preview" :element="element" :create-missing-columns="createMissingColumns" @update:columns="onUpdateColumnsConfig" />
|
||||
|
||||
<div class="row">
|
||||
<div class="fix-col-4 space-T end">
|
||||
<NcButton :aria-label="t('tables', 'Import')" type="primary" @click="actionImport">
|
||||
{{ t('tables', 'Import') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
@ -107,6 +120,8 @@ import { generateUrl } from '@nextcloud/router'
|
||||
import { mapGetters } from 'vuex'
|
||||
import NcIconTimerSand from '../../shared/components/ncIconTimerSand/NcIconTimerSand.vue'
|
||||
import ImportResults from './ImportResults.vue'
|
||||
import ImportPreview from './ImportPreview.vue'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
|
||||
@ -119,6 +134,7 @@ export default {
|
||||
NcModal,
|
||||
NcButton,
|
||||
ImportResults,
|
||||
ImportPreview,
|
||||
NcCheckboxRadioSwitch,
|
||||
RowFormWrapper,
|
||||
NcEmptyContent,
|
||||
@ -148,6 +164,8 @@ export default {
|
||||
pathError: false,
|
||||
loading: false,
|
||||
result: null,
|
||||
preview: null,
|
||||
columnsConfig: [],
|
||||
waitForReload: false,
|
||||
mimeTypes: [
|
||||
'text/csv',
|
||||
@ -179,6 +197,15 @@ export default {
|
||||
|
||||
return fileName
|
||||
},
|
||||
title() {
|
||||
let title = t('tables', 'Import table')
|
||||
|
||||
if (!this.loading && this.preview !== null && !this.result && !this.waitForReload) {
|
||||
title = t('tables', 'Preview imported table')
|
||||
}
|
||||
|
||||
return title
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
element() {
|
||||
@ -223,14 +250,14 @@ export default {
|
||||
|
||||
this.actionCancel()
|
||||
},
|
||||
actionSubmit() {
|
||||
actionPreview() {
|
||||
if (this.selectedUploadFile && this.selectedUploadFile.type !== '' && !this.mimeTypes.includes(this.selectedUploadFile.type)) {
|
||||
showWarning(t('tables', 'The selected file is not supported.'))
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.selectedUploadFile) {
|
||||
this.uploadFile()
|
||||
this.previewImportFromUploadFile()
|
||||
return
|
||||
}
|
||||
|
||||
@ -240,12 +267,119 @@ export default {
|
||||
return null
|
||||
}
|
||||
this.pathError = false
|
||||
this.import()
|
||||
this.previewImportFromPath()
|
||||
},
|
||||
async import() {
|
||||
async previewImportFromPath() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await axios.post(generateUrl('/apps/tables/import/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id), { path: this.path, createMissingColumns: this.getCreateMissingColumns })
|
||||
const res = await axios.post(generateUrl('/apps/tables/import-preview/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id), { path: this.path })
|
||||
if (res.status === 200) {
|
||||
this.preview = res.data
|
||||
this.loading = false
|
||||
} else if (res.status === 401) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, not authorized. Are you logged in?'))
|
||||
} else if (res.status === 403) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, missing needed permission.'))
|
||||
} else if (res.status === 404) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, needed resources were not found.'))
|
||||
} else {
|
||||
showError(t('tables', 'Could not import data due to unknown errors.'))
|
||||
console.debug('error while importing', res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
async previewImportFromUploadFile() {
|
||||
this.loading = true
|
||||
try {
|
||||
const url = generateUrl('/apps/tables/importupload-preview/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id)
|
||||
const formData = new FormData()
|
||||
formData.append('uploadfile', this.selectedUploadFile)
|
||||
|
||||
const res = await axios.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
this.preview = res.data
|
||||
this.loading = false
|
||||
} else if (res.status === 401) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, not authorized. Are you logged in?'))
|
||||
} else if (res.status === 403) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, missing needed permission.'))
|
||||
} else if (res.status === 404) {
|
||||
console.debug('error while importing', res)
|
||||
showError(t('tables', 'Could not import, needed resources were not found.'))
|
||||
} else {
|
||||
showError(t('tables', 'Could not import data due to unknown errors.'))
|
||||
console.debug('error while importing', res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
actionImport() {
|
||||
if (this.selectedUploadFile && this.selectedUploadFile.type !== '' && !this.mimeTypes.includes(this.selectedUploadFile.type)) {
|
||||
showWarning(t('tables', 'The selected file is not supported.'))
|
||||
return null
|
||||
}
|
||||
|
||||
if (!this.validateColumnsConfig()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.selectedUploadFile) {
|
||||
this.importFromUploadFile()
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.path === '') {
|
||||
showWarning(t('tables', 'Please select a file.'))
|
||||
this.pathError = true
|
||||
return null
|
||||
}
|
||||
this.pathError = false
|
||||
this.importFromPath()
|
||||
},
|
||||
validateColumnsConfig() {
|
||||
const existColumnCount = {}
|
||||
for (const column of this.columnsConfig) {
|
||||
if (column.action === 'exist') {
|
||||
if (!column.existColumn) {
|
||||
showWarning(t('tables', 'Please select column for mapping.'))
|
||||
return false
|
||||
}
|
||||
if (existColumnCount[column.existColumn.id] === undefined) {
|
||||
existColumnCount[column.existColumn.id] = 1
|
||||
} else {
|
||||
existColumnCount[column.existColumn.id]++
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.values(existColumnCount).some(count => count > 1)) {
|
||||
showWarning(t('tables', 'Cannot map same exist column for multiple columns.'))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
async importFromPath() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await axios.post(
|
||||
generateUrl('/apps/tables/import/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id),
|
||||
{ path: this.path, createMissingColumns: this.getCreateMissingColumns, columnsConfig: this.columnsConfig },
|
||||
)
|
||||
if (res.status === 200) {
|
||||
this.result = res.data
|
||||
} else {
|
||||
@ -269,13 +403,14 @@ export default {
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async uploadFile() {
|
||||
async importFromUploadFile() {
|
||||
this.loading = true
|
||||
try {
|
||||
const url = generateUrl('/apps/tables/importupload/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id)
|
||||
const formData = new FormData()
|
||||
formData.append('uploadfile', this.selectedUploadFile)
|
||||
formData.append('createMissingColumns', this.getCreateMissingColumns)
|
||||
formData.append('columnsConfig', JSON.stringify(this.columnsConfig))
|
||||
|
||||
const res = await axios.post(url, formData, {
|
||||
headers: {
|
||||
@ -315,6 +450,7 @@ export default {
|
||||
this.pathError = false
|
||||
this.createMissingColumns = true
|
||||
this.result = null
|
||||
this.preview = null
|
||||
this.loading = false
|
||||
},
|
||||
pickFile() {
|
||||
@ -345,6 +481,9 @@ export default {
|
||||
onUploadFileInputChange(event) {
|
||||
this.selectedUploadFile = event.target.files[0]
|
||||
},
|
||||
onUpdateColumnsConfig(event) {
|
||||
this.columnsConfig = event
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
349
src/modules/modals/ImportPreview.vue
Normal file
349
src/modules/modals/ImportPreview.vue
Normal file
@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div>
|
||||
<table class="file_import__preview">
|
||||
<tbody>
|
||||
<tr v-for="(column, colIndex) in columnsConfig" :key="colIndex" class="row">
|
||||
<td class="left space-T">
|
||||
<div class="space-L space-B">
|
||||
<div class="col-4 action-selections">
|
||||
<div class="w-100 w-mobile-85">
|
||||
<div class="column-title">
|
||||
<span>{{ column.title }} ·</span> {{ columnsConfig[colIndex].typeLabel }}
|
||||
</div>
|
||||
<div class="column-values">
|
||||
<span>{{ exampleValues(colIndex) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-center">
|
||||
<NcButton v-if="createMissingColumns" :disabled="column.action !== 'new'" @click="editColumn(colIndex)">
|
||||
<template #icon>
|
||||
<PencilIcon />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-padding-on-mobile">
|
||||
<div class="flex w-100">
|
||||
<NcCheckboxRadioSwitch v-if="createMissingColumns" :checked.sync="columnsConfig[colIndex].action" value="new" name="columnActionSelection" type="radio" @update:checked="$forceUpdate()">
|
||||
{{ t('tables', 'Create new column') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div class="col-4 action-selections mobile-block">
|
||||
<div class="w-100">
|
||||
<NcCheckboxRadioSwitch :checked.sync="columnsConfig[colIndex].action" value="exist" name="columnActionSelection" type="radio" @update:checked="$forceUpdate()">
|
||||
{{ t('tables', 'Import to existing column') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div class="inline-flex">
|
||||
<NcSelect v-model="columnsConfig[colIndex].existColumn"
|
||||
:disabled="columnsConfig[colIndex].action !== 'exist'"
|
||||
:options="existingColumnOptions"
|
||||
:selectable="selectableExistingColumnOption"
|
||||
:aria-label-combobox="t('tables', 'Existing column')" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!createMissingColumns" class="flex w-100">
|
||||
<NcCheckboxRadioSwitch :checked.sync="columnsConfig[colIndex].action" value="ignore" name="columnActionSelection" type="radio" @update:checked="$forceUpdate()">
|
||||
{{ t('tables', 'Ignore column') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
|
||||
import { ColumnTypes } from '../../shared/components/ncTable/mixins/columnHandler.js'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
||||
export default {
|
||||
name: 'ImportPreview',
|
||||
|
||||
components: {
|
||||
NcCheckboxRadioSwitch,
|
||||
NcButton,
|
||||
NcSelect,
|
||||
PencilIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
previewData: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
columns: [],
|
||||
rows: [],
|
||||
}
|
||||
},
|
||||
},
|
||||
element: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
createMissingColumns: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editingColumnIndex: null,
|
||||
editingColumnPreset: null,
|
||||
existingColumns: [],
|
||||
columnsConfig: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['isView']),
|
||||
existingColumnOptions() {
|
||||
if (!this.existingColumns.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.existingColumns.map(column => ({
|
||||
id: column.id,
|
||||
label: column.title,
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
columnsConfig: {
|
||||
handler(newVal) {
|
||||
this.$emit('update:columns', newVal)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.existingColumns = await this.$store.dispatch('getColumnsFromBE', {
|
||||
tableId: !this.isView ? this.element.id : null,
|
||||
viewId: this.isView ? this.element.id : null,
|
||||
})
|
||||
|
||||
for (let i = 0; i < this.previewData.columns.length; i++) {
|
||||
const typeName = this.previewData.columns[i].type + (this.previewData.columns[i].subtype ? `-${this.previewData.columns[i].subtype}` : '')
|
||||
this.columnsConfig.push({
|
||||
titleRaw: this.previewData.columns[i].title,
|
||||
title: this.previewData.columns[i].title,
|
||||
description: t('tables', 'This column was automatically created by the import service.'),
|
||||
action: this.previewData.columns[i].id ? 'exist' : this.createMissingColumns ? 'new' : 'ignore',
|
||||
existColumn: this.previewData.columns[i].id ? this.existingColumnOptions.find(col => col.id === this.previewData.columns[i].id) : null,
|
||||
type: this.previewData.columns[i].type,
|
||||
subtype: this.previewData.columns[i].subtype,
|
||||
typeName,
|
||||
typeLabel: this.getTypeLabel(typeName),
|
||||
numberDecimals: this.previewData.columns[i].numberDecimals,
|
||||
numberPrefix: this.previewData.columns[i].numberPrefix,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
editColumn(index) {
|
||||
this.editingColumnPreset = this.columnsConfig[index]
|
||||
this.editingColumnIndex = index
|
||||
emit('tables:column:create', { isView: this.isView, element: this.element, onSave: this.onSaveColumn, preset: this.editingColumnPreset })
|
||||
|
||||
// Fix modal overlay
|
||||
setTimeout(() => {
|
||||
const modal = document.querySelector('.modal__content.create-column').closest('.modal-mask')
|
||||
document.body.appendChild(modal)
|
||||
}, 500)
|
||||
},
|
||||
onSaveColumn(column) {
|
||||
this.columnsConfig[this.editingColumnIndex] = column
|
||||
this.columnsConfig[this.editingColumnIndex].titleRaw = this.previewData.columns[this.editingColumnIndex].title
|
||||
this.columnsConfig[this.editingColumnIndex].action = 'new'
|
||||
this.columnsConfig[this.editingColumnIndex].typeName = column.type + (column.subtype ? `-${column.subtype}` : '')
|
||||
this.columnsConfig[this.editingColumnIndex].typeLabel = this.getTypeLabel(this.columnsConfig[this.editingColumnIndex].typeName)
|
||||
this.editingColumnPreset = null
|
||||
this.editingColumnIndex = null
|
||||
this.$emit('update:columns', this.columnsConfig)
|
||||
this.$forceUpdate()
|
||||
},
|
||||
getTypeLabel(typeName) {
|
||||
switch (typeName) {
|
||||
case ColumnTypes.TextLine:
|
||||
case ColumnTypes.TextLong:
|
||||
case ColumnTypes.TextRich:
|
||||
return t('tables', 'Text')
|
||||
case ColumnTypes.TextLink:
|
||||
return t('tables', 'Link')
|
||||
case ColumnTypes.Number:
|
||||
return t('tables', 'Number')
|
||||
case ColumnTypes.NumberStars:
|
||||
return t('tables', 'Stars rating')
|
||||
case ColumnTypes.NumberProgress:
|
||||
return t('tables', 'Progress bar')
|
||||
case ColumnTypes.Selection:
|
||||
case ColumnTypes.SelectionMulti:
|
||||
case ColumnTypes.SelectionCheck:
|
||||
return t('tables', 'Selection')
|
||||
case ColumnTypes.Datetime:
|
||||
case ColumnTypes.DatetimeDate:
|
||||
case ColumnTypes.DatetimeTime:
|
||||
return t('tables', 'Date and time')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
selectableExistingColumnOption(option) {
|
||||
return !this.columnsConfig.some(column => column.existColumn?.id === option.id)
|
||||
},
|
||||
exampleValues(colIndex) {
|
||||
let result = ''
|
||||
for (let i = 0; i < this.previewData.rows.length; i++) {
|
||||
// Limit value length to 30 characters
|
||||
let value = this.previewData.rows[i][colIndex]
|
||||
if (value.length > 20) {
|
||||
value = value.substring(0, 20) + '...'
|
||||
}
|
||||
if (i > 0) {
|
||||
result += ', '
|
||||
}
|
||||
result += value
|
||||
}
|
||||
if (result.length > 0) {
|
||||
result += '...'
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file_import__preview {
|
||||
margin: auto;
|
||||
|
||||
& caption {
|
||||
font-weight: bold;
|
||||
margin: calc(var(--default-grid-baseline) * 2) auto;
|
||||
}
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
table {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
td, th {
|
||||
padding-right: 8px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
ul {
|
||||
li {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td:not(:first-child), th:not(:first-child) {
|
||||
padding-right: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 51px;
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
|
||||
&.left {
|
||||
max-width: 300px;
|
||||
}
|
||||
&.right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-selections {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.column-values {
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 641px) {
|
||||
.w-mobile-85 {
|
||||
width: 85%;
|
||||
}
|
||||
.mobile-block {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.v-select.select) {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.content-center {
|
||||
align-content: center;
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@
|
||||
<EditTable :table-id="editTable" :show-modal="editTable !== null" @close="editTable = null" />
|
||||
<TransferTable :table="tableToTransfer" :show-modal="tableToTransfer !== null" @close="tableToTransfer = null" />
|
||||
|
||||
<CreateColumn :show-modal="createColumnInfo !== null" :is-view="createColumnInfo?.isView" :element="createColumnInfo?.element" @close="createColumnInfo = null" />
|
||||
<CreateColumn :show-modal="createColumnInfo !== null" :is-view="createColumnInfo?.isView" :element="createColumnInfo?.element" :preset="createColumnInfo?.preset" :is-custom-save="!!createColumnInfo?.onSave" @save="onSaveNewColumn" @close="createColumnInfo = null" />
|
||||
<EditColumn v-if="columnToEdit" :column="columnToEdit?.column" :is-view="columnToEdit.isView" :element-id="columnToEdit?.elementId" @close="columnToEdit = false" />
|
||||
<DeleteColumn v-if="columnToDelete" :is-view="columnToDelete?.isView" :element-id="columnToDelete?.elementId" :column-to-delete="columnToDelete?.column" @cancel="columnToDelete = null" />
|
||||
|
||||
@ -178,5 +178,13 @@ export default {
|
||||
unsubscribe('tables:context:transfer', context => { this.contextToTransfer = context })
|
||||
unsubscribe('tables:context:delete', context => { this.contextToDelete = context })
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSaveNewColumn(event) {
|
||||
if (this.createColumnInfo?.onSave) {
|
||||
this.createColumnInfo.onSave(event)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user