Понадобилось тут отобразить данные в виде дерева, с возможностью редактировать разные поля, удалять/добавлять строки и т.д. В процессе поиска подходящих компонентов (хотелось найти под material-ui и react) стал пробовать devextreme-reactive. Ньюанс, однако, оказался в том, что devextreme-reactive хочет данные для дерева в виде плоского массива объектов, в каждом из которых указан parent_id «родителя». А GraphQL сервер у меня отдаёт дерево в виде вложенных друг в друга объектов с массивами объектов. Пришлось делать из одного другое — возможно, кому-то пригодится. А может кто-то скажет, что я заморочился не по делу и всё это делается куда проще.
Итак, в ответ на GraphQL запрос (есть тесты, в каждом есть вопросы, для каждого опроса есть несколько вариантов ответов и мы хотим получить всё сразу):
Получаем от сервера ответ вида:
Для нормализации используем normalizr:
В описании схемы, через processStrategy, добавляем в детей свойства pid со ссылками на родителей. К слову, в свежем normalizr изменился способ описания схем, из-за чего примеры с assignEntity, ArrayOf, define (каких много) — практически неактуальны.
Получаем такое:
Нас отсюда интересует только .entities
К слову, в процессе въезжания в normalizr, зачитавшись issues, обнаружил, что далеко не я один пытаюсь использовать его не вполне по назначению (наверное просто потому, что это чуть ли не единственный подобный инструмент). Много народу жаждет всяких фич, чтобы получить результат в как можно более настраиваемом формате. Но авторы — кремень.
В силу вышесказанного, результат работы normalizr придётся рихтовать при помощи flat (рекурсивно разворачиваем до нужного уровня вложенности):
Получаем следующее:
Избавляемся от индексов:
Получаем:
Можно было бы почистить оставшиеся тут вложенные массивы questions, answers, но это уже мелочи — на отображение не влияют. А __typename нужны, чтобы потом при редактировании понимать, с чем имеем дело.
В компоненте результат обрабатывается так, как это показано у них в примере:
Вроде бы альтернативой всему вышеописанному может являться чтение непосредственно содержимого GraphQL store (в клиенте Apollo) — там всё тоже должно быть уже плоским. Но, честно говоря, я не нашёл, как это можно стандартным способом сделать, да и не очень уверен, что формат, в котором там хранятся данные не изменится в новых версиях.
Итак, в ответ на GraphQL запрос (есть тесты, в каждом есть вопросы, для каждого опроса есть несколько вариантов ответов и мы хотим получить всё сразу):
query TestQuery { tests { id title questions { id title answers { id title } } } }
Получаем от сервера ответ вида:
Заголовок спойлера
{ "data": { "tests": [ { "id": "test_1", "title": "Test 1", "questions": [ { "id": "question_1", "title": "Question 1 (for t1)", "answers": [ { "id": "answer_1", "title": "Answer 1 (for q1)" }, { "id": "answer_2", "title": "Answer 2 (for q1)" } ] }, { "id": "question_2", "title": "Question 2 (for t1)", "answers": [ { "id": "answer_1_2", "title": "Answer 1 (for q2)" } ] } ] }, { "id": "test_2", "title": "Test 2", "questions": [ { "id": "question_1_2", "title": "Question 1 (for t2)", "answers": [] } ] }, { "id": "test_3", "title": "Test 3", "questions": [] } ] } }
Для нормализации используем normalizr:
В описании схемы, через processStrategy, добавляем в детей свойства pid со ссылками на родителей. К слову, в свежем normalizr изменился способ описания схем, из-за чего примеры с assignEntity, ArrayOf, define (каких много) — практически неактуальны.
const answerSchema = new schema.Entity('answers',{}, { processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} } } ) const questionSchema = new schema.Entity('questions',{ answers:[answerSchema]}, { processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} } }, ) const testSchema = new schema.Entity('tests',{questions:[questionSchema]}, { processStrategy: (entity, parent, key) => { return { ...entity, pid: 0 } } } ) const nRes = normalize(result.data, {tests: [testSchema]})
Получаем такое:
Заголовок спойлера
{ "entities": { "answers": { "answer_1": { "id": "answer_1", "title": "Answer 1 (for q1)", "__typename": "Answer", "pid": "question_1" }, "answer_2": { "id": "answer_2", "title": "Answer 2 (for q1)", "__typename": "Answer", "pid": "question_1" }, "answer_1_2": { "id": "answer_1_2", "title": "Answer 1 (for q2)", "__typename": "Answer", "pid": "question_2" } }, "questions": { "question_1": { "id": "question_1", "title": "Question 1 (for t1)", "answers": [ "answer_1", "answer_2" ], "__typename": "Question", "pid": "test_1" }, "question_2": { "id": "question_2", "title": "Question 2 (for t1)", "answers": [ "answer_1_2" ], "__typename": "Question", "pid": "test_1" }, "question_1_2": { "id": "question_1_2", "title": "Question 1 (for t2)", "answers": [ ], "__typename": "Question", "pid": "test_2" } }, "tests": { "test_1": { "id": "test_1", "title": "Test 1", "questions": [ "question_1", "question_2" ], "__typename": "Test", "pid": 0 }, "test_2": { "id": "test_2", "title": "Test 2", "questions": [ "question_1_2" ], "__typename": "Test", "pid": 0 }, "test_3": { "id": "test_3", "title": "Test 3", "questions": [ ], "__typename": "Test", "pid": 0 } } }, "result": { "tests": [ "test_1", "test_2", "test_3" ] } }
Нас отсюда интересует только .entities
const normalized = { entities: nRes.entities }
К слову, в процессе въезжания в normalizr, зачитавшись issues, обнаружил, что далеко не я один пытаюсь использовать его не вполне по назначению (наверное просто потому, что это чуть ли не единственный подобный инструмент). Много народу жаждет всяких фич, чтобы получить результат в как можно более настраиваемом формате. Но авторы — кремень.
В силу вышесказанного, результат работы normalizr придётся рихтовать при помощи flat (рекурсивно разворачиваем до нужного уровня вложенности):
const flattened = flatten({ entities: nRes.entities }, { maxDepth: 3 })
Получаем следующее:
Заголовок спойлера
{ "entities.answers.answer_1": { "id": "answer_1", "title": "Answer 1 (for q1)", "__typename": "Answer", "pid": "question_1" }, "entities.answers.answer_2": { "id": "answer_2", "title": "Answer 2 (for q1)", "__typename": "Answer", "pid": "question_1" }, "entities.answers.answer_1_2": { "id": "answer_1_2", "title": "Answer 1 (for q2)", "__typename": "Answer", "pid": "question_2" }, "entities.questions.question_1": { "id": "question_1", "title": "Question 1 (for t1)", "answers": [ "answer_1", "answer_2" ], "__typename": "Question", "pid": "test_1" }, "entities.questions.question_2": { "id": "question_2", "title": "Question 2 (for t1)", "answers": [ "answer_1_2" ], "__typename": "Question", "pid": "test_1" }, "entities.questions.question_1_2": { "id": "question_1_2", "title": "Question 1 (for t2)", "answers": [ ], "__typename": "Question", "pid": "test_2" }, "entities.tests.test_1": { "id": "test_1", "title": "Test 1", "questions": [ "question_1", "question_2" ], "__typename": "Test", "pid": 0 }, "entities.tests.test_2": { "id": "test_2", "title": "Test 2", "questions": [ "question_1_2" ], "__typename": "Test", "pid": 0 }, "entities.tests.test_3": { "id": "test_3", "title": "Test 3", "questions": [ ], "__typename": "Test", "pid": 0 } }
Избавляемся от индексов:
Object.keys(flattened).forEach( (key)=> rows.push(flattened[key]) )
Получаем:
Заголовок спойлера
[ { "id": "answer_1", "title": "Answer 1 (for q1)", "__typename": "Answer", "pid": "question_1" }, { "id": "answer_2", "title": "Answer 2 (for q1)", "__typename": "Answer", "pid": "question_1" }, { "id": "answer_1_2", "title": "Answer 1 (for q2)", "__typename": "Answer", "pid": "question_2" }, { "id": "question_1", "title": "Question 1 (for t1)", "answers": [ "answer_1", "answer_2" ], "__typename": "Question", "pid": "test_1" }, { "id": "question_2", "title": "Question 2 (for t1)", "answers": [ "answer_1_2" ], "__typename": "Question", "pid": "test_1" }, { "id": "question_1_2", "title": "Question 1 (for t2)", "answers": [ ], "__typename": "Question", "pid": "test_2" }, { "id": "test_1", "title": "Test 1", "questions": [ "question_1", "question_2" ], "__typename": "Test", "pid": 0 }, { "id": "test_2", "title": "Test 2", "questions": [ "question_1_2" ], "__typename": "Test", "pid": 0 }, { "id": "test_3", "title": "Test 3", "questions": [ ], "__typename": "Test", "pid": 0 } ]
Можно было бы почистить оставшиеся тут вложенные массивы questions, answers, но это уже мелочи — на отображение не влияют. А __typename нужны, чтобы потом при редактировании понимать, с чем имеем дело.
В компоненте результат обрабатывается так, как это показано у них в примере:
... <CustomTreeData getChildRows={getChildRows} /> ... const getChildRows = (currentRow, rootRows) => { const childRows = rootRows.filter(r => r.pid === (currentRow ? currentRow.id : 0)); const res = childRows.length ? childRows : null return res } ...
Вроде бы альтернативой всему вышеописанному может являться чтение непосредственно содержимого GraphQL store (в клиенте Apollo) — там всё тоже должно быть уже плоским. Но, честно говоря, я не нашёл, как это можно стандартным способом сделать, да и не очень уверен, что формат, в котором там хранятся данные не изменится в новых версиях.